曹工说Spring Boot源码(14)-- AspectJ的Load-Time-Weaving的两种实现方式细细讲解,以及怎么和Spring Instrumentation集成

写在前面的话

相关背景及资源:html

曹工说Spring Boot源码(1)-- Bean Definition究竟是什么,附spring思惟导图分享java

曹工说Spring Boot源码(2)-- Bean Definition究竟是什么,我们对着接口,逐个方法讲解git

曹工说Spring Boot源码(3)-- 手动注册Bean Definition不比游戏好玩吗,咱们来试一下web

曹工说Spring Boot源码(4)-- 我是怎么自定义ApplicationContext,从json文件读取bean definition的?spring

曹工说Spring Boot源码(5)-- 怎么从properties文件读取beanshell

曹工说Spring Boot源码(6)-- Spring怎么从xml文件里解析bean的apache

曹工说Spring Boot源码(7)-- Spring解析xml文件,到底从中获得了什么(上)json

曹工说Spring Boot源码(8)-- Spring解析xml文件,到底从中获得了什么(util命名空间)api

曹工说Spring Boot源码(9)-- Spring解析xml文件,到底从中获得了什么(context命名空间上)数组

曹工说Spring Boot源码(10)-- Spring解析xml文件,到底从中获得了什么(context:annotation-config 解析)

曹工说Spring Boot源码(11)-- context:component-scan,你真的会用吗(此次来讲说它的奇技淫巧)

曹工说Spring Boot源码(12)-- Spring解析xml文件,到底从中获得了什么(context:component-scan完整解析)

曹工说Spring Boot源码(13)-- AspectJ的运行时织入(Load-Time-Weaving),基本内容是讲清楚了(附源码)

工程代码地址 思惟导图地址

工程结构图:

ltw实现方式之定制classloader(适用容器环境)

本篇已是spring源码第14篇,前一篇讲了怎么使用aspectJ的LTW(load-time-weaver),也理解了它的原理,主要是基于java提供的intrumentation机制来实现。

这里强烈建议看下前一篇,对咱们下面的理解有至关大的帮助。

我这里简单重复一次,LTW是有多种实现方式的,它的意思是加载class时,进行切面织入。你们知道,咱们加载class,主要是经过java.lang.ClassLoader#loadClass(java.lang.String, boolean),这个方法在执行过程当中,会先交给父类classloader去加载,若是不行的话,再丢给本classloader的findClass方法来加载。

java.lang.ClassLoader#loadClass(java.lang.String, boolean)
protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            Class c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        // 委托父类classloader
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime()
                    // 父类classloader搞不定,本身来处理
                    c = findClass(name);
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

其中,findClass呢,是个空逻辑,主要供子类覆盖。咱们看看典型的java.net.URLClassLoader#findClass是怎么覆盖该方法的,这个classloader主要是根据咱们指定的url,去该url处获取字节流,加载class:

protected Class<?> findClass(final String name)
     throws ClassNotFoundException
{
    return AccessController.doPrivileged(
            new PrivilegedExceptionAction<Class>() {
                public Class run() throws ClassNotFoundException {
                    String path = name.replace('.', '/').concat(".class");
                    // 这里,获取url对应的Resource
                    Resource res = ucp.getResource(path, false);
                    if (res != null) {
                        try {
                            // 内部会调用JVM方法,define Class
                            return defineClass(name, res);
                        } catch (IOException e) {
                            throw new ClassNotFoundException(name, e);
                        }
                    } else {
                        throw new ClassNotFoundException(name);
                    }
                }
            }, acc);
    }
}

其中咱们关注defineClass:

private Class defineClass(String name, Resource res) throws IOException {
    URL url = res.getCodeSourceURL();
    ...
    // 获取url对应的资源的字节数组
    byte[] b = res.getBytes();
    // must read certificates AFTER reading bytes.
    CodeSigner[] signers = res.getCodeSigners();
    CodeSource cs = new CodeSource(url, signers);
    sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
    // 下面这个方法,最终就会调用一个JVM本地方法,交给虚拟机来加载class
    return defineClass(name, b, 0, b.length, cs);
}

其中defineClass最终会调用以下方法:

private native Class defineClass1(String name, byte[] b, int off, int len,
                                      ProtectionDomain pd, String source);

因此,你们能看到的是,loadClass其实有两个步骤:

  1. 获取class对应的字节数组
  2. 调用native方法,让JVM根据步骤1获取到的字节数组,来define一个Class。

因此,LTW的其中一种作法(前一篇文章里提到了),就是使用自定义的classloader,在第一步完成后,第二步开始前,插入一个步骤:织入切面

其实,目前来讲,不少容器就是采用这样的方式,我这里简单梳理了一下:

容器 支持设置ClassFileTransformer的classloader LTW实现方式
weblogic weblogic.utils.classloaders.GenericClassLoader 自定义classloader
glassfish org.glassfish.api.deployment.InstrumentableClassLoader 自定义classloader
tomcat org.apache.tomcat.InstrumentableClassLoader 自定义classloader
jboss http://www.javased.com/?source_dir=jboss-modules/src/main/java/org/jboss/modules/ModuleClassLoader.java 直接获取了容器使用的classloader,该classloader内含有transformer字段,能够调用该字段的addTransformer方法来添加切面逻辑。具体可参考:org.springframework.instrument.classloading.jboss.JBossModulesAdapter 自定义classloader
wehsphere com.ibm.ws.classloader.CompoundClassLoader 自定义classloader
jar包方式启动的独立应用(好比说pring ) 无支持的classloader,默认使用的sun.misc.Launcher.AppClassLoader是不支持设置ClassFileTransformer的 java instrumentation方式(即javaagent)

以上有一点要注意,第六种方式,即jar包独立应用(非tomcat容器那种),其使用的classloader,不支持设置ClassFileTransformer,因此其实现LTW是采用了其余方式的,上面也说了,是java instrumentation方式。

jboss自定义classloader实现ltw

jboss实现ltw的逻辑,是放在org.springframework.instrument.classloading.jboss.JBossLoadTimeWeaver。

这里面的逻辑简单来讲,就是:

  1. 获取当前线程使用的classloader,经过网上资料,猜想是使用了org.jboss.modules.ModuleClassLoader
  2. 获取classloader中的transformer field
  3. 调用transformer field的addTransformer方法,该方法接收一个ClassFileTransformer类型的参数

这里的第一步使用的classloader,估计是正确的,我在网上也找到了该类的代码:

http://www.javased.com/?source_dir=jboss-modules/src/main/java/org/jboss/modules/ModuleClassLoader.java

package org.jboss.modules; 

public class ModuleClassLoader extends ConcurrentClassLoader { 
 
    static { 
        try { 
            ClassLoader.registerAsParallelCapable(); 
        } catch (Throwable ignored) { 
        } 
    } 
 
    static final ResourceLoaderSpec[] NO_RESOURCE_LOADERS = new ResourceLoaderSpec[0]; 
 
    private final Module module; 
    // 这里就是我说的那个transformer 字段
    private final ClassFileTransformer transformer; 
    ...
}

由于不了解jboss,这个classloader,和我前面说的逻辑有一点点出入,有可能实际使用的classloader,是本classloader的一个子类,不过不影响分析。

咱们看看本classloader怎么loadClass的(完整代码参考以上连接):

private Class<?> defineClass(final String name, final ClassSpec classSpec, final ResourceLoader resourceLoader) { 
        final ModuleLogger log = Module.log; 
        final Module module = this.module; 
        log.trace("Attempting to define class %s in %s", name, module); 
 
        ...
        final Class<?> newClass; 
        try { 
            byte[] bytes = classSpec.getBytes(); 
            try { 
                if (transformer != null) { 
                    // 看这里啊,若是transformer不为空,就使用transformer对原有的class进行转换
                    bytes = transformer.transform(this, name.replace('.', '/'), null, null, bytes); 
                } 
                //使用转换后获得的bytes,去define一个新的class:newClass
                newClass = doDefineOrLoadClass(name, bytes, 0, bytes.length, classSpec.getCodeSource()); 
                module.getModuleLoader().addClassLoadTime(Metrics.getCurrentCPUTime() - start); 
                log.classDefined(name, module); 
            }
        }
        return newClass; 
    }

因此,从这里,你们能够看到,自定义classloader,实现ltw的思路,就在于将原始的class的字节数组拿到后,对其进行transform后,便可获取到加强或修改后的字节码,而后拿这个字节码丢给jvm去加载class。

接下来,咱们再看看tomcat的例子。

tomcat自定义classloader实现ltw

咱们能够简单看下spring的org.springframework.instrument.classloading.tomcat.TomcatLoadTimeWeaver#TomcatLoadTimeWeaver(java.lang.ClassLoader),里面的逻辑就是:在tomcat容器环境下,怎么实现ltw的。

里面大概有如下步骤:

  1. 利用当前线程的classloader,判断是否为org.apache.tomcat.InstrumentableClassLoader
  2. 若是是,则反射获取该classloader的addTransformer方法并保存起来,该方法接收一个ClassFileTransformer对象;
  3. 后续spring启动过程当中,就会调用第二步获取到的addTransformer来设置ClassFileTransformer

我本地有tomcat的源码,org.apache.tomcat.InstrumentableClassLoader 实际为一个接口:

package org.apache.tomcat;

import java.lang.instrument.ClassFileTransformer;

/**
 * Specifies a class loader capable of being decorated with
 * {@link ClassFileTransformer}s. These transformers can instrument
 * (or weave) the byte code of classes loaded through this class loader
 * to alter their behavior. Currently only
 * {@link org.apache.catalina.loader.WebappClassLoaderBase} implements this
 * interface. This allows web application frameworks or JPA providers
 * bundled with a web application to instrument web application classes
 * as necessary.
 *
 * @since 8.0, 7.0.64
 */
public interface InstrumentableClassLoader {

    /**
     * Adds the specified class file transformer to this class loader. The
     * transformer will then be able to instrument the bytecode of any
     * classes loaded by this class loader after the invocation of this
     * method.
     *
     * @param transformer The transformer to add to the class loader
     * @throws IllegalArgumentException if the {@literal transformer} is null.
     */
    void addTransformer(ClassFileTransformer transformer);

    /**
     * Removes the specified class file transformer from this class loader.
     * It will no longer be able to instrument the byte code of any classes
     * loaded by the class loader after the invocation of this method.
     * However, any classes already instrumented by this transformer before
     * this method call will remain in their instrumented state.
     *
     * @param transformer The transformer to remove
     */
    void removeTransformer(ClassFileTransformer transformer);
    
    ...

}

你们也看到了,这个接口,主要的方法就是添加或者删除一个ClassFileTransformer对象。咱们能够仔细看看这个类的javadoc:

Specifies a class loader capable of being decorated with

  • {@link ClassFileTransformer}s. These transformers can instrument
  • (or weave) the byte code of classes loaded through this class loader
  • to alter their behavior. Currently only
  • {@link org.apache.catalina.loader.WebappClassLoaderBase} implements this
  • interface. This allows web application frameworks or JPA providers
  • bundled with a web application to instrument web application classes
  • as necessary.

这里提到了,这些转换器(即ClassFileTransformer)主要用于织入其余字节码来改变原始class的行为。目前,仅org.apache.catalina.loader.WebappClassLoaderBase实现了这个接口。

那咱们就看看实现类的逻辑:

org.apache.catalina.loader.WebappClassLoaderBase

//用来保存add进来的ClassFileTransformer
private final List<ClassFileTransformer> transformers = new CopyOnWriteArrayList<ClassFileTransformer>();

@Override
public void addTransformer(ClassFileTransformer transformer) {

    if (transformer == null) {
        throw new IllegalArgumentException(sm.getString(
                "webappClassLoader.addTransformer.illegalArgument", getContextName()));
    }
    // 添加到了一个transformers字段里
    this.transformers.add(transformer);

    log.info(sm.getString("webappClassLoader.addTransformer", transformer, getContextName()));
}

接下来,咱们看看transformers在何时被使用:

/**
     * Find specified resource in local repositories.
     *
     * @return the loaded resource, or null if the resource isn't found
     */
    protected ResourceEntry findResourceInternal(final String name, final String path,
            final boolean manifestRequired) {
        // 这前面不少代码,都是去tomcat的各类类路径下(本身的lib、webapp的lib下)查找class字节码 
        ...

        

        if (isClassResource && entry.binaryContent != null &&
                this.transformers.size() > 0) {
            // If the resource is a class just being loaded, decorate it
            // with any attached transformers
            String className = name.endsWith(CLASS_FILE_SUFFIX) ?
                    name.substring(0, name.length() - CLASS_FILE_SUFFIX.length()) : name;
            String internalName = className.replace(".", "/");

            for (ClassFileTransformer transformer : this.transformers) {
                try {
                    // 这里,就是对获取到的原始字节码进行transform,该方法返回值就是修改过的字节码
                    byte[] transformed = transformer.transform(
                            this, internalName, null, null, entry.binaryContent
                    );
                    if (transformed != null) {
                        // 改后的字节码存起来,等待下一次循环时,做为新的input
                        entry.binaryContent = transformed;
                    }
                } catch (IllegalClassFormatException e) {
                    log.error(sm.getString("webappClassLoader.transformError", name), e);
                    return null;
                }
            }
        }

        return entry;

    }

因此,你们从这里也看得出来,tomcat实现ltw的思路,也是自定义classloader,在classloader里作文章。

其余的容器呢,咱们就不一一分析了。接下来,咱们介绍另外一种方式,即非容器环境下,使用的agent机制。

ltw实现方式之java instrumentation(适用非容器环境)

前面说了,容器环境下,通常各大容器为了支持ltw,实现了本身的classloader。

但假设是非容器环境,好比单独的java应用,好比spring boot应用呢?

这时候通常使用的sun.misc.Launcher.AppClassLoader,但这个是不支持add ClassFileTransformer的。

因此,只能采用其余方式,而java instrumentation就能够。这部分呢,你们请翻阅前一篇文章,里面讲得比较细,你们请看完下面一篇,再回头来看这部分。

曹工说Spring Boot源码(13)-- AspectJ的运行时织入(Load-Time-Weaving),基本内容是讲清楚了(附源码)

咱们在使用aspectJ的LTW时,-javaagent是直接使用了aspectjweaver.jar,相似下面这样子:

java -javaagent:aspectjweaver-1.8.2.jar -cp java-aspectj-agent-1.0-SNAPSHOT.jar foo.Main

但若是有同窗使用过spring集成aspectJ的LTW的话,会发现使用方法略有差别:

java -javaagent:spring-instrument-4.3.7.RELEASE.jar -cp java-aspectj-agent-1.0-SNAPSHOT.jar foo.Main

这里能够发现,-javaagent指定的jar包不同,为啥呢?

我这里写了一个利用spring-instrumentation来集成aspectJ的ltw的例子。

思路以下:

  1. 利用spring-instrumentation jar包来做为javaagent参数,这个jar包做为agent,会在main执行前先执行,里面的逻辑主要是:把JVM暴露出来的instrumentation,保存起来,保存到一个static field里,方便后续使用;
  2. 在测试代码中,获取到第一步保存的instrumentation,给它设置一个ClassFileTransformer,这个ClassFileTransformer不用本身写,直接使用aspectJ的便可。这个ClassFileTransformer呢,会去读取META-INF/aop.xml里面,看看要去加强哪些类,去加强便可。

在开始以前,咱们先看看spring-instrumentation这个jar包:

因此,spring-instrumentation很简单,一个类而已。

好了,咱们开始试验:

  1. 测试类

    package foo;
    
    import java.lang.instrument.Instrumentation;
    
    public final class Main {
    
    
        public static void main(String[] args) {
            // 下面这行是重点,完成前面说的第二步思路的事情
            InstrumentationLoadTimeWeaver.init();
    
            /**
             * 通过了上面的织入,下边这个StubEntitlementCalculationService已是ltw加强过的了
             */
            StubEntitlementCalculationService entitlementCalculationService = new StubEntitlementCalculationService();
    
            entitlementCalculationService.calculateEntitlement();
        }
    }
    package foo;
    
    public class StubEntitlementCalculationService {
    
        public void calculateEntitlement() {
            System.out.println("calculateEntitlement");
        }
    }
  2. 集成aspectJ

    foo.InstrumentationLoadTimeWeaver#init
    
    // 这个方法里的 ClassPreProcessorAgentAdapter,就是aspectJ的类,实现了ClassFileTransformer接口;
    // AspectJClassBypassingClassFileTransformer装饰了ClassPreProcessorAgentAdapter,对aspectJ自己的类不进行ltw,相似于一个静态代理,把须要ltw的类,交给ClassPreProcessorAgentAdapter
    public static void init() {
        addTransformer(new AspectJClassBypassingClassFileTransformer(new ClassPreProcessorAgentAdapter()));
    }

    这里的addTransformer,咱们看下,首先获取到spring-instrumentation.jar做为javaagent,保存起来的Instrumentation,而后调用其addTransformer,添加ClassFileTransformer

    public static void addTransformer(ClassFileTransformer transformer) {
        Instrumentation instrumentation = getInstrumentation();
        if (instrumentation != null) {
            instrumentation.addTransformer(transformer);
        }
    }
    
    
    private static final boolean AGENT_CLASS_PRESENT = isPresent(
                "org.springframework.instrument.InstrumentationSavingAgent",
                InstrumentationLoadTimeWeaver.class.getClassLoader());
    
    private static Instrumentation getInstrumentation() {
        if (AGENT_CLASS_PRESENT) {
            // 获取保存起来的Instrumentation
            return InstrumentationAccessor.getInstrumentation();
        }
        else {
            return null;
        }
    }
    
    private static class InstrumentationAccessor {
    
        public static Instrumentation getInstrumentation() {
            return InstrumentationSavingAgent.getInstrumentation();
        }
    }
  3. 其余aspectJ的ltw须要使用的东西

    咱们上面添加了aspectJ的ClassPreProcessorAgentAdapter,这个ClassFileTransformer就会去查找META-INF/aop.xml,进行处理。

    package foo;
    
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Pointcut;
    
    @Aspect
    public class ProfilingAspect {
    
        @Around("methodsToBeProfiled()")
        public Object profile(ProceedingJoinPoint pjp) throws Throwable {
            System.out.println("before");
            try {
                return pjp.proceed();
            } finally {
                System.out.println("after");
            }
        }
    
        @Pointcut("execution(public * foo..*.*(..))")
        public void methodsToBeProfiled(){}
    }

    aop.xml:

    <!DOCTYPE aspectj PUBLIC "-//AspectJ//DTD//EN" "https://www.eclipse.org/aspectj/dtd/aspectj.dtd">
    <aspectj>
    
        <weaver>
            <!-- only weave classes in our application-specific packages -->
            <include within="foo.*"/>
        </weaver>
    
        <aspects>
            <!-- weave in just this aspect -->
            <aspect name="foo.ProfilingAspect"/>
        </aspects>
    
    </aspectj>
  4. 测试效果:

    本实验的逻辑在于:
    1.经过agent的premain,将jvm暴露的instrumentation保存起来,到一个static的field里。
    2.这样,在main方法执行前,咱们已经把 instrumentation 存到了一个能够地方了,后续能够供咱们使用。
    3.而后,咱们再把aspectJ的classFileTransformer设置到第二步获取到的instrumentation里。
    
    
    执行步骤:
    1.mvn clean package,获得jar包:spring-aspectj-integration-1.0-SNAPSHOT.jar
    
    2.把aspectjweaver-1.8.2.jar和spring-instrument-4.3.7.RELEASE.jar拷贝到和本jar包同路径下
    
    3.cmd下执行:
    java -javaagent:spring-instrument-4.3.7.RELEASE.jar -cp spring-aspectj-integration-1.0-SNAPSHOT.jar;aspectjweaver-1.8.2.jar foo.Main

代码呢,我放在了:

https://gitee.com/ckl111/spring-boot-first-version-learn/tree/225530ad7fe1f1f6cd14e5ef5a954d8642ecefb5/all-demo-in-spring-learning/spring-aspectj-integration

总结

万丈高楼平地起,若是没有一个好的地基,多高的高楼也盖不起来。上面咱们就详细讲了ltw依赖的两种底层实现。

容器环境,主要靠自定义classloader,这种呢,启动时,无需加javaagent参数;

非容器环境,则主要靠java instrumentation,这种就要加javaagent,里面的jar呢,能够直接使用aspectJ的aspectjweaver.jar;也能够直接使用spring-instrumentation.jar。

spring的 使用时,若是是在非容器环境下,其实就是使用的spring-instrumentation.jar。

这部分呢,我截取了spring官方文档的一段话:

https://docs.spring.io/spring/docs/5.0.16.RELEASE/spring-framework-reference/core.html#aop-aj-ltw-environments

Generic Java applications

When class instrumentation is required in environments that do not support or are not supported by the existing LoadTimeWeaver implementations, a JDK agent can be the only solution. For such cases, Spring provides InstrumentationLoadTimeWeaver, which requires a Spring-specific (but very general) VM agent, org.springframework.instrument-{version}.jar (previously named spring-agent.jar).

To use it, you must start the virtual machine with the Spring agent, by supplying the following JVM options:

-javaagent:/path/to/org.springframework.instrument-{version}.jar

Note that this requires modification of the VM launch script which may prevent you from using this in application server environments (depending on your operation policies). Additionally, the JDK agent will instrument the entire VM which can prove expensive.

For performance reasons, it is recommended to use this configuration only if your target environment (such as Jetty) does not have (or does not support) a dedicated LTW.

翻译:简单来讲,就是,当class instrumentation 须要时,JDK agent就是惟一选择。此时,spring提供了InstrumentationLoadTimeWeaver,这时,须要指定一个agent,org.springframework.instrument-{version}.jar

使用方式以下:

-javaagent:/path/to/org.springframework.instrument-{version}.jar

这样呢,就会须要修改VM的启动脚本。并且,JDK agent会instrument整个VM,代价高昂。为了性能考虑,推荐只有在不得不使用时,才使用这种方式。

总的来讲,通过这两讲,把ltw的基础讲清楚了,下一讲,看看spring是怎么实现 的,有了这些基础,那会很轻松。

相关文章
相关标签/搜索