最近在看Spring AOP,里面使用到了动态代理,自己对两种代理模式进行了学习,这里做个总结。本文主要介绍动态代理,开始之前还是先介绍一下代理的相关内容。
代理分为静态代理和动态代理,无论哪种代理,它们都是为了对目标方法进行增强,让增强的动作和目标动态分开,达到解耦的目的。目标类可以只关注业务,而不关注其他的东西,比如添加用户时,只关注业务实现,不关注谁调用相关的日志输出等操作。
其实就是创建一个代理类去继承目标类,在代理类中重写目标方法,添加增强动作的同时对目标方法进行使用。下面是一个简单的静态代理示例,这里没有使用接口,直接使用类继承。代理类中包含了目标类的引用,并通过重写目标方法添加增强目的。
// 目标类
public class UserService {
public void delUser(String name) {
xxx;
}
}
// 代理类
public class UserServiceProxy extend UserService{
private UserService userService;
public UserServiceProxy(UserService userService) {
this.userService = userService;
}
@Override
public void delUser(String name) {
System.out.println("我要增强了")
userService.delUser();
System.out.println("我增强完了")
}
}
// 主函数
public static void main(String[] args) {
UserService userService = (UserService)new UserServiceProxy(new UserService);
userService.delUser("tom);
}
静态代理的优点其实就是代理的优点,将业务逻辑和增强逻辑解耦合。缺点是系统中如何有很多需要增强的类,则要创建很多代理对象。另外如果一个类中有很多方法需要增强,则可能会导致很多重复的代码,即使进行重构也会有重复。总结起来就是代码的可重用性弱。
动态代理是使用反射和字节码的技术,在运行期创建指定接口和类的子类以及其实例对象的技术,通过动态代理可以对代码进行增强。动态代理并不存在代理类, 代理对象直接由代理生成工具动态生成。
动态代理实现了只需要将被代理对象作为参数传入代理类就可以获取代理类对象,从而实现类代理,具有较强的灵活性。此外动态代理的服务内容不需要像静态代理一样写在每个代码块中,只需要写在invoke()(JDK动态代理)或intercept()(CGLib动态代理)方法中即可,降低了代码的冗余度。缺点是执行效率相对较低,需要动态修改字节码,同反射等技术的修改存在安全问题。
Spring或者说是Java的动态代理有两种模式,一种是JDK动态代理,还有一种是CGLib动态代理。
JDK动态代理是利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。JDK的动态代理机制是委托机制。
JDK动态代理只能对实现了接口的类生成的代理,而不能针对类,这是因为JDK动态代理与静态代理类似,本质上还是生成一个子类来模拟“代理”,但是采用了动态生成的形式,而这个子类要继承Proxy类获得相关的方法(内部维护了一个InvocationHandler对象进行invoke操作),而Java是单继承多实现的形式,继承了Proxy类就不能再继承被代理类了,这也是为什么只能对实现了接口的类生成代理。
这样子也就引入了另外一个问题,为什么JDK动态代理设计时要继承Proxy,而不将Proxy改成接口,这个没有明确的答案,就好像HashMap能存null,而HashTable不能,然后大家猜测,最后设计者说我就是看null不爽。我查阅了网上资料,主要有这几点,首先是基于继承的方式可以减少产生代理类时产生的性能消耗,我们知道有个InvocationHandler对象放在Proxy类里面,如果不放在里面,则每个代理类都要去持有这个,另外如果要代理的原始类不是接口的话,代理类会继承原始类的字段,这些字段没有用。还有就是代理的原始类中如果有final的方法,动态生成的类是无法覆盖这个方法的。
那接下来我们看一个JDK动态代理的示例。比较简单代理类实现InvocationHandler接口的Invoke()方法,方法内实现增强逻辑以及调用原始类的目标方法。原始类的实例作为属性保存在代理类中。
public interface UserService {
public void addUser(String name);
}
public class UserServiceImpl implements UserService {
@Override
public void addUser(String name) {
System.out.println("调用了UserServiceImpl.addUser()方法!");
}
}
public class JDKProxy implements InvocationHandler {
private Object targetObject;
public Object newProxy(Object obj) {
this.targetObject = obj;
return Proxy.newProxyInstance(targetObject.getClass().getClassLoader(),
targetObject.getClass().getInterfaces(), this);
}
// proxy - 代理的真实对象。
// method - 所要调用真实对象的某个方法的 Method 对象
// args - 所要调用真实对象某个方法时接受的参数
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("增强");
Object ret = method.invoke(targetObject, args);
return ret;
}
public static void main(String[] args) {
//loader - 一个 ClassLoader 对象,定义了由哪个 ClassLoader 对象来对生成的代理对象进行加载。
//interfaces - 一个 Interface 对象的数组,表示的是我将要给我需要代理的对象提供一组什么接口,如果我提供了一组接口给它,那么这个代理对象就宣称实现了该接口(多态),这样我就能调用这组接口中的方法了
// h - 一个 InvocationHandler 对象,表示的是当我这个动态代理对象在调用方法的时候,会关联到哪一个 InvocationHandler 对象上
((UserService)new JDKProxy().newProxy(new UserServiceImpl())).addUser("tom");
}
}
增强
调用了UserServiceImpl.addUser()方法!
Process finished with exit code 0
这里要说明一下invoke()方法里面的proxy参数,使用较少,但是为什么要传入呢?第一点是可以使用反射获取代理对象的信息(也就是proxy.getClass().getName())。第二点 可以将代理对象返回以进行连续调用,这就是proxy存在的目的,因为this并不是代理对象。
CGLib动态代理是利用ASM开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。CGLib的动态代理机制是继承机制,通过“继承”可以继承父类所有的公开方法,然后可以重写这些方法,在重写时对这些方法增强,这就是CGLib的思想。根据里氏代换原则(LSP),父类需要出现的地方,子类可以出现,所以CGLib实现的代理也是可以被正常使用的。
CGlib是针对类实现代理,对指定的类生成一个子类,并覆盖其中的方法,因此代理类和代理类是继承关系,所以代理类是可以赋值给被代理类的,如果被代理类有接口,那么代理类也可以赋值给接口。注意CGLib不能代理final修饰的类和方法。
来看一个CGLib的示例,和JDK类似,有两个关键元素:
①Enhancer:来指定要代理的目标对象,实际处理代理逻辑的对象,最终通过调用create()方法得到代理对象、对这个对象所有的非final方法的调用都会转发给MethodInterceptor
②MethodInterceptor:动态代理对象的方法调用都会转发到intercept方法进行增强
// 接口与目标类的与JDK动态代理里面的示例一样,不多一起贴近来
public interface UserService {
public void addUser(String name);
}
public class UserServiceImpl implements UserService {
@Override
public void addUser(String name) {
System.out.println("调用了UserServiceImpl.addUser()方法!");
}
}
public class CGLibProxy implements MethodInterceptor{
private Object targetObject;
public Object createProxyObject(Object obj) {
this.targetObject = obj;
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(obj.getClass());
enhancer.setCallback(this);
return enhancer.create();
}
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("增强");
return method.invoke(targetObject, objects);
}
public static void main(String[] args) {
((UserService) new CGLibProxy().createProxyObject(new UserServiceImpl())).addUser("tom");;
}
}
增强
调用了UserServiceImpl.addUser()方法!
Process finished with exit code 0
从表面上就可以看出,JDK动态代理性能主要受制于反射时,即实际运行时性能要低。CGLib主要受制修改类的字节码生成子类时,即在创建对象的时候所花费的时间要多。因此在执行次数较少时,修改字节码影响较大,CGLib性能差一些,大量执行情况下CGLib性能更好。这只是理论上的分析,JDK6时JDK动态代理性能比较差,现在版本已经进行了大量优化,追上甚至赶超CGLib,但是CGLib似乎很难前进,感兴趣的可以去研究一下JDK对动态代理的优化。
Spring同时使用了两种动态代理机制,依据如下:
①当Bean实现接口时,Spring就会用JDK的动态代理
②当Bean没有实现接口时,Spring使用CGlib是实现
③可以强制使用CGlib(@EnableAspectJAutoProxy(proxyTargetClass = true))
动态代理虽然实现了代码的解耦,但是还是需要自己去生成代理对象,自己手写拦截器,在拦截器里自己手动的去把要增强的内容和目标方法结合起来。可以使用Spring AOP来完成这些繁杂的操作,只要在配置文件里进行配置,配置好后让Spring去帮你生成代理对象,按照你的配置把增强的内容和目标方法结合起来。