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


写在前面的话

相关背景及资源:

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

工程结构图:

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() {
                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 transformers = new CopyOnWriteArrayList();

@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就可以。这部分呢,大家请翻阅前一篇文章,里面讲得比较细,大家请看完下面一篇,再回头来看这部分。

我们在使用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:

    
    
    
        
            
            
        
    
        
            
            
        
    
    
    
  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的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是怎么实现context:load-time-weaver的,有了这些基础,那会很轻松。

相关