寫在前面的話
相關背景及資源:
曹工說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其實有兩個步驟:
- 獲取class對應的字節數組
- 調用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。
這里面的邏輯簡單來說,就是:
- 獲取當前線程使用的classloader,通過網上資料,猜測是使用了
org.jboss.modules.ModuleClassLoader
- 獲取classloader中的transformer field
- 調用transformer field的addTransformer方法,該方法接收一個ClassFileTransformer類型的參數
這里的第一步使用的classloader,估計是正確的,我在網上也找到了該類的代碼:
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的。
里面大概有以下步驟:
- 利用當前線程的classloader,判斷是否為org.apache.tomcat.InstrumentableClassLoader
- 如果是,則反射獲取該classloader的addTransformer方法並保存起來,該方法接收一個ClassFileTransformer對象;
- 后續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的例子。
思路如下:
- 利用spring-instrumentation jar包來作為javaagent參數,這個jar包作為agent,會在main執行前先執行,里面的邏輯主要是:把JVM暴露出來的instrumentation,保存起來,保存到一個static field里,方便后續使用;
- 在測試代碼中,獲取到第一步保存的instrumentation,給它設置一個ClassFileTransformer,這個ClassFileTransformer不用自己寫,直接使用aspectJ的即可。這個ClassFileTransformer呢,會去讀取META-INF/aop.xml里面,看看要去增強哪些類,去增強即可。
在開始之前,我們先看看spring-instrumentation這個jar包:
所以,spring-instrumentation很簡單,一個類而已。
好了,我們開始試驗:
-
測試類
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"); } }
-
集成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(); } }
-
其他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>
-
測試效果:
本實驗的邏輯在於: 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
代碼呢,我放在了:
總結
萬丈高樓平地起,如果沒有一個好的地基,多高的高樓也蓋不起來。上面我們就詳細講了ltw依賴的兩種底層實現。
容器環境,主要靠自定義classloader,這種呢,啟動時,無需加javaagent參數;
非容器環境,則主要靠java instrumentation,這種就要加javaagent,里面的jar呢,可以直接使用aspectJ的aspectjweaver.jar;也可以直接使用spring-instrumentation.jar。
spring的context:load-time-weaver使用時,如果是在非容器環境下,其實就是使用的spring-instrumentation.jar。
這部分呢,我截取了spring官方文檔的一段話:
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 providesInstrumentationLoadTimeWeaver
, which requires a Spring-specific (but very general) VM agent,org.springframework.instrument-{version}.jar
(previously namedspring-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是怎么實現context:load-time-weaver的,有了這些基礎,那會很輕松。