1. 程式人生 > >曹工說Spring Boot原始碼(14)-- AspectJ的Load-Time-Weaving的兩種實現方式細細講解,以及怎麼和Spring Instrumentation整合

曹工說Spring Boot原始碼(14)-- AspectJ的Load-Time-Weaving的兩種實現方式細細講解,以及怎麼和Spring Instrumentation整合

寫在前面的話

相關背景及資源:

曹工說Spring Boot原始碼(1)-- Bean Definition到底是什麼,附spring思維導圖分享

曹工說Spring Boot原始碼(2)-- Bean Definition到底是什麼,咱們對著介面,逐個方法講解

曹工說Spring Boot原始碼(3)-- 手動註冊Bean Definition不比遊戲好玩嗎,我們來試一下

曹工說Spring Boot原始碼(4)-- 我是怎麼自定義ApplicationContext,從json檔案讀取bean definition的?

曹工說Spring Boot原始碼(5)-- 怎麼從properties檔案讀取bean

曹工說Spring Boot原始碼(6)-- Spring怎麼從xml檔案裡解析bean的

曹工說Spring Boot原始碼(7)-- Spring解析xml檔案,到底從中得到了什麼(上)

曹工說Spring Boot原始碼(8)-- Spring解析xml檔案,到底從中得到了什麼(util名稱空間)

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