AOP 有三种实现方式:
-
-
编译时织入(Compile Time Weaving,CTW)
-
加载时织入(Load Time Weaving,LTW)
动态代理
添加 AOP 相关依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.24</version>
</dependency>
程序入口:
proxyTargetClass = true) // 强制使用 CGLIB 代理
(public class Demo1Application {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(Demo1Application.class, args);
DemoService demoService = context.getBean(DemoService.class);
System.out.println(demoService.getClass());
}
}
添加 Service:
public class DemoService {
public void doSomething() {
System.out.println("业务逻辑执行中...");
}
}
添加 Controller:
public class DemoController {
private DemoService demoService;
"/test")
( public String test() {
demoService.doSomething();
return "AOP 测试成功";
}
}
添加切面:
public class LoggingAspect {
// 定义切点:拦截所有 DemoService 方法
"execution(* com.example.demo.DemoService.*(..))")
( public void serviceMethods() {}
// 前置通知
"serviceMethods()")
( public void logBefore(JoinPoint joinPoint) {
System.out.println("方法执行前:" + joinPoint.getSignature().getName());
}
// 环绕通知(支持控制方法执行流程)
"serviceMethods()")
( public Object logAround(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("方法执行前(环绕)");
Object result = pjp.proceed(); // 执行目标方法
System.out.println("方法执行后(环绕)");
return result;
}
}
运行程序,访问页面后就能看到 AOP 生效。此外,被 AOP 定义切点的类(DemoService
)从容器中获取的实例是代理类型(DemoService$$SpringCGLIB$$0
),因此这里是使用代理实现的 AOP。
编译时织入
可以在编译阶段将 AOP 相关代码织入字节码文件,这样做的好处是可以绕过代理实现的局限性(比如不能对类方法 AOP,不能自调用等)。
同样需要增加 AspectJ 相关依赖:
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>${aspectj.version}</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>${aspectj.version}</version>
</dependency>
使用 AspectJ Maven 插件进行编译:
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<version>1.15.0</version>
<executions>
<execution>
<goals>
<!-- mvn compile 时使用插件 -->
<goal>compile</goal>
<!-- mvn test 时使用插件 -->
<goal>test-compile</goal>
</goals>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjtools</artifactId>
<!-- 与引入的 AspectJ 依赖一致 -->
<version>${aspectj.version}</version>
</dependency>
</dependencies>
<configuration>
<!-- 要与源码及目标字节码 JAVA 级别一致 -->
<complianceLevel>${java.version}</complianceLevel>
</configuration>
</plugin>
更多 aspectj-maven-plugin 插件的信息可以看。
执行 mvn clean compile
编译项目。查看切点所在类的字节码:
public class DemoService {
public DemoService() {
}
public void doSomething() {
JoinPoint var1 = Factory.makeJP(ajc$tjp_0, this, this);
LoggingAspect.aspectOf().logBefore(var1);
doSomething_aroundBody1$advice(this, var1, LoggingAspect.aspectOf(), (ProceedingJoinPoint)var1);
}
static {
ajc$preClinit();
}
}
可以看到被插件写入了用于 AOP 的增强代码。运行程序后同样可以看到 AOP 效果生效,且此时从容器中获取到的DemoService
的 bean 是原始类型,并非代理类型。
此外,因为是编译时织入,所以不需要将切面类LoggingAspect
添加到容器:
@Aspect
public class LoggingAspect {...}
只使用
@Aspect
注解即可,不需要@Component
注解。
需要特别注意的是,这种方式下,只有通过 AspectJ Maven 插件编译的代码才具备 AOP 功能,如果不是(比如通过 Idea 编译),就不会生效。
通过修改 Idea 下的 Maven 运行相关配置,可以开启将 IDE 的编译功能委托给 Maven。
加载时织入
添加依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-instrument</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId> <!-- 提供 TransactionAspectSupport -->
</dependency>
spring-instrument
是一个 Spring 定义的 instrument,spring-tx
是事务相关的类,LTW 会用到,如果不引入会报错。
在入口类增加@EnableLoadTimeWeaving
注解:
@SpringBootApplication
@EnableLoadTimeWeaving
public class Demo3Application {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(Demo3Application.class, args);
DemoController bean = context.getBean(DemoController.class);
System.out.println(bean.getClass());
}
}
该注解的aspectjWeaving
属性表明运行方式:
-
ENABLED
,启用 -
DISABLED
,关闭 -
AUTODETECT
,自动,如果检测到META-INF/ aop. xml
文件就启用
默认是自动。
切面类:
@Aspect
public class LogAspect {
// 定义切点:拦截所有 Service 层方法
@Pointcut("execution(* cn.icexmoon.demo3.controller.*.*(..))")
public void controllerMethods() {}
// 前置通知
@Before("controllerMethods()")
public void logBefore(JoinPoint joinPoint) {
System.out.println("方法执行前:" + joinPoint.getSignature().getName());
}
// 环绕通知(支持控制方法执行流程)
@Around("controllerMethods()")
public Object logAround(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("方法执行前(环绕)");
Object result = pjp.proceed(); // 执行目标方法
System.out.println("方法执行后(环绕)");
return result;
}
}
同样的,不需要由容器管理,因此不需要使用
@Component
注解。
切点所在的类:
@Controller
@RequestMapping("/demo")
@Slf4j
public class DemoController {
@GetMapping
@ResponseBody
public String demo() {
log.info("demo() is called.");
return "demo";
}
}
添加META-INF/aop.xml
文件:
<!DOCTYPE aspectj PUBLIC "-//AspectJ//DTD//EN" "http://www.eclipse.org/aspectj/dtd/aspectj.dtd">
<aspectj>
<!--要织入切面的目标类-->
<weaver>
<include within="cn.icexmoon.demo3..*" />
</weaver>
<!--切面类-->
<aspects>
<aspect name="cn.icexmoon.demo3.aspect.LogAspect" />
</aspects>
</aspectj>
在这个文件中描述要加载时织入的切点与切面。
修改应用的启动配置,增加 JVM 参数:
内容如下:
-javaagent:C:\Users\70748\.m2\repository\org\springframework\spring-instrument\6.2.8\spring-instrument-6.2.8.jar -javaagent:C:\Users\70748\.m2\repository\org\aspectj\aspectjweaver\1.9.24\aspectjweaver-1.9.24.jar
要增加两个 jar 包作为javaagent
,一个是spring-instrument.jar
(注意版本要与项目的 Spring 版本一致),另一个是 aspectjweaver.jar
(AspectJ 执行 LTW 的 jar 包)。
现在启动应用后,虽然编译后的字节码没有任何 AOP 增强代码,但 JVM 加载的字节码被增强了,因此可以观察到 AOP 依然有效。
除了通过 IDE 启动,还可以修改 Maven 插件,使用 Spring Maven 插件启动:
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<jvmArguments>-Dfile.encoding=UTF-8</jvmArguments>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
<agents>
<agent>
C:\Users\70748\.m2\repository\org\aspectj\aspectjweaver\1.9.24\aspectjweaver-1.9.24.jar
</agent>
<agent>
C:\Users\70748\.m2\repository\org\springframework\spring-instrument\6.2.8\spring-instrument-6.2.8.jar
</agent>
</agents>
</configuration>
</plugin>
现在直接使用插件运行(spring-boot:run
),可以正常启动应用,但是如果尝试打包(mvn package
),就会报错。原因是在测试阶段(goal:run
)mvn 会使用 JVM 加载并执行测试用例,此时没有通过 JVM 参数指定 agent。
可以通过配置maven-surefire-plugin
插件,在测试阶段同样通过 JVM 参数指定 agent:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.3</version>
<configuration>
<argLine>
-javaagent:"C:\Users\70748\.m2\repository\org\aspectj\aspectjweaver\1.9.24\aspectjweaver-1.9.24.jar"
-javaagent:"C:\Users\70748\.m2\repository\org\springframework\spring-instrument\6.2.8\spring-instrument-6.2.8.jar"
</argLine>
</configuration>
</plugin>
maven-surefire-plugin
是一个扩展 Maven 测试阶段行为的插件,可以利用它实现跳过测试阶段等功能。
Arthas
可以借助工具 查看 JVM 中运行的字节码以确认 LTW 是否生效。
以 LTW 的方式运行项目:
java -javaagent:./spring-instrument-6.2.8.jar -javaagent:./aspectjweaver-1.9.24.jar -jar demo3-0.0.1-SNAPSHOT.jar
这里我采用打包后在 Linux 上运行,并在 Linux 上使用 Arthas 查看 LTW 效果。原因是在 Windows 的 Powershell 中启动 Arthas 后非常卡,无法正常输入命令,
原因不明。原因是 JDK 导致的,WIndows 下使用 JDK 22 启动 Arthas 后会存在此问题,将 JDK 换为 21 后问题消失。
启动 Arthas:
[icexmoon@192 download]$ java -jar arthas-boot.jar
Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8
[INFO] JAVA_HOME: /usr/lib/jvm/jdk22
[INFO] arthas-boot version: 4.0.5
[INFO] JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8
[INFO] Found existing java process, please choose one and input the serial number of the process, eg : 1. Then hit ENTER.
* [1]: 21482 demo3-0.0.1-SNAPSHOT.jar
Arthas 会自动嗅探 JVM 运行的应用,这里是demo3-0.0.1-SNAPSHOT.jar
,因此只要按照提示输入相应的编号进入对应的应用进行后续操作即可:
1
[INFO] Start download arthas from remote server: https://arthas.aliyun.com/download/4.0.5?mirror=aliyun
[INFO] Download arthas success.
[INFO] arthas home: /home/icexmoon/.arthas/lib/4.0.5/arthas
[INFO] Try to attach process 21482
可以使用命令help
查看可用的命令列表:
[arthas@21482]$ help NAME DESCRIPTION help Display Arthas Help auth Authenticates the current session
可以用sc
命令查询 JVM 加载了哪些类:
[arthas@21482]$ sc cn.icexmoon.* cn.icexmoon.demo3.Demo3Application cn.icexmoon.demo3.controller.DemoController cn.icexmoon.demo3.controller.DemoController$AjcClosure1
使用jad
命令可以对类的字节码进行反编译:
[arthas@21482]$ jad cn.icexmoon.demo3.controller.DemoController
本文的完整示例代码可以从获取。
文章评论