前段时间在为公司的工程加入 AOP 支持时,选用了 AspectJ 的编译期织入静态 AOP 的实现方案,在此总结一下吧。
说到 AOP 想必大家受 spring 熏陶多年,都会有所了解。在 spring 中,我们一般会选用其基于代理的动态 AOP 方案,但是,在一些比较特殊的工程中,使用动态 AOP 并不合适,比如一些使用了基于代理机制的 RPC 框架的工程,因此就需要一种纯粹静态的 AOP 决方案。而 AspectJ 就提供了基于 编译期织入
的纯静态 AOP 方案。
那么,该如何使用呢(我们这里讲的是脱离 spring 的使用方式)。我们通过小例子来展示一下。在这个小例子中,我们来做一个基于注解的方法运行计时器,最终的效果应该如下:
@StopWatch public class Scratch { public void test() { // do something... } }
当前线程 [main] 计时开始 执行 当前线程 [main] 当前方法 [execution(public com.xx.Scratch com.xx.Scratch.test())] 当前线程 [main] 计时结束,共耗时 [1986 MILLISECONDS]
也就是在类级别加上注解 @StopWatch
就能够统计出来此类所有 public
方法的运行时间。
实现我们来看代码吧,一码胜千言:
import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.util.concurrent.TimeUnit; @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface StopWatch { TimeUnit timeUnit() default TimeUnit.MILLISECONDS; }
import com.google.common.base.Stopwatch; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; // https://stackoverflow.com/questions/17481183/aspectj-class-level-annotation-advice-with-annotation-as-method-argument @Aspect public class StopWatchAspect { private static final Logger LOGGER = LoggerFactory.getLogger(StopWatchAspect.class); private static Stopwatch _stopwatch; @Pointcut("execution(public * *(..))") public void publicMethod() { } @Pointcut(value = "@within(stopWatch)", argNames = "stopWatch") public void annotatedWithStopWatch(StopWatch stopWatch) { } @Pointcut(value = "publicMethod() && annotatedWithStopWatch(stopWatch)", argNames = "stopWatch") public void publicMethodAnnotatedWithStopWatch(StopWatch stopWatch) { } @Before(value = "publicMethodAnnotatedWithStopWatch(stopWatch)", argNames = "stopWatch") public void before(StopWatch stopWatch) { _stopwatch = Stopwatch.createStarted(); LOGGER.info(String.format("当前线程 [%s] 计时开始", Thread.currentThread().getName())); } @Around(value = "publicMethodAnnotatedWithStopWatch(stopWatch)", argNames = "proceedingJoinPoint,stopWatch") public Object around(ProceedingJoinPoint proceedingJoinPoint, StopWatch stopWatch) throws Throwable { LOGGER.info(String.format("执行 当前线程 [%s] 当前方法 [%s]", Thread.currentThread().getName(), proceedingJoinPoint.toLongString())); return proceedingJoinPoint.proceed(); } @After(value = "publicMethodAnnotatedWithStopWatch(stopWatch)", argNames = "stopWatch") public void after(StopWatch stopWatch) { LOGGER.info(String.format("当前线程 [%s] 计时结束,共耗时 [%d %s] ", Thread.currentThread().getName(), _stopwatch.elapsed(stopWatch.timeUnit()), stopWatch.timeUnit().name())); } }
上边两段代码分别展示了我们的定时器 注解
和用于处理定时器逻辑的 切面
,因为我们要实现基于注解的计时,必然会需要自己定义一个注解,这个不用赘述,主要的,我们来看 切面
代码,这个是实现定时器逻辑的关键。
先来看类定义:
@Aspect public class StopWatchAspect {
要想将一个普通的类声明为 切面
,就需要加上一个 AspectJ 自己的注解 @Aspect
,加上这个注解后,AspectJ 就会识别出来这个类,从而来走后续的流程。
再来看 切入点
定义
@Pointcut("execution(public * *(..))") // 在所有 public 方法中切入 public void publicMethod() { } @Pointcut(value = "@within(stopWatch)", argNames = "stopWatch") // 在标注有 @StopWatch 的类中的所有方法中切入 public void annotatedWithStopWatch(StopWatch stopWatch) { } @Pointcut(value = "publicMethod() && annotatedWithStopWatch(stopWatch)", argNames = "stopWatch") // 在同时满足 public 以及所在类标注有 @StopWatch 这两个条件的方法切入 public void publicMethodAnnotatedWithStopWatch(StopWatch stopWatch) { }
在 AspectJ 中使用特殊的表达式来定义切入点,具体的表达式语法请参照官方的文档,在此不再复述。
然后看 通知
定义
@Before(value = "publicMethodAnnotatedWithStopWatch(stopWatch)", argNames = "stopWatch") public void before(StopWatch stopWatch) { _stopwatch = Stopwatch.createStarted(); LOGGER.info(String.format("当前线程 [%s] 计时开始", Thread.currentThread().getName())); } @Around(value = "publicMethodAnnotatedWithStopWatch(stopWatch)", argNames = "proceedingJoinPoint,stopWatch") public Object around(ProceedingJoinPoint proceedingJoinPoint, StopWatch stopWatch) throws Throwable { LOGGER.info(String.format("执行 当前线程 [%s] 当前方法 [%s]", Thread.currentThread().getName(), proceedingJoinPoint.toLongString())); return proceedingJoinPoint.proceed(); } @After(value = "publicMethodAnnotatedWithStopWatch(stopWatch)", argNames = "stopWatch") public void after(StopWatch stopWatch) { LOGGER.info(String.format("当前线程 [%s] 计时结束,共耗时 [%d %s] ", Thread.currentThread().getName(), _stopwatch.elapsed(stopWatch.timeUnit()), stopWatch.timeUnit().name())); }
关于 AspectJ 通知的类型请参看官方文档,在此例中,我们用到了 @Before
、 @Around
和 @After
这三种,分别在其中开启计时器、输出运行方法签名信息和结束计时。
这样基本的切面逻辑我们就写好了,但是,只有这些切面逻辑是不能在运行时实现真正的我们想要实现的 AOP 逻辑的,还需要 织入
的过程(我们选用编译期织入,所以是在编译时完成织入的)。
我们来看 织入
的配置
<dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjrt</artifactId> <version>${version.aspectjrt}</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>${version.aspectjweaver}</version> </dependency>
<plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>aspectj-maven-plugin</artifactId> <version>${version.aspectj-maven-plugin}</version> <configuration> <complianceLevel>${maven.compiler.target}</complianceLevel> </configuration> <executions> <execution> <goals> <goal>compile</goal> <goal>test-compile</goal> </goals> </execution> </executions> </plugin>
配置编译期织入需要我们配置好 AspectJ 的依赖以及 maven 插件,有了这些之后,在编译时,AspectJ 会自动为我们生成如下这样的 .class(以下为反编译生成的代码):
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) // import com.google.common.base.Stopwatch; import org.aspectj.lang.NoAspectBoundException; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @Aspect public class StopWatchAspect { private static final Logger LOGGER = LoggerFactory.getLogger(StopWatchAspect.class); private static Stopwatch _stopwatch; static { try { ajc$postClinit(); } catch (Throwable var1) { ajc$initFailureCause = var1; } } public StopWatchAspect() { } @Before( value = "publicMethodAnnotatedWithStopWatch(stopWatch)", argNames = "stopWatch" ) public void before(StopWatch stopWatch) { _stopwatch = Stopwatch.createStarted(); LOGGER.info(String.format("当前线程 [%s] 计时开始", Thread.currentThread().getName())); } @Around( value = "publicMethodAnnotatedWithStopWatch(stopWatch)", argNames = "proceedingJoinPoint,stopWatch" ) public Object around(ProceedingJoinPoint proceedingJoinPoint, StopWatch stopWatch) throws Throwable { ajc$inlineAccessFieldGet$com_bj58_jxedt_pay_unity_core_stopwatch_StopWatchAspect$com_bj58_jxedt_pay_unity_core_stopwatch_StopWatchAspect$LOGGER().info(String.format("执行 当前线程 [%s] 当前方法 [%s]", Thread.currentThread().getName(), proceedingJoinPoint.toLongString())); return proceedingJoinPoint.proceed(); } @After( value = "publicMethodAnnotatedWithStopWatch(stopWatch)", argNames = "stopWatch" ) public void after(StopWatch stopWatch) { LOGGER.info(String.format("当前线程 [%s] 计时结束,共耗时 [%d %s] ", Thread.currentThread().getName(), _stopwatch.elapsed(stopWatch.timeUnit()), stopWatch.timeUnit().name())); } public static StopWatchAspect aspectOf() { if (ajc$perSingletonInstance == null) { throw new NoAspectBoundException("com.bj58.jxedt.pay.unity.core.stopwatch.StopWatchAspect", ajc$initFailureCause); } else { return ajc$perSingletonInstance; } } public static boolean hasAspect() { return ajc$perSingletonInstance != null; } }
对比前面我们写的 切面
代码,这个生成的 .class 多了一些生成出来的内容,这些内容是 AspectJ 为我们生成的,生成了这些之后,就实现了我们真正的 AOP 逻辑了。
到此为止,我们就完成了 AspectJ 编译期织入 AOP 的代码编写了,就可以愉快的使用了。在这里补充一点,在一些需要排除掉兼容性困扰的场景中推荐选择编译期织入,而不是其他织入方式,因为编译期织入在编译时就完成了 AOP 逻辑,如果有不兼容的情况,早在编译期就能发现,这样就确保了在加载期和运行期是兼容的稳定的。