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

寫在前面的話

相關背景及資源: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文件讀取beanjson

曹工說Spring Boot源碼(6)-- Spring怎麼從xml文件裏解析bean的tomcat

曹工說Spring Boot源碼(7)-- Spring解析xml文件,到底從中獲得了什麼(上)app

曹工說Spring Boot源碼(8)-- Spring解析xml文件,到底從中獲得了什麼(util命名空間)運維

曹工說Spring Boot源碼(9)-- Spring解析xml文件,到底從中獲得了什麼(context命名空間上)eclipse

曹工說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這個系列放一塊的,其實今天的代碼比較獨立,你們能夠加我,我單獨發給你們也能夠。

相關文章
相關標籤/搜索