曹工說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是怎麼實現 的,有了這些基礎,那會很輕鬆。

相關文章
相關標籤/搜索