主要讲解AOP的实现姿势,包括AspectJ和Spring,以及自己对AOP内部实现的理解。
前言
看这篇文章前,请大家先看《【Spring基础系列5】Spring AOP基础(上)》,因为这两篇文章是自成体系,对于Spring AOP的基础知识,本来打算只写一篇,因为担心大家不愿意看长文,就拆成了上-下两篇文章。
Java中的AOP主要有4种实现方式,对于AspectJ基于XML的声明式,上一篇文章已经给出非常详细的示例,这篇文章主要针对AspectJ基于Annotation的声明式、基于Spring的JDK动态代理和CGLib动态代理这3种AOP实现方式进行讲解。
这篇文章也是Spring系列的最后一篇,前后花了近2周的时间总结的成果,如果想学习Spring的同学,建议从第一篇文章开始看:
- 《【Spring基础系列1】基于注解装配Bean》
- 《【Spring基础系列2】很全的Spring IOC基础知识》
- 《【Spring基础系列3】Spring常用的注解》
- 《【Spring基础系列4】注解@Transactional》
- 《【Spring基础系列5】Spring AOP基础(上)》
- 《【Spring基础系列5】Spring AOP基础(下)》
Spring AOP
Spring JDK动态代理
JDK 动态代理是通过 JDK 中的 java.lang.reflect.Proxy 类实现的。下面通过具体的案例演示 JDK 动态代理的使用,我们先新建一个接口,并给出具体实现类:
public interface CustomerDao { public void add(); // 添加 public void update(); // 修改 public void delete(); // 删除 public void find(); // 查询 }
public class CustomerDaoImpl implements CustomerDao { @Override public void add() { System.out.println("添加客户..."); } @Override public void update() { System.out.println("修改客户..."); } @Override public void delete() { System.out.println("删除客户..."); } @Override public void find() { System.out.println("修改客户..."); } }
新建一个切面类:
public class MyAspect { public void myBefore() { System.out.println("方法执行之前"); } public void myAfter() { System.out.println("方法执行之后"); } }
再新建一个通过代理实现的工厂类:
public class MyBeanFactory { public static CustomerDao getBean() { // 准备目标类 final CustomerDao customerDao = new CustomerDaoImpl(); // 创建切面类实例 final MyAspect myAspect = new MyAspect(); // 使用代理类,进行增强 return (CustomerDao) Proxy.newProxyInstance( // MyBeanFactory.class.getClassLoader(), // 这个也可以 CustomerDao.class.getClassLoader(), new Class[] { CustomerDao.class }, new InvocationHandler() { public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { myAspect.myBefore(); // 前增强 Object obj = method.invoke(customerDao, args); myAspect.myAfter(); // 后增强 return obj; } }); } }
我使用MyBeanFactory.class.getClassLoader(),发现也不影响功能,后面有空再研究一下。
最后是测试示例:
public class JDKProxyTest { @Test public void test() { // 从工厂获得指定的内容(相当于spring获得,但此内容时代理对象) CustomerDao customerDao = MyBeanFactory.getBean(); // 执行方法 customerDao.add(); customerDao.update(); //customerDao.delete(); //customerDao.find(); } } // 输出: // 方法执行之前 // 添加客户... // 方法执行之后 // 方法执行之前 // 修改客户... // 方法执行之后
从输出结果中可以看出,在调用目标类的方法前后,成功调用了增强的代码,由此说明,JDK 动态代理已经实现。
Spring CGLlB动态代理
JDK 动态代理使用起来非常简单,但是它也有一定的局限性,这是因为 JDK 动态代理必须要实现一个或多个接口,如果不希望实现接口,则可以使用 CGLIB 代理。
CGLIB(Code Generation Library)是一个高性能开源的代码生成包,它被许多 AOP 框架所使用,其底层是通过使用一个小而快的字节码处理框架 ASM(Java 字节码操控框架)转换字节码并生成新的类,因此 CGLIB 要依赖于 ASM 的包。
下面看一下CGLlB动态代理的实现姿势,先定义一个实现类:
public class GoodsDao { public void add() { System.out.println("添加商品..."); } public void update() { System.out.println("修改商品..."); } public void delete() { System.out.println("删除商品..."); } public void find() { System.out.println("修改商品..."); } }
新建一个切面类:
public class MyAspect { public void myBefore() { System.out.println("方法执行之前"); } public void myAfter() { System.out.println("方法执行之后"); } }
再新建一个通过代理实现工厂类:
public class MyBeanFactory { public static GoodsDao getBean() { // 准备目标类 final GoodsDao goodsDao = new GoodsDao(); // 创建切面类实例 final MyAspect myAspect = new MyAspect(); // 生成代理类,CGLIB在运行时,生成指定对象的子类,增强 Enhancer enhancer = new Enhancer(); // 确定需要增强的类 enhancer.setSuperclass(goodsDao.getClass()); // 添加回调函数 enhancer.setCallback(new MethodInterceptor() { // intercept 相当于 jdk invoke,前三个参数与 jdk invoke—致 @Override public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { myAspect.myBefore(); // 前增强 Object obj = method.invoke(goodsDao, args); // 目标方法执行 myAspect.myAfter(); // 后增强 return obj; } }); // 创建代理类 GoodsDao goodsDaoProxy = (GoodsDao) enhancer.create(); return goodsDaoProxy; } }
最后是测试示例:
public class JDKProxyTest { @Test public void test() { // 从工厂获得指定的内容(相当于spring获得,但此内容时代理对象) GoodsDao goodsDao = MyBeanFactory.getBean(); // 执行方法 goodsDao.add(); goodsDao.update(); // goodsDao.delete(); // goodsDao.find(); } } // 输出: // 方法执行之前 // 添加商品... // 方法执行之后 // 方法执行之前 // 修改商品... // 方法执行之后
从输出结果中可以看出,在调用目标类的方法前后,也成功调用了增强的代码,由此说明,使用 CGLIB 代理的方式同样实现了手动代理。
两者比较
可以直接参考文章《【Spring基础系列5】Spring AOP基础(上)》,再盗用上一篇文章的图,回顾一下:
重点:JDK动态代理是基于接口,CGLIB动态代理是基于类,上面的示例也能看出两者的区别,如果你在CGLIB代理的示例用接口替换,肯定会报错的。
使用ProxyFactoryBean创建AOP代理
这种方式我没有在项目中遇到过,仅作为扩展知识了解即可。
基础知识
上述已经讲解了 AOP 手动代理的两种方式,下面介绍 Spring 是如何创建 AOP 代理的。Spring 创建一个 AOP 代理的基本方法是使用 org.springframework.aop.framework.ProxyFactoryBean,这个类对应的切入点和通知提供了完整的控制能力,并可以生成指定的内容。
ProxyFactoryBean 类中的常用可配置属性如表所示:
具体实例
我们还是先定义一个接口和一个实现类,CustomerDao、CustomerDaoImpl同“Spring JDK动态代理“示例内容一样,只是需要对CustomerDaoImpl加上注解@Component("customerDao"),主要是为了减少配置。
@Component("customerDao") public class CustomerDaoImpl implements CustomerDao { // 方法同“Spring JDK动态代理“示例内容 }
然后定义一个切面类:
@Component public class MyAspect implements MethodInterceptor { public Object invoke(MethodInvocation mi) throws Throwable { System.out.println("方法执行之前"); Object obj = mi.proceed(); System.out.println("方法执行之后"); return obj; } }
下面这个非常重要,就是在applicationContext.xml文件中增加相应配置:
<context:component-scan base-package="com.java.spring.aop.xml" /> <!--生成代理对象 --> <bean id="customerDaoProxy" class="org.springframework.aop.framework.ProxyFactoryBean"> <!--代理实现的接口 --> <property name="proxyInterfaces" value="com.java.spring.aop.xml.CustomerDao" /> <!--代理的目标对象 --> <property name="target" ref="customerDao" /> <!--用通知增强目标 --> <property name="interceptorNames" value="myAspect" /> <!-- 如何生成代理,true:使用cglib; false :使用jdk动态代理 --> <property name="proxyTargetClass" value="true" /> </bean>
最后看一下测试用例:
public class FactoryBeanTest { @Test public void test() { ApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:applicationContext.xml"); CustomerDao customerDao = (CustomerDao) applicationContext.getBean("customerDaoProxy"); customerDao.add(); customerDao.update(); // customerDao.delete(); // customerDao.find(); } } // 输出: // 方法执行之前 // 添加客户... // 方法执行之后 // 方法执行之前 // 修改客户... // 方法执行之后
这个和“Spring JDK动态代理”、“Spring CGLlB动态代理”的区别在于,前两者是手动方式,这个是自动方式,然后结合了“通知的分类”(可以参考《【Spring基础系列5】Spring AOP基础(上)》)。
AspectJ AOP
AspectJ 基于XML方式
直接参考文章《【Spring基础系列5】Spring AOP基础(上)》中的示例。
AspectJ 基于Annotation方式
基础知识
在 Spring 中,尽管使用 XML 配置文件可以实现 AOP 开发,但是如果所有的相关的配置都集中在配置文件中,势必会导致 XML 配置文件过于臃肿,从而给维护和升级带来一定的困难。
为此,AspectJ 框架为 AOP 开发提供了另一种开发方式——基于 Annotation 的声明式。AspectJ 允许使用注解定义切面、切入点和增强处理,而 Spring 框架则可以识别并根据这些注解生成 AOP 代理。关于 Annotation 注解的介绍如表所示:
具体实例
先定义一个具体的实现类:
@Component("customerDao") public class CustomerDaoImpl { public void add() throws Exception { System.out.println("添加客户..."); //throw new Exception("抛出异常测试"); } public void update() { System.out.println("修改客户..."); } public void delete() { System.out.println("删除客户..."); } public void find() { System.out.println("修改客户..."); } }
然后定义一个切面类:
@Aspect @Component public class MyAspect { // 用于取代:<aop:pointcut expression="execution(* com.java.spring.aop.customer.*.*(..))" id="myPointCut"/> @Pointcut("execution(* com.java.spring.aop.customer.*.*(..))") private void myPointCut() { } // 前置通知 @Before("myPointCut()") public void myBefore(JoinPoint joinPoint) { System.out.println("前置通知,方法名称:" + joinPoint.getSignature().getName()); } // 后置通知 @AfterReturning(value = "myPointCut()") public void myAfterReturning(JoinPoint joinPoint) { System.out.println("后置通知,方法名称:" + joinPoint.getSignature().getName()); } // 环绕通知 @Around("myPointCut()") public Object myAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { System.out.println("环绕开始"); // 开始 Object obj = proceedingJoinPoint.proceed(); // 执行当前目标方法 System.out.println("环绕结束"); // 结束 return obj; } // 异常通知 @AfterThrowing(value = "myPointCut()", throwing = "e") public void myAfterThrowing(JoinPoint joinPoint, Throwable e) { System.out.println("异常通知,出错了"); } // 最终通知 @After("myPointCut()") public void myAfter() { System.out.println("最终通知"); } }
这里和“AspectJ 基于XML方式”一样,需要在applicationContext.xml文件中增加相应配置:
<context:component-scan base-package="com.java.spring.aop.customer" /> <context:component-scan base-package="com.java.spring.aop.annotation" /> <aop:aspectj-autoproxy proxy-target-class="true"/>
前面两行是自动注入注解的包,第三行是需要开启AOP,下面是测试用例:
public class AnnotationTest { @Test public void test() throws Exception { ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml"); // 从spring容器获取实例 CustomerDaoImpl customerDao = (CustomerDaoImpl) applicationContext.getBean("customerDao"); // 执行方法 customerDao.add(); } } // 输出: // 环绕开始 // 前置通知,方法名称:add // 添加客户... // 环绕结束 // 最终通知 // 后置通知,方法名称:add
AOP实现方式探讨
开始看“Spring JDK动态代理”的示例时,感觉这个示例非常熟悉,大家可以看看《【设计模式系列6】代理模式》这篇文章,其实就是通过代理模式进行前后增强,唯一的区别是“Spring JDK动态代理”中对代理的对象封装成一个工厂,所以是工厂模式 + 代理模式,感觉也没啥技术含量。
但是看到“AspectJ 基于XML方式”示例时,我们回顾一下它的测试用例:
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml"); CustomerDaoImpl customerDao = (CustomerDaoImpl) applicationContext.getBean("customerDao"); // 执行方法 customerDao.add();
大家有没有发现疑问,我们直接获取customerDao这个Bean,调用里面的add()方法,就能实现增强,这个是怎么实现的呢。我理解通过Spring拿到customerDao这个Bean,其实不是customerDao本身,而是customerDao的装饰类M,也就是这个装设类M对customerDao的方法进行了修饰,怎么修饰呢?就是通过切面方法进行修饰!
上面听得可能有点蒙,大家可以看文章《【设计模式系列7】装饰器模式》,我还是直接盗用里面的图:
这里实体类是Chicken,装饰器是RoastFood,如果没有装饰器,调用cook()输出的是“鸡肉”,加上RoastFood装饰器修饰之后,调用cook()输出的是“烤鸡肉”,那么这个“烤”就是对“鸡肉”的装饰。
同理,CustomerDaoImpl对比Chicken,MyAspect对比“烤”,最后通过装饰器装饰后,将MyAspect中的方法装饰到CustomerDaoImpl中(《【设计模式系列7】装饰器模式》只在装饰类中定义了“烤”这个方法,你可以单独封装一个类N,里面封装一个“烤”方法,那么这个类N就可以对比为MyAspect)。
再回到我们上面的那个测试用例,应该就不难理解了,Spring先拿到CustomerDaoImpl类,这个类其实是通过MyAspect进行装饰,所以拿到的不是CustomerDaoImpl类本身,而是它的装饰器,最后将这个装饰器转成CustomerDaoImpl,至于为什么能转,是因为CustomerDaoImpl类和装饰器共同继承了CustomerDao接口。
总结:对于Spirng AOP,获取到的实体类其实不是实体类本身,而是这个实体类的装饰器,这个装饰器里应该是将实体类作为了装饰器的成员变量,然后通过切面MyAspect的方法进行增强,这里其实就是用到了装饰器模式。这个装饰器的获取,是通过工厂的封装,然后装饰器中实体类方法的调用,应该是采用的代理模式,所以Spirng AOP至少用到了装饰器、代理模式和工厂模式,然后完成整个功能的封装。
这个总结,仅是一家之言,可能有点天马行空,毕竟没有看AOP相关的源码和原理相关的文章,如果不是这样去设计AOP,那还有哪些其它的方法呢?目前我没有想到其它方式,如果文中有理解不对的地方,或者只是自己“一厢情愿”的话,欢迎大家给我指出!
后记
这篇文章就不写总结了,因为关于Spring AOP基础知识,我应该讲的还是比较详细,脉络也很清晰,就写点水文吧。
学习Spring相关的知识,总共学了10天,总结了6篇文章,虽然中间有几天状态不佳,但是整体学习的进度,我感觉还是不错的。下个系列我打算学习MyBatis,如果按照这个进度,MyBatis应该可以在6.20学习完毕。
学了Java也有一段时间,我也想说说我对Java的看法,我觉得Java真的就是一个工具,我学习Java的并发编程、Spring,感觉没有学到任何有关技术方面的东西,甚至可以说,学的东西和技术完全不靠边!学习Java语言生态的过程,其实就是学习Java的这一堆工具怎么使用,然后就没了,如果真说还学到其它什么知识,那可能就是编程的思想吧,因为Java的这些工具,用到了大量的设计模式,封装的功能确实非常通用,但是这个我其实只需要掌握到一定程度就够了,后面我还是需要在技术上深挖。
可能有同学会说,我学的还不够,是的,我也是刚开始学习Java,等我把MyBatis、SpringCloud、Dubbo等都学完了,那又能怎么样了,估计还是一堆工具。
不过话说话来,既然要去学习Java,这些工具我还是必须要掌握,不过等这堆工具我掌握到差不多的时候,我就不会再在上面花时间了,还是汲取些更优营养的知识,我才能更快成长。