Spring Boot教程(20) – 用AspectJ实现AOP内部调用

你一定用过@Transactional注解吧,它加在方法上可以实现声明式事务。第一次接触到它的时候,我感觉这种设计挺完美的。后来发现由于它是基于Spring AOP的代理实现的,所以有个坑——不支持内部调用的。

比如你的@Service类,里面有个A方法,调用了带有@Transactional注解的B方法,那么你在调用A方法,执行到B方法的时候是没有事务关联的。因为A调用B的时候,并不是通过代理类,而事务相关逻辑是放在代理类的。

如果你还不懂面向切面编程(AOP),不懂什么是代理,不妨看看我之前的文章

既然必须通过代理来实现事务,那就好说了。把B放到另外一个@Service类里,这个时候A再调用B,就有事务了。那你肯定要问,我的A和B方法关联性非常大的,分开的话代码不是乱了么?

那可以使用另外一种方法,使用AopContext.currentProxy()方法来获取到当前类的代理对象,调用代理对象的B方法,也是可以完成事务的,不过就是有点丑,代码里充斥着AopContext.currentProxy()会让有点代码洁癖的人很难受。你还可以通过@Autowired将自身的代理对象引入,不过写法没有本质的改变,你都需要对内部调用的代码进行针对性的修改,还是不太完美。

接下来本文将介绍如何通过LTW(加载时织入)的方式使用AspectJ,从而解决Spring AOP无法内部调用的问题。

解决思路

为什么使用加载时织入,而不是编译时织入或者编译后织入?因为前者不需要引入AspectJ的编译工具ajc,毕竟引入新的工具链并不是所有人都能接受的。

原理是什么?加载时织入可以在JVM加载类的时候,对jar包里的类做一些修改。有两种方式进行修改,一种是在ClassLoader加载的时候修改,一种是给JVM传递javaagent参数(值是个jar包),由它来进行修改。

利用ClassLoader来修改类是对代码影响最小的,你只需要添加@EnableLoadTimeWeaving就行。可惜的是,这种方式只支持在应用容器里运行,比如Tomcat, JBoss, WebSphere, WebLogic等,因为要利用他们底层的ClassLoader实现。通常我们的Spring Boot是通过jar包来部署运行的,所以这种情况不适用。

给JVM传递javaagent也有两种方式。一种是传递aspectjweaver.jar,一种是传递spring-instrument.jar,他们利用Java Instrumentation API对字节码进行修改。spring-instrument是Spring框架的一个模块,你可以在中央仓库中下载其对应的jar包。我尝试过使用@EnableLoadTimeWeaving注解并传递spring-instrument.jar来开启AspectJ的加载时织入,但是并没有成功,也没找到原因。

那么就只有最后一种选择了,把javaagent的参数设置为aspectjweaver.jar,aspectjweaver.jar是AspectJ官方提供的加载时织入工具,使用起来还是比较简单的。我通过这种方式成功实现了内部调用,下面来说说如何一步步实现:

操作步骤

第一步,引入依赖

spring-boot-starter-aop引入了aspectjweaver,它其实跟上面说的aspectjweaver.jar其实是同一个东西。其中包含了许多AspectJ相关的工具类,比如用来编写切面的@Aspect注解,比如用来执行加载时织入的相关代码等等。spring-aspects是Spring框架的一个模块,它包含了使用AspectJ语言编写的切面文件,比如用于@Transactional注解的AnnotationTransactionAspect.aj切面文件,加载时织入的时候,切面文件里相关代码会被插到你的@Transactional标注的代码里。

第二步,开启AspectJ支持

@EnableAsync@EnableCaching@EnableTransactionManagement都有一个mode参数,他的类型是AdviceMode,用来表示通过Spring AOP的代理还是通过AspectJ来实现功能。我们的目的是使用AspectJ实现内部调用,当然选择AdviceMode.ASPECTJ。图中的三个@Enable*并不是每个都需要的,你看看你的项目里,具体哪一个功能要开启,开启之后要不要使用AspectJ,你都是可以配置的。要我说,既然你的项目已经可以使用AspectJ了,那干脆全都开启好了,性能还高一点呢。

第三步,把冰箱门关上(把javaagent参数传递进去)

我通常会把下载下来的aspectweaver.jar放到项目根目录下的tools目录中。如果在生产环境中,你可以通过下面的方法来添加aspectweaver.jar:

java -jar app.jar -javaagent:path/to/weaver.jar

这样其实就OK了。可以用了,只要你的B方法添加了@Async或者@Cacheable或者@Transactional注解,不管从什么地方调用B方法,他都可以完成你预想的功能,不存在内部调用失效的问题。

总的来看,使用成本也很低,你只需要告诉你的同事,在IDE加个配置就可以开发了,如果怕新手不懂,你可以写到文档里,再不行,你就写个代码,检测下AspectJ有没有启用,没有就让程序挂掉,提醒开发者去添加javaagent。在生产环境使用的成本也很低,毕竟一劳永逸,比如我一般用Docker部署,会在Dockerfile里直接传递javaagent参数,一次改动,以后都不用再改了。

相关答疑:

1. 我看文档,使用AspectJ的时候需要指定META-INF/aop.xml,没见你用呀?

其实spring-aspects模块已经自带了一个aop.xml,将其实现的切面文件都包含了进来。AspectJ织入的时候自动找到了这里。

2. 我看终端,输出了很多“[Xlint:cantFindType]”字样的日志,很烦,怎么去掉?

在项目目录下新建个自己的aop.xml,然后将目标类限定在你的项目包下,比如:

或者直接将Xlint去掉(不推荐):

值得一提的是,上面weaver标签的options有个-showWeaveInfo参数,会在日志里输出你的切面织入目标类的信息,比如@Transactional方法是否已经关联了事务相关代码,这些输出信息可以方便你纠错。类似下图:

3. 为什么有warning提示“warning javax.* types are not being woven because the weaver option ‘-Xset:weaveJavaxPackages=true’ has not been specified”?

先说解决办法把,一个是按照提示,在weaver标签的options里添加-Xset:weaveJavaxPackages=true,另一个办法是添加-nowarn直接忽略所有warning。但是,这不是根本解决办法,你不知道weaveJavaxPackages是干啥的为啥要加上,忽略warning是给自己的项目埋隐患。其实我也没找到为啥,搜索引擎没有给我答案,路过的懂得话给我留言,谢了~

总结

看完本文之后,在你的项目里加上AspectJ的支持,要的了10分钟么?要不了!还不赶紧把项目里的AopContext.currentProxy()删掉?噢,不对,先别急,你好像应该先花一个月时间说服你项目的技术负责人和同事们才对~

参考资料:

发表评论

电子邮件地址不会被公开。