关于java:动态代理JDK-和-Cglib

5次阅读

共计 6741 个字符,预计需要花费 17 分钟才能阅读完成。

动静代理的出镜率十分高,不论是在框架中的利用,还是在面试中,都频繁呈现。

因而,弄懂动静代理的前因后果,是了解框架的根底,也是进阶路上绕不过来的垫脚石。

一、动态代理

先聊下动态代理,也就是代理模式的呈现解决了什么问题?

现实生活中,保姆是家庭事务的代理,经纪人是明星的代理,代理服务于被代理人,个别是在某类事物上更业余的人。

在代码中,来模仿下雇佣保洁来清扫房子的场景,CleanProxyPerson 是保洁,Person 代表业主,CleanThing 代表协商好的清洁范畴,如下所示,运行后,cleanHouse() 办法会被加强,不在接口中的 cleanSafeBox() 是无奈被代理的。

接口的一个作用,是作为协定,定义职责,例如把生孩子这种事件也放到接口外面代理,显著是不适合的。

public class MainOfUndynamicProxy {public static void main(String[] args) {CleanThing person = new Person();
        CleanThing proxyPerson = new CleanProxyPerson(person);
        proxyPerson.cleanHouse();}
}
// 业主
public class Person implements CleanThing{
    
    @Override
    public void cleanHouse() {System.out.println("本人清扫下房间的外围区域");
    }

    public void cleanSafeBox() {System.out.println("清扫下保险箱");
    }
  
    public Person getChild(){return new Person();
    }
}

public class CleanProxyPerson implements CleanThing {

    private CleanThing cleanThing;

    public CleanProxyPerson(CleanThing cleanThing) {this.cleanThing = cleanThing;}

    @Override
    public void cleanHouse() {System.out.println("----- 整体清洁下(专业人士)-----");
        cleanThing.cleanHouse();}
}

public interface CleanThing {void cleanHouse();
}

代理的利用,以接口为纽带,与指标类解耦的同时,达到了加强指标类的目标。

理论业务场景中,接口与实现各式各样,如果都有加强需要,例如做调用统计,耗时统计等,用这种形式须要一个个写,显然是不事实的。

二、动静代理

动静代理解决的就是工作量的问题。

一般来说,要省掉编写代码的工作,须要在编译时或运行时使用点黑科技。

先看下对象实例化的过程。

如下所示,要实例化 Person 对象,首先 Person.java 被编译成 Person.class 文件,接着被 ClassLoader 加载到 JVM 中,生成了 Class<Person>,放在办法区中,再依据 Class<Person> 实例化成 person 对象,放在了堆中。

Class<Order>java.lang 中的类,形容的是类的原始信息,例如类定义了哪些成员变量,办法,字段等。

所以,要实例化一个对象,须要拿到它的 Class<>,个别状况下,是从 .class 文件中加载的。

是否能够凭空发明进去呢?

答案是能够,因为指标类与代理类的信息基本一致,间接从接口的 Class<> 中复制一份便可。

JDK 动静代理

这就是 JDK 动静代理的核心思想。

其中,实现这个过程的外围类为 ProxyInvocationHandler

Proxy 中的 getProxyClass() 用来取得 Class<>

InvocationHandler 是一个钩子,代理对象生成后,执行办法时会先回调 InvocationHandlerinvoke()

来看下具体的写法:

public class MainOfJDKProxy {public static void main(String[] args) {IOrder order = new Order(); // 指标类
        LogInvocationHandler handler = new LogInvocationHandler(order); // 回调函数
        Class<?> proxyClass = Proxy.getProxyClass(order.getClass().getClassLoader(), order.getClass().getInterfaces()); // 这里就是 copy Class<>
        Constructor<?> constructor = proxyClass.getConstructor(InvocationHandler.class);
        IOrder proxyOrder = (IOrder) constructor.newInstance(handler);
        proxyOrder.run();}
}

public interface IOrder {void run();
}

public class Order implements IOrder {

    @Override
    public void run() {System.out.println("Order run");
    }
}

public class LogInvocationHandler implements InvocationHandler {

    private Object targetObject;

    public LogInvocationHandler(Object targetObject){this.targetObject = targetObject;}

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {System.out.println("----- LogInvocationHandler begin -----");
        method.invoke(targetObject, args);
        System.out.println("----- LogInvocationHandler end -----");
        return null;
    }
}

其实还有个更简便的办法,用 Proxy.newProxyInstance() 能够间接返回代理对象,底层原理相似。

public class MainOfJDKProxy {public static void main(String[] args) {IOrder order = new Order(); // 指标类
        LogInvocationHandler handler = new LogInvocationHandler(order); // 回调函数
        IOrder proxyOrder = (IOrder) Proxy.newProxyInstance(order.getClass().getClassLoader(), order.getClass().getInterfaces(), handler);
        proxyOrder.run();}
}

所以,代理模式是为了加强业务代码,JDK 用了反射机制,复制类的元信息来实例化代理类,缩小了手动编写的问题。

然而,指标类须要实现接口这个限度在应用上还是有很大的局限性,是否有其它解法呢?

Cglib:Code Generation Library

cglib 用继承指标类的形式,给出了本人的答案。

核心思想是作为子类来加强指标类的办法,而不是通过实现接口的模式。

其中,外围类是 EnhancerMethodInterceptor,相当于 ProxyInvocationHandler

如下所示,Enhancer 设置父类和回调函数,创立出代理对象。

        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(Item.class); // 设置父类
        enhancer.setCallback(new LogMethodInterceptor(new Item())); // 回调函数
        Item proxyItem = (Item) enhancer.create();
        proxyItem.run();

回调函数的实现是这样的:

public class LogMethodInterceptor implements MethodInterceptor {

    private Object targetObject;

    public LogMethodInterceptor(Object targetObject){this.targetObject = targetObject;}

    @Override
    public Object intercept(Object proxyObject, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {System.out.println("----- LogMethodInterceptor begin -----");
        Object result = method.invoke(targetObject, args);
        System.out.println("----- LogMethodInterceptor end -----");
        return result;
    }
}

这个写法跟 JDK 的相似,在创立的时候,将指标对象存为成员变量,真正回调的时候,通过反射 invoke 对应的办法。

intercept() 有 4 个入参,proxyObject 是代理,method 是指标类的办法,args 是办法入参,methodProxy 是代理办法。

其它的都好了解,根本跟 JDK 的回调办法统一,但多了个 methodProxy,为什么要有它呢?

如果这样写,其实就是调用的代理类的代理办法,而不是间接调用指标类,invokeSuper()invoke() 的区别在于是否持续走 intercept(),所以,invoke() 会造成一个死循环,相当于递归调用了。

public class LogSuperMethodInterceptor implements MethodInterceptor {

    @Override
    public Object intercept(Object proxyObject, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {System.out.println("----- LogMethodInterceptor begin -----");
        Object result = methodProxy.invokeSuper(proxyObject, args);
//        Object result = methodProxy.invoke(obj, args); // 死循环
        System.out.println("----- LogMethodInterceptor end -----");
        return result;
    }
}

如图所示,首先调用的是代理类的 run() 办法,而后回调到拦截器的 intercept() 办法,这步是 cglib 主动实现的。

所以在 intercept() 中如果持续调用代理办法,就会走图中的③,而后主动又走②,导致死循环。

③ 不让用,那 ④ 和 ⑤ 的区别是啥呢?看起来都是调用指标类的办法。

这的本质区别,就是通过子类去调用父类办法与间接调用父类办法的区别,体现在了关键字 this 上。

如下所示,如果 run() 调用 runElse() 是子类调用过去的,这里的 this 是指的子类,而不是父类,这一点会比拟反直觉。

public class Item {public void run(){System.out.println("item run");
        this.runElse();}

    public void runElse(){System.out.println("item run else");
    }
}

所以最终体现的就是嵌套调用的时,还会不会走到代理的办法上,进而又走到回调办法外面,这样就会让整个调用链路上的办法都被加强了。

弄懂这个后,来持续看看底层实现,在首行加上面这行代码,能够看到编译后的代理类文件。

System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "target/temp");

能够看到,代理类继承了指标类 Order,成员变量中能够看到传入的拦截器。

图一发现两个带有 FastClass 文件是用来干啥的?

FastClass 机制,用另外一种思路来达到反射调用的成果。

通过反射调用对象的具体方法时,个别这么写:

public class MainOfReflection {public static void main(String[] args) throws Throwable {Person person = new Person();
        invokeByName(person, "cleanHouse");
        invokeByName(person, "cleanSafeBox");

        Order order = new Order();
        invokeByName(order, "run");
    }

    public static void invokeByName(Object object, String methodName) throws Throwable{Class<?> aClass = Class.forName(object.getClass().getName());
        Method method = aClass.getMethod(methodName, new Class[0]);
        method.invoke(object, null);
    }
}

然而,反射的操作是比拟重的,个别要通过鉴权,native 等的调用。

FastClass 用空间换工夫的思路,将要调用的办法存下来,放到文件中。

生成的文件,记录了一份类的办法列表,能够设想成数据库记录,办法名就是索引,这样要运行指定办法名的时候,依据办法名去调用对应的办法。

    public static void invokeByName(Person object, String methodName){if ("cleanHouse".equals(methodName)) {object.cleanHouse();
        } else if ("cleanSafeBox".equals(methodName)) {object.cleanSafeBox();
        }
    }

这里不是间接依据办法名称判断,办法名是能够反复的,所以依据办法签名做了一层映射,用映射后的 Id 来示意。

类的办法个数是无限的,提前记录并给与索引,防止应用过重的反射机制,属于将空间换工夫玩出了花来。

总结下,cglib 通过继承指标类,成为指标类的子类来扩大性能,理论调用的过程中,还用了 FastClass 来优化性能。

三、JDK vs cglib

接下来,比照下 JDK 形式和 cglib 的形式。

最直观的,对指标类的限度上,JDK 的形式要求必须实现接口,无接口不代理,而 cglib 则另辟蹊径,用继承的形式来实现代理,也有肯定的限度,例如被 final 润饰的类无奈代理。

其次,在实现机制上,JDK 用了反射机制运行指标办法,而 cglib 则是通过 FastClass 的形式,优化调用过程。

性能上,cglib 实践上讲是更快的,当在个别业务场景中,类的数量无限,个别不会有太大的差距。

应用形式上,JDK 的形式是原生的,写法简略,不必引入其它依赖,能够平滑降级,而 cglib 是三方包,须要投入更多的保护老本。

具体选型过程,在可维护性、可靠性、性能、以及工作量上要多加考量。

四、利用场景

对实现原理了解后,再看平时利用的货色,比方 RPC 调用时的接口,比方写 mybatis 为何只写接口就能够,不必写实现?还有 spring AOP 机制,都是基于动静代理实现的。

在这根底上,更高阶的玩法是代理链,例如 mybatis 外面的拦截器,多个的状况下,就是代理套代理,层层代理,跟套娃一样。

五、总结

从代理模式,到动静代理,加强代码的同时,解决了代码侵入,与繁琐工作的问题。JDKcglib 从不同视角给出了解决方案,也有着不同的劣势和局限性。

其余

文中示例代码都在 github 上,欢送把玩:

https://github.com/JayeGuo/Ja…

正文完
 0