AOP,面向切面编程,是Spring框架中的核心思想之一;在Spring中是通过动态代理来实现的,在目标类的基础上增加切面逻辑,对原对象进行增强;
SpringAOP的源码中用到了两种动态代理来实现拦截切入功能:JDK动态代理和CGlib动态代理,两种方法的适用条件和效率各有不同,各有优劣;
本来准备写一篇关于SpringAOP相关的文章的,这篇文章介绍,作为Spring AOP的基础知识,介绍代理设计模式以及Java中的几种不同的代理模式的实现:静态代理、JDK动态代理和CGlib代理,将3种代理分别做一个比较;最后介绍SpringAOP实现原理;
代理模式是一种设计模式,提供了对目标对象额外的访问方式,即通过代理对象访问目标对象,这样可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能;简言之,代理模式就是设置一个中间代理来控制访问原目标对象,以达到增强原对象的功能和简化访问方式;代理模式UML类图如下(来自维基百科代理模式);
[delegate]:授(权),把……委托给他人;委派……为代表,任命;
关于代理模式,如果觉得"代理"这个词难以理解,可以把它翻译成"代表";即原本由A做的事doAction,改为了由proxy$A代表A来做;总结下来关注几个点即可:
1. 为什么proxy$A可以代表A?
——因为他们的类型(父类/接口)都是IA;
2. 为什么需要proxy$A来代表A?
——因为需要在A执行某件事doAction的前后,再插入一些其他的事宜,让整件事做的更加完善,proxy$A可以看做是IA类型对外的对象,核心是执行A的doAction方法;
3. 如何让proxy$A能在A.doAction前后执行其他方法?
——(1)通过持有A对象;(2)通过继承A对象获得其doAction方法;
实际生产中,这个"在A执行某件事doAction的前后,再插入一些其他的事宜",一般是一些通用的处理逻辑,它不是某个特定类独有的,例如:日志打印、异常捕获、接口出入参统一处理等;接下来用几个例子分别介绍Java的3种代理模式的实现,包括:静态代理、动态代理和CGlib代理;
Java中有几种不同的代理模式的实现:静态代理、JDK动态代理和CGlib代理;接下来一一介绍,并给出示例;
这种代理方式需要代理对象和原对象实现一样的接口,而代理对象需要持有原对象,一般通过属性注入;在代理对象实现的同一接口的抽象方法中,包括两部分:(1)自己的增强逻辑和(2)原对象的方法调用;
优点:可以在不修改原对象所在类的前提下扩展(增强)原对象的功能,因为代理对象和原对象实现相同的接口,只需要将对原对象的调用换成代理对象即可;
缺点:
1. 冗余,由于代理对象要实现与原对象一样的接口,会产生过多的代理类,这些类都需要我们手动去编写,编译完成后每个代理类对应1个class文件;——这也就是静态的由来,代理类的代码必须提前写好;
2. 不易维护,一旦接口增加抽象方法,原对象与代理对象所在的类都要进行修改;
给个示例:
(1)接口
public interface IUserDAO {
void save(UserDO user);
}
(2)原对象的类
public class UserDAO implements IUserDAO {
@Override
public void save(UserDO user) {
System.out.println("UserDAO-插入/更新用户");
}
}
(3)代理类
public class UserDAOTransactionProxy implements IUserDAO {
private IUserDAO realMapper;
public UserDAOTransactionProxy(IUserDAO realMapper) {
this.realMapper = realMapper;
}
@Override
public void save(UserDO user) {
// 增强逻辑:执行前处理
System.out.println("proxy-开启事务");
// 调用被代理对象的方法
try {
realMapper.save(user);
} catch (Exception e) {
// 增强逻辑:执行异常时处理
System.out.println("proxy-异步通知");
}
// 增强逻辑:执行后处理
System.out.println("proxy-提交事务");
}
}
测试:
public static void main(String[] args) {
// 真实对象(原对象)
IUserDAO realMapper = new UserDAO();
// 代理对象 持有原对象
IUserDAO mapper = new UserDAOTransactionProxy(realMapper);
// 执行IUserDAO接口对象的方法
mapper.save(new UserDO());
}
// 测试结果
proxy-开启事务
UserDAO-插入/更新用户
proxy-提交事务
JDK动态代理基于JDK API,动态地在内存中构建代理对象,从而实现对目标对象的代理功能,又被称为接口代理;
静态代理与动态代理的区别主要在:
1. 静态代理在编译时就已经实现,编译完成后代理类是一个实际的class文件;
2. 动态代理是在运行时动态生成的,即编译完成后没有实际的class文件,而是在运行时动态生成类字节码,并加载到JVM中;
特点:代理对象不需要实现接口,但是要求原对象必须实现接口,否则不能使用JDK动态代理,就需要考虑使用CGlib代理了;
执行代理包括2个步骤:(1)通过原对象获取其代理对象,定义被代理增强的方法逻辑;(2)执行代理对象的方法;这两个步骤分别对应JDK API中2个关键的类;
1. java.lang.reflect.Proxy#newProxyInstance
public static Object newProxyInstance(ClassLoader loader, // 指定当前目标对象使用类加载器
Class[] interfaces, //目标对象实现的接口的类型
InvocationHandler h //事件处理器
)
throws IllegalArgumentException {...}
2. java.lang.reflect.InvocationHandler#invoke
public Object invoke(Object proxy, // 代理对象
Method method, // 目标方法
Object[] args // 方法参数
)
throws Throwable;
示例如下:
先定义代理工厂,根据目标对象获取其代理对象;
public class ProxyFactory {
// (原)目标对象
private Object target;
public ProxyFactory(Object target) {
this.target = target;
}
public Object getProxyInstance() {
// 为目标对象生成代理对象
return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("当前类实现了IUserDAO接口:" + (proxy instanceof IUserDAO));
System.out.println("JdkProxy-执行前增强");
// 执行目标对象方法
Object returnValue = method.invoke(target, args);
System.out.println("JdkProxy-执行后增强");
return returnValue;
}
});
}
}
测试:
public static void main(String[] args) {
// 真实对象(原对象)
IUserDAO realMapper = new UserDAO();
// 从代理工厂获取代理对象 类型强转
IUserDAO mapper = (IUserDAO) new ProxyFactory(realMapper).getProxyInstance();
// 执行IUserDAO接口对象的方法
mapper.save(new UserDO());
}
// 测试结果
当前类实现了IUserDAO接口:true
JdkProxy-执行前增强
UserDAO-插入/更新用户
JdkProxy-执行后增强
CGlib(Code Generation Library)是一个第三方代码生成类库,运行时在内存中动态生成一个子类对象从而实现对目标对象功能的扩展;
JDK动态代理有一个限制,就是使用动态代理的对象必须实现一个或多个接口;如果想代理没有实现接口的类,就可以使用CGlib实现;
CGlib是一个强大的高性能的代码生成包,它可以在运行期扩展Java类与实现Java接口;它广泛的被许多AOP的框架使用,例如Spring AOP和dynaop,为他们提供方法的interception(拦截);
CGlib包的底层是通过使用一个小而快的字节码处理框架ASM,来转换字节码并生成新的类;
不鼓励直接使用ASM,因为它需要你对JVM内部结构包括class文件的格式和指令集都很熟悉;
cglib与动态代理最大的区别就是:使用JDK动态代理的对象必须实现一个或多个接口;而使用CGlib代理的对象则无需与被代理对象共同实现同一接口,可以达到对被代理类的无侵入;
使用CGlib代理需要引入CGlib的jar包,如果已经引入spring-core的jar包,则无需单独引入,因为spring-core中包含了CGlib;
单独引入CGlib的jar包:
cglib
cglib
3.3.0
给个基于CGlib的动态代理示例:
public class CglibProxyFactory implements MethodInterceptor {
// (原)目标对象
private Object target;
public CglibProxyFactory(Object target) {
this.target = target;
}
public Object getProxyInstance() {
// 工具类 用来创建代理对象
Enhancer en = new Enhancer();
// 设置父类
en.setSuperclass(target.getClass());
// 设置回调函数
en.setCallback(this);
// 创建子类对象代理
return en.create();
}
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("当前代理类继承了UserDAO类:" + (obj instanceof UserDAO));
System.out.println("CglibProxy-执行前增强");
// 执行目标对象的方法
Object returnValue = method.invoke(target, args);
System.out.println("CglibProxy-执行后增强");
return returnValue;
}
}
测试:
public static void main(String[] args) {
// 目标对象
UserDAO target = new UserDAO();
// cglib代理对象
UserDAO proxy = (UserDAO) new CglibProxyFactory(target).getProxyInstance();
// 执行代理对象方法
proxy.save(new UserDO());
}
// 测试结果
当前代理类继承了UserDAO类:true
CglibProxy-执行前增强
UserDAO-插入/更新用户
CglibProxy-执行后增强
实现上:
性能上:
关于Spring AOP是基于动态代理实现的,就是上面所介绍的这些,下面介绍下Spring具体是怎么使用的;
在代理模式中有2个重要的角色:委托类(目标类)、代理类;
委托类
在切面定义的类上加上@Aspect的注解,通过@Pointcut注解来申明“切点”,这个切点即标注了委托类和委托方法的路径(也可以是方法名匹配、注解匹配等);
有了这些信息就足够获取委托类了,这里用到Java反射,先找到包含@Aspect注解的类,然后找到该类下的@Pointcut注解,读取所定义的委托类和委托方法路径,就完全能拿到委托类对象;
代理类/目标类
因为Spring AOP只代理目标方法,这里的代理类可以被替换成代理方法;在@Aspect注解的切面定义类中,用@Around、@Before、@After修饰的方法,就是我们想要的代理方法;
小结
我们可以通过BeanFactoryPostProcessor的实现类,完成对所有BeanDefinition的扫描,找出我们定义的所有的切面类,然后循环里面的方法,找到切点、以及所有的通知方法,然后根据注解判断通知类型(也就是前置,后置还是环绕),最后解析切点的内容,扫描出所有的目标类;这样就获取了委托类和代理方法;
现在委托类和代理方法 都有了,我们知道在动态代理模式中,最终的目的是将委托类的方法执行,替换成代理类的方法执行;但是在Spring中,我们是感知不到代理类的,我们在代码中还是调用原委托类的方法,那么Spring框架是如何神不知鬼不觉地将委托类替换成代理类的呢?
在Bean的生命周期中,Bean加载执行到初始化方法initializeBean()时,会执行BeanPostProcessor的方法;可以把它理解成一个增强方法,可以将原始的Bean经过“增强”处理后加载到IOC容器中;正是在此处通过原委托类的对象生成其代理类对象,再将代理对象加载进IOC容器;
Spring如何选择是用JDK还是CGlib代理?
Spring会自动在JDK动态代理和CGlib代理之间转换,
1. 目标对象生成了接口,则默认用JDK动态代理;也可以强制使用CGlib代理(在Spring配置中加入
2. 如果目标对象没有实现接口,必须采用CGlib代理;
参考:
Java三种代理模式 - SegmentFault 思否
Spring Aop 动态代理 - SegmentFault 思否