写点什么

🏆【Spring 技术专题】「动态代理技术」Spring 框架中 Aspectj 和 LoadTimeWeaving 的动态代理技术实现指南

发布于: 5 小时前
🏆【Spring技术专题】「动态代理技术」Spring框架中Aspectj和LoadTimeWeaving的动态代理技术实现指南

前提介绍

当我们聊到 Spring 框架的项目实际开发中,用的强大的功能之一就是(面向切面编程)的这门 AOP 技术。如果使用得当,它的最大的作用就是侵入性比较少并且简化我们的工作任务(节省大量的重复性编码),最为重要的一点是,它可以让我们在不改变原有代码的情况下,织入我们的逻辑,尤其是在我们没有源代码的时候,而且当我们恢复之前的逻辑的时候,只需要去掉代理就可以了。

AOP 的动态代理

Spring AOP 的常规的实现方式为 cglib 和 jdk 动态代理。两者均可实现,只是性能上略有差异,此处不再详述。


  1. 无论是 JDK 的动态代理和 Cglib 实现的 Spring 代理机制都有一些的局限性,那就是生成的代理对象在内部调用方法时,AOP 功能无效,因为毕竟不是代理对象的调用而是 this 的调用。

  2. 他们都只能对 public、protected 类型的成员方法生效。


当然,也是有变通的方案解决,比如将 bean 当做属性注入到自身,然后所有方法调用都通过这个属性来调用。或者通过 AopContext.currentProxy 的方式去获取代理对象。但是这些解决方案,在开发过程中开发者很容易因为疏忽导致出现问题。


所以,如果需要一种更加强大和易用的 aop 实现方案,那就是字节码编织技术 aspectj。通过修改字节码,可以实现对所有方法进行切面,包括(final、private、static 类型的方法),功能强大。并且 spring 支持 aspectj 方式的 aop。

AOP 技术类型

在介绍强大的 Aspectj 的技术之前,我们先进行对 AOP 技术实现基础进行分类,通过为目标类织入切面的方式,实现对目标类功能的增强。按切面被织如到目标类中的时间划分


  • 编译时:使用特殊的编译器在编译期将切面织入目标类,这种比较少见,因为需要特殊的编译器的支持。例如,AspectJ 编译器,很少有到,目前我还没有用到。

  • 加载时:通过字节码编辑技术在类加载期将切面织入目标类中,它的核心思想是:在目标类的 class 文件被 JVM 加载前,通过自定义类加载器或者类文件转换器将横切逻辑织入到目标类的 class 文件中,然后将修改后 class 文件交给 JVM 加载。这种织入方式可以简称为 LTW(LoadTimeWeaving),AspectJ 的 LoadTimeWeaving (LTW)

  • 运行时:运行期通过为目标类生成动态代理的方式实现 AOP 就属于运行期织入,这也是 Spring AOP 中的默认实现,并且提供了两种创建动态代理的方式:JDK 自带的针对接口的动态代理和使用 CGLib 动态创建子类的方式创建动态代理。


这里我们介绍是 Spring 整合 AspectJ 的 LTW 机制。属于动态加载织入。

LTW 可以解决的问题

  • 非 spring 管理的类依赖注入和切面不生效的问题。

  • 调用类内方法切面不生效的问题。

  • AOP 切面织入方式

LTW 的原理

类加载期通过字节码编辑技术将切面织入目标类,这种方式叫做 LTW(Load Time Weaving)。


使用 JDK5 新增的 java.lang.instrument 包,在类加载时对字节码进行转换,从而实现 AOP 功能。


JDK 的代理功能让代理器访问到 JVM 的底层组件,借此向 JVM 注册类文件转换器,在类加载时对类文件的字节码进行转换 ClassFileTransformer 接口。具体方向可以研究一下 java agent 技术即可。

Spring 中实现 LTW

  • Spring 中默认通过运行期生成动态代理的方式实现切面的织入,实现 AOP 功能,但是 Spring 也可以使用 LTW 技术来实现 AOP,并且提供了细粒度的控制,单个 ClassLoader 范围内实施类文件转换。

  • Spring 中的 org.springframework.instrument.classloading.LoadTimeWeaver 接口定义了为类加载器添加 ClassFileTransfomer 的抽象

  • Spring 的 LTW 支持 AspectJ 定义的切面,既可以是直接使用 AspectJ 语法定义的切面,也可以是使用 @AspectJ 注解,通过 java 类定义的切面。

  • Spring LTW 通过读取 classpath 下 META-INF/aop.xml 文件,获取切面类和要被切面织入的目标类的相关信息,通过 LoadTimeWeaver 在 ClassLoader 加载类文件时将切面织入目标类中。



  1. Spring 中可以通过 LoadTimeWeaver 将 Spring 提供的 ClassFileTransformer 注册到 ClassLoader 中。

  2. 在类加载期,注册的 ClassFileTransformer 读取类路径下 META-INF/aop.xml 文件中定义的切面类和目标类信息,在目标类的 class 文件真正被 VM 加载前织入切面信息,生成新的 Class 文件字节码,然后交给 VM 加载。

  3. 因而之后创建的目标类的实例,就已经实现了 AOP 功能。

maven 依赖和插件

spring-AOP 和 aspectJ

spring 的 maven 配置
    <dependency>      <groupId>org.springframework</groupId>      <artifactId>spring-core</artifactId>      <version>5.2.5</version>    </dependency>    <dependency>      <groupId>org.springframework</groupId>      <artifactId>spring-context</artifactId>      <version>5.2.5</version>    </dependency>    <dependency>      <groupId>org.springframework</groupId>      <artifactId>spring-aop</artifactId>      <version>5.2.5</version>    </dependency>    <dependency>      <groupId>org.springframework</groupId>      <artifactId>spring-aspects</artifactId>      <version>5.2.5</version>    </dependency>    <dependency>      <groupId>org.springframework</groupId>      <artifactId>spring-instrument</artifactId>      <version>5.2.5</version>    </dependency>
复制代码
springboot 的配置
<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>  <dependency>      <groupId>org.springframework</groupId>      <artifactId>spring-instrument</artifactId>            <version>5.2.7.RELEASE</version>    </dependency></dependencies>
复制代码


其中都会有 maven aspectjWeaver 包


 <dependency>    <groupId>aspectj</groupId>    <artifactId>aspectjweaver</artifactId>    <version>1.5.3</version></dependency>
复制代码

spring 全注解方式

@Configuration//开启Aspectj运行时编织@EnableLoadTimeWeaving(aspectjWeaving = AspectJWeaving.ENABLED)//开启非spring容器bean的依赖注入支持@EnableSpringConfigured@ComponentScan(basePackages = "demo")public class AppConfig extends WebMvcConfigurationSupport {}
复制代码

springboot 全注解方式

@SpringBootApplication@EnableLoadTimeWeaving@EnableSpringConfigured@EnableAsync(mode = AdviceMode.ASPECTJ)
复制代码

切面类

@Aspect//让spring可以将依赖注入到切面对象中@Configurablepublic class SampleAspectj {  @Autowired  private RestBean rbean;  @Around(value = "@annotation(rpc) ")  public void aop(ProceedingJoinPoint joinPoint,Rpc rpc) {    rbean.rwar();  }}
复制代码

对象切点类

@Configurablepublic class TestOriginObject {    // 依赖注入    @Autowired    public TestService testService;
// spring 的切面 @Async public void print() { System.out.println("TestOriginObject print thread " + Thread.currentThread().toString()); }
// 自定义的切面 @Rpc public void print1() { System.out.println("TestOriginObject print1"); }}
复制代码


@Componentpublic class TestService {
@Async @Profile public void asyncPrint() { System.out.println("TestService print thread " + Thread.currentThread().toString()); }
public void print() { asyncPrint(); } private static void test04() { log.info("------------test04-----------"); }
private void test03() { log.info("------------test03-----------"); }
public void test02() { log.info("------------test02-----------"); }}
复制代码

aop.xml 配置

放到/src/main/resources/META-INF 目录下


<!DOCTYPE aspectj PUBLIC "-//AspectJ//DTD//EN" "https://www.eclipse.org/aspectj/dtd/aspectj.dtd"><aspectj>    <weaver options="-showWeaveInfo -XnoInline -Xset:weaveJavaxPackages=true -Xlint:ignore -verbose -XmessageHandlerClass:org.springframework.aop.aspectj.AspectJWeaverMessageHandler">        <!-- 只对指定包下的类进行编织 -->        <include within="demo..*"/>    </weaver>    <aspects>        <!-- 使用指定的切面类进行编织 -->        <aspect name="test.SampleAspectj"/>    </aspects></aspectj>
复制代码


@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 中        TestOriginObject pojo = new TestOriginObject();        System.out.println("inject bean " + pojo.testService);        TestService testService = context.getBean(TestService.class);        // 正常调用切面        testService.asyncPrint();        // 切面的内部调用        testService.print();        // 非 spring 管理的类切面调用,spring 定义的切面        pojo.print();        // 非 spring 管理的类切面调用,自定义的切面        pojo.print1();        testService.test02();      testService.test03();      testService.test04();    }}
复制代码
说明
  • @EnableLoadTimeWeaving 为开启 LTW,或使用 context:load-time-weaver/

  • @Configurable 必须和 @EnableSpringConfigured (或 context:spring-configured/) 配合使用

  • @Configurable 可指明在构造函数前或后注入

  • @EnableAsync 或 @EnableCaching 必须使用 ASPECTJ 模式


@EnableAspectJAutoProxy 也会启动运行时代理植入方式。

启动 VM 参数

-javaagent:path\spring-instrument-5.1.6.RELEASE.jar-javaagent:path\aspectjweaver-1.9.2.jar
复制代码


可以采用 Maven 方式进行打包进入执行


<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <configuration> <argLine> -javaagent:"${settings.localRepository}/org/aspectj/aspectjweaver/${aspectj.version}/aspectjweaver-${aspectj.version}.jar" -javaagent:"${settings.localRepository}/org/springframework/spring-instrument/${spring.version}/spring-instrument-${spring.version}.jar" <!-- -Dspring.profiles.active=test--> </argLine> </configuration> </plugin> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <agent> ${settings.localRepository}/org/aspectj/aspectjweaver/${aspectj.version}/aspectjweaver-${aspectj.version}.jar </agent> <agent> ${settings.localRepository}/org/springframework/spring-instrument/${spring.version}/spring-instrument-${spring.version}.jar </agent> </configuration>   </plugin> </plugins></build>
复制代码

LTW 官方文档

  • AspectJ: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


public class Run {  public static void main(String[] args) {    //关键代码,用于在程序中开启agent    //通过bytebuddy拿到当前jvm的Instrumentation实例    Instrumentation instrumentation = ByteBuddyAgent.install();    //激活Aspectj的代理对象    Agent.agentmain("", instrumentation);    //激活spring代理对象    InstrumentationSavingAgent.agentmain("", instrumentation);    //启动spring容器    //...  }}
复制代码


发布于: 5 小时前阅读数: 7
用户头像

🏆 2021年InfoQ写作平台-签约作者 🏆 2020.03.25 加入

👑【酷爱计算机技术、醉心开发编程、喜爱健身运动、热衷悬疑推理的”极客狂人“】 🏅 【Java技术领域,MySQL技术领域,APM全链路追踪技术及微服务、分布式方向的技术体系等】 “任何足够先进的技术都是魔法“

评论

发布
暂无评论
🏆【Spring技术专题】「动态代理技术」Spring框架中Aspectj和LoadTimeWeaving的动态代理技术实现指南