寫在前面的話
相關背景及資源:
曹工說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進行修改:
-
編譯器織入,比如aspectJ的ajc編譯器,假如你自己負責實現這個ajc編譯器,你當然可以自己夾帶私貨,悄悄地往要編譯的class文件里,加點料,對不?這樣的話,編譯出來的class,和java源文件里的,其實是不一致的;
-
自己實現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,這一篇原文沒代碼,我自己整理了下,附上了具體的步驟,放在碼雲
第一種,需要使用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
andWeavingAdapter
.
第一種方式呢,我這里弄了個例子,代碼放在:
整個demo的代碼結構如下圖:
-
目標類,是要被增強的對象
package foo; public class StubEntitlementCalculationService { public void calculateEntitlement() { System.out.println("calculateEntitlement"); } }
-
切面類
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配置,指定要使用的切面,和要掃描的范圍
<!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>
-
測試類
package foo; public final class Main { public static void main(String[] args) { StubEntitlementCalculationService entitlementCalculationService = new StubEntitlementCalculationService(); // 如果進展順利,這處調用會被增強 entitlementCalculationService.calculateEntitlement(); } }
-
啟動測試
執行步驟: 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這個系列放一塊的,其實今天的代碼比較獨立,大家可以加我,我單獨發給大家也可以。