详解 Spring AOP LoadTimeWeaving (LTW)

LTW 可以解决的问题

  • 非spring管理的类依赖注入和切面不生效的问题
  • 调用类内方法切面不生效的问题

AOP 切面织入方式

编译时:比如使用 AspectJ 编译器
加载时:本文介绍的 AspectJ 的 LoadTimeWeaving (LTW)
运行时:Spring AOP 默认方式,通过动态代理或 cglib

LTW 原理

在类加载期通过字节码编辑技术将切面织入目标类,这种方式叫做 LTW(Load Time Weaving)。
使用 JDK5 新增的 java.lang.instrument 包,在类加载时对字节码进行转换,从而实现 AOP 功能。

使用 LTW

依赖类库
JDK 8 及以上, spring-AOP 和 aspectJ
(最低为 JDK 5)

org.springframework.boot:spring-boot-starter-aop:2.3.1.RELEASE
org.springframework:spring-aspects:5.2.7.RELEASE

完整代码
com\example\

@SpringBootApplication
@EnableLoadTimeWeaving
@EnableSpringConfigured
@EnableAsync(mode = AdviceMode.ASPECTJ)
public class AppApplication {
    public static void main(String[] args) {
        // 初始化 spring context
        ApplicationContext context = SpringApplication.run(AppApplication.class, args);

        // 创建 POJO,此时 TestService 会被注入到 POJO 中
        POJO pojo = new POJO();
        System.out.println("inject bean " + pojo.testService);

        TestService testService = context.getBean(TestService.class);
        // 正常调用切面
        testService.asyncPrint();
        // 切面的内部调用
        testService.print();
        // 非 spring 管理的类切面调用,spring 定义的切面
        pojo.print();
        // 非 spring 管理的类切面调用,自定义的切面
        pojo.print1();
    }
}
@Configurable
public class POJO {
	// 依赖注入
    @Autowired
    public TestService testService;

	// spring 的切面
    @Async
    public void print() {
        System.out.println("POJO print thread " + Thread.currentThread().toString());
    }
	
	// 自定义的切面
    @Profile
    public void print1() {
        System.out.println("POJO print1");
    }
}
public @interface Profile {
}
// 切面定义
@Aspect
public class ProfilingAspect {
	// 环绕通知,打印方法执行时间
    @Around("methodsToBeProfiled()")
    public Object profile(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.currentTimeMillis();
        Object obj = pjp.proceed();
        long cost = System.currentTimeMillis() - start;
        System.out.println("profile " + pjp.getSignature().toShortString() + " " + cost);
        return obj;
    }

	// 切点为 Profile 注解,且织入到方法执行中
    @Pointcut("execution(* *(..)) && @annotation(com.example.Profile)")
    public void methodsToBeProfiled() {
    }
}
@Component
public class TestService {

    @Async
    @Profile
    public void asyncPrint() {
        System.out.println("TestService print thread " + Thread.currentThread().toString());
    }

    public void print() {
        asyncPrint();
    }
}

org\aspectj\aop.xml (注意路径)

<!DOCTYPE aspectj PUBLIC "-//AspectJ//DTD//EN" "http://www.eclipse.org/aspectj/dtd/aspectj.dtd">
<aspectj>
	<weaver options="-showWeaveInfo -verbose">
		<include within="com.example.*" />
	</weaver>
	<aspects>
		<aspect name="com.example.ProfilingAspect" />
	</aspects>
</aspectj>

说明
@EnableLoadTimeWeaving 为开启 LTW,或使用 <context:load-time-weaver/>
@Configurable 必须和 @EnableSpringConfigured (或 <context:spring-configured/>) 配合使用
@Configurable 可指明在构造函数前或后注入
@EnableAsync 或 @EnableCaching 必须使用 ASPECTJ 模式

启动 VM 参数

-javaagent:path\spring-instrument-5.1.6.RELEASE.jar
-javaagent:path\aspectjweaver-1.9.2.jar

spring-instrument 用于类加载时修改字节码
aspectjweaver 用于 @Async、@Cacheable 等 spring 内置切面
(path替换为本地路径;若使用 Intellij Idea,配在 VM options 中)

输出

inject bean com.example.TestService@63cd604c
POJO print thread Thread[task-3,5,main]
TestService print thread Thread[task-1,5,main]
POJO print1
TestService print thread Thread[task-2,5,main]
profile TestService.asyncPrint() 0ms
profile POJO.print1() 0ms
profile TestService.asyncPrint() 0ms   

成功向 new 创建的对象注入 bean,且类内调用切面生效、 new 创建的对象切面生效

maven 配置

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>test-ltw</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.1.RELEASE</version>
        <relativePath/>
    </parent>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
            <version>2.3.1.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
            <version>5.2.7.RELEASE</version>
        </dependency>
    </dependencies>
</project>

Web项目

引入maven依赖 spring-boot-starter-web 即可,LoadTimeWeaving 用法不变。
对于直接使用 Tomcat 或 Jboss 的项目不需要 spring-instrument,Tomcat 等容器自带实现。

常见报错

  1. java.lang.IllegalStateException: ClassLoader [sun.misc.Launcher$AppClassLoader] does NOT provide an ‘addTransformer(ClassFileTransformer)’ method. Specify a custom LoadTimeWeaver or start your Java virtual machine with Spring’s agent: -javaagent:spring-instrument-{version}.jar

    缺少 javaagent spring-instrument

  2. Caused by: java.lang.NoSuchMethodException: XXX.aspectOf()。

    缺少 javaagent aspectjweaver

  3. 切面通知执行两次的问题
    若切点为 annotation 类型,对 method-call 和 method-execution 都会生效,导致通知代码被注入了两次,一次在外部调用方法的地方,一次在方法体执行的地方。应加上 execution(* *(…)), 详见:

https://stackoverflow.com/questions/34218012/spring-aop-and-aspectj-load-time-weaving-around-advice-will-be-invoked-twice-fo

LTW 官方文档

AspectJ:(第5节为 Load-Time Weaving)

https://www.eclipse.org/aspectj/doc/released/devguide/index.html

Spring AOP using AspectJ: (5.10.4)

https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#aop-using-aspectj

附录

  1. 调用类内方法切面失效的其他解决方案:

https://www.cnblogs.com/yjmyzz/p/why-spring-aop-does-not-work.html

思路是将自己注入到自己中

  1. 非spring管理的类实现依赖注入的其他解决方案:

https://stackoverflow.com/questions/310271/injecting-beans-into-a-class-outside-the-spring-managed-context

You can do this:

ApplicationContext ctx = ...
YourClass someBeanNotCreatedBySpring = ...
ctx.getAutowireCapableBeanFactory().autowireBeanProperties(
    someBeanNotCreatedBySpring,
    AutowireCapableBeanFactory.AUTOWIRE_AUTODETECT, true);

You can use @Autowired and so on within YourClass to specify fields to be injected etc.

  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值