在我另一篇博客SSM之Spring 02 —— 动态代理、AOP、Spring-MyBatis、声明式事务中有简单描述代理模式和AOP原理,当时自己也还是有些没明白,后面在面试过程中被问到,这里详细说明一下。
AOP是什么就不再说明了,主要解释下AOP的原理——动态代理。
动态代理
和静态代理不同,动态代理是动态生成代理对象,克服了静态代理的问题——不同目标对象都需要写一个代理类。
动态代理主要有两种方式:JDK的动态代理和cglib代理。
JDK的动态代理
它是对对象进行代理,底层利用反射机制实现,同时需要我们自定义一个实现了InvocationHandler接口的类,这个接口中的invoke方法就是AOP的本质。
下面我们通过代码来进行理解。
1、定义好UserDao以及实现类,这是我们常见的方式。
//为了方便就不写参数了
public interface UserDao {
boolean addUser();
boolean deleteUser();
}
public class UserDaoImpl implements UserDao {
@Override
public boolean addUser() {
System.out.println("addUser();");
return false;
}
@Override
public boolean deleteUser() {
System.out.println("deleteUser();");
return true;
}
}
2、写一个测试,具体解释看注解。可以看到Proxy和InvocationHandler都是reflect包下的,说明底层肯定是反射。
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
public class Test {
public static void main(String[] args) {
//创建好目标对象
UserDao ud = new UserDaoImpl();
//这个就是我们自定义的一个实现类,里面invoke方法就是AOP的原理。
InvocationHandler ih = new MyInvocationHandler(ud);//将ud作为目标对象传进去
/*
这个方法返回一个代理对象,我们会在代理对象中做一些其他操作
newProxyInstance(ClassLoader, interfaces, InvocationHandler)
1、ClassLoader:目标对象的类加载器,代码是固定的。
2、interfaces:目标对象的接口数组,代码也是固定的,都是通过反射获得。
3、InvocationHandler:这是关键点,AOP能实现方法的增强就在这个接口里。需要自己定义。
*/
Object newProxyInstance = Proxy.newProxyInstance(ud.getClass().getClassLoader(),
ud.getClass().getInterfaces(), ih);
//执行代理对象被增强后的方法(增强的意思是在原有方法的基础上添加了其他效果,同时不影响原有业务,同Spring AOP的事务)
UserDao proxyUserDao = (UserDao)newProxyInstance;
proxyUserDao.addUser();
System.out.println("--------------");
proxyUserDao.deleteUser();
}
}
3、自定义的实现类,这是实现AOP增强方法的关键。
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class MyInvocationHandler implements InvocationHandler {
private Object object;
public MyInvocationHandler(Object object) {
this.object = object;
}
/**
*
* @param proxy 代理对象,就是我们从外部传进来的,但一般不用这个,会用我们自定义的object成员。
* @param method 目标对象的方法对象,是JDK传过来的。
* @param args 目标对象方法的参数对象。
* @return 对应于我们目标对象的方法的返回值
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String before = "";
String after = "";
//这里通过反射来判断UserDao的两个方法,如果不判断,那么就会针对目标对象的所有方法进行增强。
if(method.getName().equals("addUser")){
before = "前置通知:addUser()方法执行 之前";
after = "后置通知:addUser()方法执行 之后";
}
if(method.getName().equals("deleteUser")){
before = "前置通知:deleteUser()方法执行 之前";
after = "后置通知:deleteUser()方法执行 之后";
}
//下面这样就实现了和Spring AOP 一样的效果:方法的增强,即在方法执行前做一些其他事,且不影响到原有业务。
System.out.println(before);
Object result = method.invoke(object, args);//通过反射调用目标对象的方法
System.out.println(after);
return result;
}
}
4、运行结果:
为什么说JDK是面向接口的?
这里的接口不是指InvocationHandler,而是指UserDao,我们看看通过
newProxyInstance
得带的代理类的父类。
显然,只要我们通过JDK的Proxy代理获得的类,都会继承Proxy,那么我们是如何能够让这个类有UserDaoImpl的方法的呢?因为这个类继承了UserDao。
这也就解释了,为什么我们说JDK动态代理是面向接口的。因为Java是单继承,而我们通过JDK动态代理得到的代理都会继承Proxy,同时我们要表明这个代理对象是UserDaoImpl的实例,那么就只能通过接口的方式。
因此,如果我们有实现类(例如UserDaoImpl),就可以考虑到JDK动态代理。这也是Spring AOP的默认实现方式,我们一般都是用实现类。
cglib
和JDK动态代理不同,cglib(code generator library 代码生成库)代理的是字节码对象(即Class),而JDK动态代理代理的是对象。
步骤:
- cglib生成一个字节码对象(即Class对象),内容是空的。
- 设置字节码对象的父类为目标对象,即UserDaoImpl。
- 通过字节码对象中继承了目标对象的方法,即addUser,进行回调,调用父类的addUser,并在执行其方法前完成方法增强。
- 创建代理对象,通过代理对象执行Class的方法。
注意:cglib是Spring中的,所以需要导入Spring核心包才能使用。
导入进来
下面进行代码说明:
这里面CallBack就是AOP的原理,它通过子类调用父类UserDaoImpl的addUser方法,并在调用时完成增强。
package test02_AOP.cglib;
import org.springframework.cglib.proxy.Callback;
import org.springframework.cglib.proxy.Enhancer;
public class Test {
public static void main(String[] args) {
Enhancer enhancer = new Enhancer();//空的字节码对象
enhancer.setSuperclass(UserDaoImpl.class);//设置父类
Callback callback = new MyCallback();
enhancer.setCallback(callback);//设置回调
UserDao userDao = (UserDao) enhancer.create();//用代理类Enhancer 创建代理对象
userDao.addUser();
}
}
package test02_AOP.cglib;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
//本来是继承Callback接口,表示回调
//但Callback接口里什么都没有,只是起到一个标志作用,所以继承其子类MethodInterceptor
//MethodInterceptor接口仅有一个方法,拦截我们待增强的方法
public class MyCallback implements MethodInterceptor {
/**
*
* @param proxy 代理对象,即前面图里面的 class XX extends UserDaoImpl的XX对象
* @param method 目标对象的方法
* @param args 目标对象中的 方法对象 的参数对象(相当于addUser方法的参数)
* @param methodProxy 代理对象中的 方法代理对象(相当于父类UserDaoImpl的addUser方法)
* @return 返回代理对象方法的结果
* @throws Throwable
*/
@Override
public Object intercept(
Object proxy, Method method, Object[] args, MethodProxy methodProxy
) throws Throwable {
System.out.println("前置通知");
//执行原来的方法
Object object = methodProxy.invokeSuper(proxy, args);//这里不能用invoke,相当于前面图里super.addUser()
System.out.println("后置通知");
return object;
}
}
结果:
总结
AOP在Spring中实现:
1、注解方式
- 有一个注解@EnableAspectJAutoProxy(ProxyTargetClass = false)
- 默认为false。如果是实现类(实现),就会用jdk,如果没有实现接口 ,就会用 cglib。
- 如果是true。无论是否实现接口,都会使用 cglib。
2、XML方式
3、JDK和cglib小结
- JDK动态代理是面向接口的。
- cglib是通过字节码底层继承被代理类,然后重写父类方法(如果被代理类继承final则会失败)
- 如果被代理的对象是一个实现类(例如UserDaoImpl),那么Spring AOP会默认用JDK动态代理,否则使用cglib。
- 性能比较:(参考博客 https://blog.csdn.net/xlgen157387/article/details/82497594)
-
关于两者之间的性能的话,JDK动态代理所创建的代理对象,在以前的JDK版本中,性能并不是很高,虽然在高版本中JDK动态代理对象的性能得到了很大的提升,但是他也并不是适用于所有的场景。主要体现在如下的两个指标中:
1、CGLib所创建的动态代理对象在实际运行时候的性能要比JDK动态代理高不少,有研究表明,大概要高10倍;
2、但是CGLib在创建对象的时候所花费的时间却比JDK动态代理要多很多,有研究表明,大概有8倍的差距;
3、因此,对于singleton的代理对象或者具有实例池的代理,因为无需频繁的创建代理对象,所以比较适合采用CGLib动态代理,反正,则比较适用JDK动态代理。
-
经过原博主的实验后得出结果:(具体请参考原博客)
最终的测试结果大致是这样的,在1.6和1.7的时候,JDK动态代理的速度要比CGLib动态代理的速度要慢,但是并没有教科书上的10倍差距,在JDK1.8的时候,JDK动态代理的速度已经比CGLib动态代理的速度快很多了。
-
原博主下的一个评论(曾经的随性的回复)
jdk的版本优化中主要是针对虚拟机对反射调用的优化,在jdk1.6中,我们采用JDK代理的方式来生成动态代理类,反射方法的调用在15次以内是调用本地方法,即是java到c++代码转换的方法,这种方式比直接生成字节码文件要快的多,而在15次之后则开始使用java实现的方式。而在1.8的版本优化中,反射调用的次数达到阈值[也就是发射调用的类成为热点时]之后采用字节码的方式,因为字节码的 方式只有在第一次生成字节码文件时比较消耗时间。
-