红茶的个人站点

  • 首页
  • 专栏
  • 开发工具
  • 其它
  • 隐私政策
Awalon
Talk is cheap,show me the code.
  1. 首页
  2. 专栏
  3. Spring Boot 学习笔记
  4. 正文

Spring 源码学习 6:AOP

2025年6月23日 30点热度 0人点赞 0条评论

AOP 有三种实现方式:

  • 动态代理(Dynamic Proxy)

  • 编译时织入(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>

程序入口:

@SpringBootApplication
@EnableAspectJAutoProxy(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:

@Service
public class DemoService {
    public void doSomething() {
        System.out.println("业务逻辑执行中...");
    }
}

添加 Controller:

@RestController
public class DemoController {
    @Autowired
    private DemoService demoService;
​
    @GetMapping("/test")
    public String test() {
        demoService.doSomething();
        return "AOP 测试成功";
    }
}

添加切面:

@Aspect
@Component
public class LoggingAspect {
​
    // 定义切点:拦截所有 DemoService 方法
    @Pointcut("execution(* com.example.demo.DemoService.*(..))")
    public void serviceMethods() {}
​
    // 前置通知
    @Before("serviceMethods()")
    public void logBefore(JoinPoint joinPoint) {
        System.out.println("方法执行前:" + joinPoint.getSignature().getName());
    }
​
    // 环绕通知(支持控制方法执行流程)
    @Around("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 编译项目。查看切点所在类的字节码:

@Service
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 参数:

image-20250622215947158

内容如下:

-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

可以借助工具 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

本文的完整示例代码可以从这里获取。

参考资料

  • 黑马程序员Spring视频教程,深度讲解spring5底层原理

  • Mojo's AspectJ Maven Plugin – Introduction

  • Spring Boot集成 Load-time Weaving (LTW)

  • SpringBoot中使用LoadTimeWeaving技术实现AOP功能 - takumiCX - 博客园

  • Maven Plugin :: Spring Boot

本作品采用 知识共享署名 4.0 国际许可协议 进行许可
标签: aop spring
最后更新:2025年6月23日

魔芋红茶

加一点PHP,加一点Go,加一点Python......

点赞
< 上一篇
下一篇 >

文章评论

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回复

COPYRIGHT © 2021 icexmoon.cn. ALL RIGHTS RESERVED.
本网站由提供CDN加速/云存储服务

Theme Kratos Made By Seaton Jiang

宁ICP备2021001508号

宁公网安备64040202000141号