1. 程式人生 > >曹工說Spring Boot原始碼(13)-- AspectJ的執行時織入(Load-Time-Weaving),基本內容是講清楚了(附原始碼)

曹工說Spring Boot原始碼(13)-- AspectJ的執行時織入(Load-Time-Weaving),基本內容是講清楚了(附原始碼)

寫在前面的話

相關背景及資源:

曹工說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原始碼第13篇,前一篇講了context:component-scan的完整解析,本篇,繼續解析context名稱空間裡的另一個重量級元素:load-time-weaver。它可以解決你用aop搞不定的事情。

大家如果熟悉aop,會知道aop的原理是基於beanPostProcessor的。比如平時,我們會在service類的部分方法上加@transactional,對吧,transactional是基於aop實現的。最終的效果就是,注入到controller層的service,並不是原始的service bean,而是一個動態代理物件,這個動態代理物件,會去執行你的真正的service方法前後,去執行事務的開啟和關閉等操作。

aop的限制就在於:被aop的類,需要被spring管理,管理的意思是,需要通過@component等,弄成一個bean。

那,假設我們想要在一個第三方的,沒被spring管理的類的一個方法前後,做些aop的事情,該怎麼辦呢?

一般來說,目前的方法主要是通過修改class檔案。

class檔案在什麼時候才真正生效?答案是:在下面這個方法執行完成後:

public Class<?> loadClass(String name) throws ClassNotFoundException {
    return loadClass(name, false);
}

一旦通過上述方法,獲取到返回的Class物件後,基本就不可修改了。

那根據這個原理,大致有3個時間節點(第二種包含了2個時間點),對class進行修改:

  1. 編譯器織入,比如aspectJ的ajc編譯器,假如你自己負責實現這個ajc編譯器,你當然可以自己夾帶私貨,悄悄地往要編譯的class檔案裡,加點料,對不?這樣的話,編譯出來的class,和java原始檔裡的,其實是不一致的;

  2. 自己實現classloader,在呼叫上述的loadClass(String name)時,自己加點料;通俗地說,這就是本課要講的load-time-weaving,即,載入時織入;

    其中,又分為兩種,因為我們知道,classloader去loadClass的時候,其實是分兩步的,一個是java程式碼層面,一個是JVM層面。

    java程式碼層面:你自定義的classloader,想怎麼玩就怎麼玩,比如針對傳進來的class,獲取到其inputStream後,對其進行修改(增強或進行解密等)後,再丟給JVM去載入為一個Class;

    JVM層面:Instrumentation機制,具體理論的東西我也說不清,簡單來說,就是java命令啟動時,指定agent引數,agent jar裡,有一個premain方法,該方法可以註冊一個位元組碼轉換器。

    位元組碼轉換器介面大致如下:

    public interface ClassFileTransformer {
        // 這個方法可以對引數中指定的那個class進行轉換,轉換後的class的位元組碼,通過本方法的返回引數返回
        // 即,本方法的返回值,就是最終的class的位元組碼
        byte[]
        transform(  ClassLoader         loader,
                    String              className,
                    Class<?>            classBeingRedefined,
                    ProtectionDomain    protectionDomain,
                    byte[]              classfileBuffer)
            throws IllegalClassFormatException;
    }

    大家參考下面兩篇文章。

    Java Instrumentation,這一篇原文沒程式碼,我自己整理了下,附上了具體的步驟,放在碼雲

    參考文章2

第一種,需要使用aspectj的編譯器來進行編譯,還是略顯麻煩;這裡我們主講第二種,LTW。

LTW其實,包含了兩部分,一部分是切面的問題(切點定義切哪兒,通知定義在切點處要嵌進去的邏輯),一部分是切面怎麼生效的問題。

我們下面分別來講。

Aspectj的LTW怎麼玩

我們可以參考aspectj的官網說明:

https://www.eclipse.org/aspectj/doc/released/devguide/ltw-configuration.html

這裡面提到了實現ltw的三種方式,其中第一種,就是我們前面說的java instrumentation的方式,只是這裡的agent是使用aspectjweaver.jar;第二種,使用了專有命令來執行,這種方式比較奇葩,直接跳過不理;第三種,和我們前面說的類似,就是自定義classloader的方式:

Enabling Load-time Weaving

AspectJ 5 supports several ways of enabling load-time weaving for an application: agents, a command-line launch script, and a set of interfaces for integration of AspectJ load-time weaving in custom environments.

  • Agents

    AspectJ 5 ships with a number of load-time weaving agents that enable load-time weaving. These agents and their configuration are execution environment dependent. Configuration for the supported environments is discussed later in this chapter.Using Java 5 JVMTI you can specify the -javaagent:pathto/aspectjweaver.jar option to the JVM.Using BEA JRockit and Java 1.3/1.4, the very same behavior can be obtained using BEA JRockit JMAPI features with the -Xmanagement:class=org.aspectj.weaver.loadtime.JRockitAgent

  • Command-line wrapper scripts aj

    The aj command runs Java programs in Java 1.4 or later by setting up WeavingURLClassLoader as the system class loader. For more information, see aj.The aj5 command runs Java programs in Java 5 by using the -javaagent:pathto/aspectjweaver.jar option described above. For more information, see aj.

  • Custom class loader

    A public interface is provided to allow a user written class loader to instantiate a weaver and weave classes after loading and before defining them in the JVM. This enables load-time weaving to be supported in environments where no weaving agent is available. It also allows the user to explicitly restrict by class loader which classes can be woven. For more information, see aj and the API documentation and source for WeavingURLClassLoader and WeavingAdapter.

第一種方式呢,我這裡弄了個例子,程式碼放在:

https://gitee.com/ckl111/spring-boot-first-version-learn/tree/master/all-demo-in-spring-learning/java-aspectj-agent

整個demo的程式碼結構如下圖:

  1. 目標類,是要被增強的物件

    package foo;
    
    public class StubEntitlementCalculationService {
    
        public void calculateEntitlement() {
            System.out.println("calculateEntitlement");
        }
    }
  2. 切面類

    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(){}
    }
  3. aop配置,指定要使用的切面,和要掃描的範圍

    <!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. 測試類

    package foo;
    
    public final class Main {
    
        public static void main(String[] args) {
            StubEntitlementCalculationService entitlementCalculationService = new StubEntitlementCalculationService();
         // 如果進展順利,這處呼叫會被增強
            entitlementCalculationService.calculateEntitlement();
        }
    }
  5. 啟動測試

    執行步驟:
    1.mvn clean package,得到jar包:java-aspectj-agent-1.0-SNAPSHOT
    
    2.把aspectjweaver-1.8.2.jar拷貝到和本jar包同路徑下
    
    3.cmd下執行:
    java -javaagent:aspectjweaver-1.8.2.jar -cp java-aspectj-agent-1.0-SNAPSHOT.jar foo.Main

    執行的效果如下:

Aspectj的LTW的原理剖析

我們這一小節,簡單說說其原理。我們前面提到,aspectj的ltw共三種方式,我們上面用了第一種,這種呢,其實就是基於instrumentation機制來的。

只是呢,這裡我們指定的agent是aspectj提供的aspectjweaver.jar。我這裡把這個jar包(我這裡版本是1.8.2)解壓縮了一下,我們來看看。

解壓縮後,在其META-INF/MANIFEST.MF中,我們看到了如下內容:

Manifest-Version: 1.0
Name: org/aspectj/weaver/
Specification-Title: AspectJ Weaver Classes
Specification-Version: 1.8.2
Specification-Vendor: aspectj.org
Implementation-Title: org.aspectj.weaver
Implementation-Version: 1.8.2
Implementation-Vendor: aspectj.org
Premain-Class: org.aspectj.weaver.loadtime.Agent   這個地方重點關注,這個是指定main執行前要執行的類
Can-Redefine-Classes: true

上面我們看到,其指定了:

Premain-Class: org.aspectj.weaver.loadtime.Agent

那麼我們看看這個類:

/**
 * Java 1.5 preMain agent to hook in the class pre processor
 * Can be used with -javaagent:aspectjweaver.jar
 * */
public class Agent { 

    /**
     * The instrumentation instance
     */
    private static Instrumentation s_instrumentation;

    /**
     * The ClassFileTransformer wrapping the weaver
     */
    private static ClassFileTransformer s_transformer = new ClassPreProcessorAgentAdapter();

    /**
     * JSR-163 preMain Agent entry method
     * 敲黑板,這個premain的方法簽名是定死了的,和我們main方法類似。其中,引數instrumentation是由JVM傳進來的
     * @param options
     * @param instrumentation
     */
    public static void premain(String options, Instrumentation instrumentation) {
        /* Handle duplicate agents */
        if (s_instrumentation != null) {
            return;
        }
        s_instrumentation = instrumentation;
        // 這裡,加了一個位元組碼轉換器
        s_instrumentation.addTransformer(s_transformer);
    }

    /**
     * Returns the Instrumentation system level instance
     */
    public static Instrumentation getInstrumentation() {
        if (s_instrumentation == null) {
            throw new UnsupportedOperationException("Java 5 was not started with preMain -javaagent for AspectJ");
        }
        return s_instrumentation;
    }

}

別的我也不多說,多的我也不懂,只要大家明白,這裡premain會在main方法執行前執行,且這裡的instrumentation由JVM傳入,且這裡通過執行:

s_instrumentation.addTransformer(s_transformer);

給JVM注入了一個位元組碼轉換器。

這個位元組碼轉換器的型別是,ClassPreProcessorAgentAdapter。

這個類裡面呢,翻來覆去,程式碼很複雜,但是大家想也知道,無非是去aop.xml檔案裡,找到要使用的Aspect切面。切面裡面定義了切點和切面邏輯。拿到這些後,就可以對目標class進行轉換了。

我大概翻了程式碼,解析aop.xml的程式碼在:org.aspectj.weaver.loadtime.ClassLoaderWeavingAdaptor類中。

    // aop檔案的名稱
    private final static String AOP_XML = "META-INF/aop.xml";

    /**
     * 載入aop.xml
     * Load and cache the aop.xml/properties according to the classloader visibility rules
     * 
     * @param loader
     */
    List<Definition> parseDefinitions(final ClassLoader loader) {
        
        List<Definition> definitions = new ArrayList<Definition>();
        try {
            String resourcePath = System.getProperty("org.aspectj.weaver.loadtime.configuration", AOP_XML);
            

            StringTokenizer st = new StringTokenizer(resourcePath, ";");

            while (st.hasMoreTokens()) {
                String nextDefinition = st.nextToken();
                ... 這裡面是具體的解析
            }
        }
         ...
        return definitions;
    }

AspectJ的LTW的劣勢

優勢我就不多說了,大家可以自由發揮,比如大家熟知的效能監控啥的,基本都是基於這個來做的。

劣勢是啥?大家發現了嗎,我們總是需要在啟動時,指定-javaagent引數,就像下面這樣:

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

大概有以下問題:

  • 很多時候,部署是由運維去做的,開發不能做到只給一個jar包,還得讓運維去加引數,要是運維忘了呢?風險很大;
  • 假設我們要進行ltw的是一個tomcat的webapp應用,但這個tomcat同時部署了好幾個webapp,但是另外幾個webapp其實是不需要被ltw的,但是麼辦法啊,粒度就是這麼粗。

基於以上問題,出現了spring的基於aspectJ進行了優化的,粒度更細的LTW。

具體我下節再講。

總結

本來是打算講清楚spring的context:load-time-weaver,無奈內容太多了,只能下節繼續。今天內容到這,謝謝大家。原始碼我是和spring這個系列放一塊的,其實今天的程式碼比較獨立,大家可以加我,我單獨發給大家也可以