• SpringBoot中使用LoadTimeWeaving技术实现AOP功能


    1.1 LTW与不同的切面织入时机

    AOP——面向切面编程,通过为目标类织入切面的方式,实现对目标类功能的增强。按切面被织如到目标类中的时间划分,主要有以下几种:

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

    • 2.编译期织入
      使用特殊的编译器在编译期将切面织入目标类,这种比较少见,因为需要特殊的编译器的支持。

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

    1.2 JDK实现LTW的原理

    可以使用JKD的代理功能让代理器访问到JVM的底层组件,借此向JVM注册类文件转换器,在类加载时对类文件的字节码进行转换。具体而言,java.lang.instrument包下定义了ClassFileTransformer接口,该接口的作用如下面的注释所描述

    * An agent provides an implementation of this interface in order
    * to transform class files.
    * The transformation occurs before the class is defined by the JVM.
    

    可以通过实现该接口,并重写如下抽象方法自定义类文件转换规则

    byte[]
    transform(  ClassLoader         loader,
                String              className,
                Class<?>            classBeingRedefined,
                ProtectionDomain    protectionDomain,
                byte[]              classfileBuffer)
        throws IllegalClassFormatException;
    

    classfileBuffer是原始类文件对应的字节码数组,返回的byte[]为转化后的字节码数组,如果返回null,则表示不进行字节码处理。

    而java.lang.instrument包下的Instrumentation接口则可以将我们自定义的ClassTransFormer向JVM内部的组件进行注册

    void
    addTransformer(ClassFileTransformer transformer);
    

    在实际使用中,可以通过JVM的-javaagent代理参数在启动时获取JVM内部组件的引用,将ClassFileTransformer实例注册到JVM中,JVM在加载Class文件时,会先调用这个ClassTransformer的transform()方法对Class文件的字节码进行转换,比如织入切面中定义的横切逻辑,实现AOP功能。整个过程可以入下所示

    1.3 如何在Spring中实现LTW

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

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

    * Defines the contract for adding one or more
    * {@link ClassFileTransformer ClassFileTransformers} to a {@link ClassLoader}.
    *
    public interface LoadTimeWeaver {
    

    Spring的LTW支持AspectJ定义的切面,既可以是直接使用AspectJ语法定义的切面,也可以是使用@AspectJ注解,通过java类定义的切面。Spring LTW通过读取classpath下META-INF/aop.xml文件,获取切面类和要被切面织入的目标类的相关信息,通过LoadTimeWeaver在ClassLoader加载类文件时将切面织入目标类中,其工作原理如下所示

    Spring中可以通过LoadTimeWeaver将Spring提供的ClassFileTransformer注册到ClassLoader中。在类加载期,注册的ClassFileTransformer读取类路径下META-INF/aop.xml文件中定义的切面类和目标类信息,在目标类的class文件真正被VM加载前织入切面信息,生成新的Class文件字节码,然后交给VM加载。因而之后创建的目标类的实例,就已经实现了AOP功能。

    2. Springboot中使用LTW实现AOP的例子

    实现一个简单的AOP需求,在方法调用前后打印出开始和结束的日志信息。

    • 相关的maven依赖和插件
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
    
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    
    </dependencies>
    
    <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>
    
    

    这里通过maven插件的方式为JVM设置代理,通过-javaagent参数指定织入器类包的路径,这样就可以在类加载期将切面织入,更多关于javaagent的知识可以参考javaagent

    • 织入目标类
    /**
     * @author: takumiCX
     * @create: 2018-12-19
     **/
    @Component
    public class LtwBean {
    
    
        public void test(){
            System.out.println("process.......");
        }
    
    }
    

    只有一个test()方法,通过@Componet注解向容器注册。

    • 切面类
    /**
     * @author: takumiCX
     * @create: 2018-12-19
     **/
    @Aspect
    public class LogMethodInvokeAspect {
    
        @Pointcut("execution(public * com.takumiCX.ltw.*.*(..))")
        public void pointCut(){
    
        }
    
    
        @Around("pointCut()")
        public void advise(ProceedingJoinPoint pjp) throws Throwable {
    
    
            Signature signature = pjp.getSignature();
            System.out.println(signature+" start..... ");
            pjp.proceed();
            System.out.println(signature+" end......");
        }
    
    }
    

    @Aspect注解表示这是一个切面类

    • 配置类
    /**
     * @author: takumiCX
     * @create: 2018-12-19
     **/
    @Configuration
    @ComponentScan("com.takumiCX.ltw")
    @EnableLoadTimeWeaving(aspectjWeaving=AUTODETECT)
    public class CustomLtwConfig{
    }
    

    通过@@EnableLoadTimeWeaving开启LTW功能,可以通过属性aspectjWeaving指定LTW的开启策略

    1. ENABLED
      开启LTW
    2. DISABLED
      不开启LTW
    3. AUTODETECT
      如果类路径下能读取到META-INF/aop.xml文件,则开启LTW,否则关闭
    • 在META-INF文件夹下编写aop.xml文件

    aop.xml文件内容

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE aspectj PUBLIC "-//AspectJ//DTD//EN" "http://www.eclipse.org/aspectj/dtd/aspectj.dtd">
    <aspectj>
    
        <!--要织入切面的目标类-->
        <weaver>
            <include within="com.takumiCX.ltw..*" />
        </weaver>
        <!--切面类-->
        <aspects>
            <aspect name="com.takumiCX.ltw.aspect.LogMethodInvokeAspect" />
        </aspects>
    </aspectj>
    

    这样我们的Spring容器就能加载该文件读取到描述目标类和切面类的相关信息,容器在加载目标类的class文件到jvm之前,会将切面类中定义的增强逻辑织入到class文件中,真正加载到jvm中的是织入切面后的class文件,因而通过该class文件创建出的目标类的实例,不需要经过动态代理就能实现AOP相关功能。

    • 测试类
    /**
     * @author: takumiCX
     * @create: 2018-12-20
     **/
    @RunWith(SpringRunner.class)
    @SpringBootTest(classes ={CustomLtwConfig.class})
    public class LTWTest {
    
        @Autowired
        private LtwBean ltwBean;
    
    
        @Test
        public void testLTW() throws InterruptedException {
    
            ltwBean.test();
    
        }
    
    }
    

    最后的结果如下

    方法调用前后分别记录的开始和结束的日志信息,说明我们的切面成功的织入到了目标类。但是这里可能有一个疑问,这真的是LTW(Load TimeWeaving)通过在类加载期织入切面起到的作用吗?有没有可能是LTW没起作用,是Spring AOP默认通过运行期生成动态代理的方式实现的AOP。
    我们的LogMethodInvokeAspect切面类上并没有加@Component注解向容器注册,并且配置类CustomLtwConfig上也没有加@EnableAspectJAutoProxy注解开启Aspectj的运行时动态代理,所以这里基于动态代理的AOP并不会生效。

    为了验证我们的想法,将aop.xml文件删除

    重新运行测试代码

    AOP没起到作用,说明刚才的AOP功能确实是通过LTW技术实现的。

    当我们给切面类加上@Component注解,给配置类加上@EnableAspectJAutoProxy

    /**
     * @author: takumiCX
     * @create: 2018-12-19
     **/
    @Aspect
    @Component
    public class LogMethodInvokeAspect {
    
    /**
     * @author: takumiCX
     * @create: 2018-12-19
     **/
    @Configuration
    @ComponentScan("com.takumiCX.ltw")
    @EnableAspectJAutoProxy
    @EnableLoadTimeWeaving(aspectjWeaving=AUTODETECT)
    public class CustomLtwConfig{
    }
    

    再次运行测试类时,发现AOP又生效了,这时候类路径下并没有aop.xml,所以这时候AOP是Spring在运行期通过动态代理的方式实现的。

    3. 参考资料

    《精通Spring4.x企业应用开发实战》
    《spring揭秘》
    https://sexycoding.iteye.com/blog/1062372

  • 相关阅读:
    lombok 下的@Builder注解用法
    (springboot)自定义Starter
    各种 Spring-Boot-Starters系列 介绍
    MQ的使用场景
    SpringBoot中使用rabbitmq,activemq消息队列和rest服务的调用
    RPC原理详解
    Java性能优化的50个细节
    thinkphp3.2.2有预览的多图上传
    关于php中的exec命令
    关于thinkphp3.1无法加载模块解决办法
  • 原文地址:https://www.cnblogs.com/takumicx/p/10150344.html
Copyright © 2020-2023  润新知