昨天刚刚侍弄完 Spring 下基于自定义注解拦截方法调用,现在试下纯 AspectJ 的方式来打造,因为不是每一个项目都是 Spring。这次要推到 5 年前试验过用 javac 命令行编译的方式织入方面,见 AspectJ 基于自定义的方法注解来拦截方法,这次着重在用 aspectj-maven-plugin 插件的方法来织入 AspectJ 方面。
基本上代码还是昨天的,需求还是一样的:
被 @LogStartTime 注解的方法在进入该方法时记录当前时间在 ThreadLocal 中,并能根据 @LogStartTime 的属性值决定处理逻辑
因为 Java5+ 之后 AspectJ 可以写成 Java 类加注解的方式,*.aj 文件一般都没太大必要了,所以可以和 Spring AOP 共用一个 @Aspect 注解的方面代码 MethodStartAspect
。
我们将采用编译器织入,因此项目依赖只需要一个 org.aspectj:aspectjrt:1.8.0
, 它也不会引入别的组件。同样我们从 Main 方法和测试用例两方面来验证实现的效果,下面是整个测试项目的布局,以及依赖,除掉单元测试的其时就只需要一个 jar 包。
继续罗列代码
@LogStartTime, 注解被拦截的方法
1 2 3 4 5 |
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface LogStartTime { String value() default ""; } |
MethodStartAspect, 定义切面和 Advice
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
@Aspect public class MethodStartAspect { private static ThreadLocal<Long> startTime = new ThreadLocal<>(); @Pointcut("execution(* cc.unmi..*(..)) && @annotation(logStartTime)") private void logStartTimePointcut(LogStartTime logStartTime) { } @Before("logStartTimePointcut(logStartTime)") public void setStartTimeInThreadLocal(LogStartTime logStartTime) { System.out.println(logStartTime.value()); startTime.set(System.currentTimeMillis()); System.out.println("saved method start time in threadLocal"); } public static Long getStartTime() { return startTime.get(); } public static void clearStartTime() { startTime.set(null); } } |
只要用 @Aspect
标识出它是一个 Aspect, 或者也可以完全用 AspectJ 语法,创建 *.aj 文件,里面写 public aspect MethodStartAspect
这样的的定义。在进入有注解 @LogStartTime
方法之前把当前时间写到 ThreadLocal 中去,并可读取注解的属性值。
UserService, 被拦截的方法用了 @LogStartTime 注解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public class UserService { @LogStartTime("Hello World") public String fetchUserById(int userId) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("start time: " + MethodStartAspect.getStartTime()); return "nameOf" + userId; } } |
HelloAop, 使用被 AOP 的代码
1 2 3 4 5 6 7 |
public class HelloAop { public static void main(String[] args) { UserService userService = new UserService(); System.out.println(userService.fetchUserById(234)); } } |
现在直接执行 HelloAop 是没有什么特别效果的,因为编译器没有被告知的情况下是不知道 @LogStartTime
和 @Aspect
所代表的意义的。执行 HelloAop 只会说
start time: null
nameOf234
这时候的重头戏就是 aspectj-maven-plugin 这个 Maven 插件的了,需要在 pom.xml 文件中加上以下构建插件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>aspectj-maven-plugin</artifactId> <version>1.10</version> <configuration> <complianceLevel>1.8</complianceLevel> <source>1.8</source> </configuration> <executions> <execution> <goals> <goal>compile</goal> <!-- <goal>test-compile</goal>--> </goals> </execution> </executions> </plugin> |
多数情况下只需要让切面作用到正式代码上去,所以绑定任务到 compile
。 没什么实际的理由要在测试代码中应用切面。
我们可以来到命令行下来执行相应的 Maven 命令
mvn compile #编译代码,上面插件会把切面应用到连接点上去,我们待会可以看下织入了方面的字节码
mvn exec:java -Dexec.mainClass="cc.unmi.HelloAop" #Maven 下执行 Main 方法
效果如下:
看到以上的输出,说明方面确实介入到了 UserService 的被 @LogStartTime 注解的 fetchUserById(int userId)
方法。
下面用单元测试来验证一下,我们创建
HelloAopTest 测试用例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public class HelloAopTest { @Before public void setup() { MethodStartAspect.clearStartTime(); } @Test public void testSettingMethodStartTimeInThreadLocal() { new UserService().fetchUserById(9999); assertThat(MethodStartAspect.getStartTime(), notNullValue()); } } |
仍然是在 Maven 命令行下执行 mvn test
, 测试结果截屏如下
发生了什么?
我们说这是编译器把方面织入到了字节码中,那么来看一下到底发生了什么。别的类生成的字节码没任何异样,只有方面类和被关注的类,如 MethodStartAspect 和 UserService 生成的字节码与往常不同。在 IDEA 中打开 UserService 生成的类反编译后的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
package cc.unmi.service; import cc.unmi.aspects.LogStartTime; import cc.unmi.aspects.MethodStartAspect; import java.lang.annotation.Annotation; public class UserService { public UserService() { } @LogStartTime("Hello World") public String fetchUserById(int userId) { MethodStartAspect var10000 = MethodStartAspect.aspectOf(); Annotation var10001 = ajc$anno$0; if (ajc$anno$0 == null) { var10001 = ajc$anno$0 = UserService.class.getDeclaredMethod("fetchUserById", Integer.TYPE).getAnnotation(LogStartTime.class); } var10000.setStartTimeInThreadLocal((LogStartTime)var10001); try { Thread.sleep(1000L); } catch (InterruptedException var3) { var3.printStackTrace(); } System.out.println("start time: " + MethodStartAspect.getStartTime()); return "nameOf" + userId; } } |
我们看到在关注方法实现前端插入了对 MethodStartAspect
的 setStartTimeInThreadLocal()
的调用,也就是说把方面应用到了 UserService.fetchUserById(int userId)
上了。
再看下 MethodStartAspect
字节码反编译出的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
package cc.unmi.aspects; import org.aspectj.lang.NoAspectBoundException; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; @Aspect public class MethodStartAspect { private static ThreadLocal<Long> startTime = new ThreadLocal(); static { try { ajc$postClinit(); } catch (Throwable var1) { ajc$initFailureCause = var1; } } public MethodStartAspect() { } @Before("logStartTimePointcut(logStartTime)") public void setStartTimeInThreadLocal(LogStartTime logStartTime) { System.out.println(logStartTime.value()); startTime.set(System.currentTimeMillis()); System.out.println("saved method start time in threadLocal"); } public static Long getStartTime() { return (Long)startTime.get(); } public static void clearStartTime() { startTime.set((Object)null); } public static MethodStartAspect aspectOf() { if (ajc$perSingletonInstance == null) { throw new NoAspectBoundException("cc.unmi.aspects.MethodStartAspect", ajc$initFailureCause); } else { return ajc$perSingletonInstance; } } public static boolean hasAspect() { return ajc$perSingletonInstance != null; } } |
加入了静态初始化代码块,以及关注方法中要调用的 public static MethodStartAspect aspectOf()
静态方法。
再次说明一下,我们了解到使用 aspectj-maven-plugin 插件会对方面类(如 MethodStartAspect)以及被连接的类(如 UserService) 的字节码进行修改,这对我们接下来把切面定义在另一个模块时如何配置 pom.xml 会有所帮助。
切面定义由另一模块提供
到目前为止的例子都是在一个模块中既包含切面定义类,又包含被连接的类,所以只要在同一个 pom.xml 中简单配置就行。在实际应用场景中,我们很可能用一个单独的 Maven 模块来进行方面的定义,然后连接到另一个模块中去。我们来看一下这种场景该如何配置两个模块的 pom.xml 文件。
假设我们把方面定义相关的类 @LogStartTime
和 MethodStartAspect
移动一个单独的模块中去,该模块命名为 aspects
; 然后需要被切入的代码 UserService
放在模块 clients
中。整个项目目录结构如下组织:
由于我们知道了提供切面定义的模块的在编译器时字节码需要被修改,所以模块 aspects
的 pom.xml 需要如下的配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
<dependencies> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjrt</artifactId> <version>1.8.9</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>aspectj-maven-plugin</artifactId> <version>1.10</version> <configuration> <complianceLevel>1.8</complianceLevel> <source>1.8</source> </configuration> <executions> <execution> <goals> <goal>compile</goal> </goals> </execution> </executions> </plugin> </plugins> </build> |
这样编译器的 MethodStartAspect
类中会插入静态方法 aspectOf()
, hasAspect()
, 以及静态初始化代码块。如该提供切面定义的模块不配置 aspectj-maven-plugin
, 将在执行后面被连接的 UserService.fetchUserById(..)
时出现 MethodStartAspect.aspectOf()
方法找不到的错误。
而 UserService 的字节码也将被修改,所以模块 clients
也需要有 aspectj-maven-plugin
的配置,但与之前有所不同, 见下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
<dependencies> <dependency> <groupId>cc.unmi</groupId> <artifactId>aspects</artifactId> <version>1.0-SNAPSHOT</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>aspectj-maven-plugin</artifactId> <version>1.10</version> <configuration> <complianceLevel>1.8</complianceLevel> <source>1.8</source> <aspectLibraries> <aspectLibrary> <groupId>cc.unmi</groupId> <artifactId>aspects</artifactId> </aspectLibrary> </aspectLibraries> </configuration> <executions> <execution> <goals> <goal>compile</goal> </goals> </execution> </executions> </plugin> </plugins> </build> |
- 由
clients
要aspects
来提供切面定义,所以需要依赖aspects
模块 - 因为切面定义源文件不在本模块中,所以需要用 <aspectLibraries> 配置来指定切面定义提供者模块
- 由于
aspects
模块中配置了org.aspectj:aspectjrt:1.8.9
依赖,会传递到clients
模块,所以它不用配置
现在可以进到 clients
模块,命令行下执行
mvn exec:java -Dexec.mainClass="cc.unmi.HelloAop"
mvn test
能得到同样的执行结果
AspectJ 与 JDK 9
本文所有测试都是在 JDK 1.8 环境下进行的。
一开始因为我的电脑安装了 JDK 9, 配置了 aspectj-maven-plugin
后在 JDK 9 的环境下执行 mvn compile
就出现如下的错误:
查看了下 java -version
显示的是 JDK 9,后来通过 $JAVA_HOME, $PATH 的配置让 java -version
显示为 JDK 8 也是一样的错误,因为 /usr/bin/java -version
仍然是 JDK 9。因为 JDK 9 模块化处理后,相应目录中没有了 tools.jar 文件上,只有把 JDK 9 卸载后,/usr/bin/java -version
恢复为 JDK 8 后才能正常使用 aspectj-maven-plugin
.
这应该是目前 AspectJ 尚不能与 JDK 9 兼容的缘故。
本文示例代码在 Github 仓库 maven-weave-aspectj.
参考链接:
本文链接 https://yanbin.blog/maven-plugin-aspectj-weaving/, 来自 隔叶黄莺 Yanbin Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。
博主您好,我手里的一个项目跟文章中描述的很像,遇到一个问题请教一下。
跟你代码不同的是,我在项目A中,通过对三方jar包进行weave,比如logback,通过拦截指定方法生成链路,然后项目A打成jar包供项目B使用,但是B在执行package的时候始终无法成功织入,具体代码片段麻烦看下这里:https://stackoverflow.com/questions/74233815/meet-some-problems-about-aspectj-maven-plugin-and-aspectj
麻烦大佬有空看下,谢谢
博主,您好,按照你的aspectj-maven配置,编译时期还是会使用默认的编译插件,由于我是用的是jdk8,所以会报版本过低
我也是用的 JDK 8, 试下指定编译插件的版本。