Spring之LoadTimeWeaver——一個需求引發(fā)的思考

最近有個需求——記錄應(yīng)用中某些接口被調(diào)用的軌跡,說白了,記錄下入?yún)?、出參等即可?/p>

我選用ApsectJ解決這個問題,前期討論說在接口層埋點(diǎn),但這樣有個問題,代碼侵入比較嚴(yán)重,需要修改每個需要關(guān)注的接口實現(xiàn)類。經(jīng)過一番討論,決定使用AOP攔截所有這樣的接口。

后面又有個新的要求——沙箱環(huán)境攔截,生產(chǎn)環(huán)境不予攔截。

這樣就有個眼前的問題需要我們解決,就是同一份應(yīng)用包如何區(qū)分沙箱環(huán)境和生產(chǎn)環(huán)境并執(zhí)行不同的行為。同事提醒我可以考慮Spring的LTW,即Load Time Weaving。

在Java 語言中,從織入切面的方式上來看,存在三種織入方式:編譯期織入、類加載期織入和運(yùn)行期織入。編譯期織入是指在Java編譯期,采用特殊的編譯器,將切面織入到Java類中;而類加載期織入則指通過特殊的類加載器,在類字節(jié)碼加載到JVM時,織入切面;運(yùn)行期織入則是采用CGLib工具或JDK動態(tài)代理進(jìn)行切面的織入。

AspectJ采用編譯期織入和類加載期織入的方式織入切面,是語言級的AOP實現(xiàn),提供了完備的AOP支持。它用AspectJ語言定義切面,在編譯期或類加載期將切面織入到Java類中。

AspectJ提供了兩種切面織入方式,第一種通過特殊編譯器,在編譯期,將AspectJ語言編寫的切面類織入到Java類中,可以通過一個Ant或Maven任務(wù)來完成這個操作;第二種方式是類加載期織入,也簡稱為LTW(Load Time Weaving)。

如何使用Load Time Weaving?首先,需要通過JVM的-javaagent參數(shù)設(shè)置LTW的織入器類包,以代理JVM默認(rèn)的類加載器;第二,LTW織入器需要一個 aop.xml文件,在該文件中指定切面類和需要進(jìn)行切面織入的目標(biāo)類。

下面我將通過一個簡單的例子在描述如何使用LTW。

例子所作的是記錄被調(diào)用方法的執(zhí)行時間和CPU使用率。其實這在實際生產(chǎn)中很有用,與其拉一堆性能測試工具,不如動手做個簡單的分析切面,使我們能很快得到一些性能指標(biāo)。我指的是沒有硬性的性能測試需求下。

首先我們編寫一個被織入的受體類,也就是被攔截的對象。

public class DemoBean {

public void run() {

System.out.println("Run");

}

}

接著,我們編寫分析方法執(zhí)行效率的切面。

@Aspect

public class ProfilingAspect {

@Around("profileMethod()")

public Object profile(ProceedingJoinPoint pjp) throws Throwable {

StopWatch sw = new StopWatch(getClass().getSimpleName());

try {

sw.start(pjp.getSignature().getName());

return pjp.proceed();

} finally {

sw.stop();

System.out.println(sw.prettyPrint());

}

}

@Pointcut("execution(public * com.shansun..*(..))")

public void profileMethod() {}

}

前文提到,我們還需要一個aop.xml。這個文件要求放在META-INF/aop.xml路徑下,以告知AspectJ Weaver我們需要把ProfilingAspect織入到應(yīng)用的哪些類中。

http://www.eclipse.org/aspectj/dtd/aspectj.dtd">


目前為止,本次切面的“攻”和“受”都準(zhǔn)備好了,我們還需要一個中間媒介——LoadTimeWeaver。我們將Spring的配置文件添加紅色標(biāo)識內(nèi)容。


http://www.springframework.org/schema/beans"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"

xmlns:context="Index of /schema/context" xmlns:tx="Index of /schema/tx"

xsi:schemaLocation="

Index of /schema/beans?http://www.springframework.org/schema/beans/spring-beans-2.5.xsd

Index of /schema/aop?http://www.springframework.org/schema/aop/spring-aop-2.5.xsd

Index of /schema/context?http://www.springframework.org/schema/context/spring-context-2.5.xsd

http://www.springframework.org/schema/tx?http://www.springframework.org/schema/tx/spring-tx-2.5.xsd">

通過<context:load-time-weaveraspectj-weaving="on" />使spring開啟loadtimeweaver,注意aspectj-weaving有三個選項: on, off, auto-detect, 如果設(shè)置為auto-detect, spring將會在classpath中查找aspejct需要的META-INF/aop.xml,如果找到則開啟aspectj weaving,這個邏輯在LoadTimeWeaverBeanDefinitionParser#isAspectJWeavingEnabled方法中:

protected boolean isAspectJWeavingEnabled(String value, ParserContext parserContext) {

if ("on".equals(value)) {

return true;

}

else if ("off".equals(value)) {

return false;

}

else {

// Determine default...

ClassLoader cl = parserContext.getReaderContext().getResourceLoader().getClassLoader();

return (cl.getResource(ASPECTJ_AOP_XML_RESOURCE) != null);

}

}

一切都準(zhǔn)備就緒——切面類、aop.xml、Spring的配置,我們就創(chuàng)建一個main方法來掩飾LTW的功效吧。

public class Main {

public static void main(String[] args) {

ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");

// DemoBean bean = (DemoBean) ctx.getBean("demoBean");

DemoBean bean = new DemoBean();

bean.run();

}

}

因為這個LTW使用成熟的AspectJ,我們并不局限于通知Spring beans的方法。所以上述代碼中從ApplicationContext中獲取Bean和直接實例化一個Bean的效果是一樣的。

注意,這里以使用Eclipse演示上述代碼為例,需要在運(yùn)行參數(shù)中稍作設(shè)置,即添加前文提到的-javaagent,來取代默認(rèn)的類加載器。

輸出結(jié)果如下:

Run

StopWatch 'ProfilingAspect': running time (millis) = 0

-----------------------------------------

ms?%?Task name

-----------------------------------------

0001?100%?run

至此,LTW可以正常使用了,但是麻煩的是我需要在VM參數(shù)里加上-javaagent這么個東東,如果不加會如何呢?試試看。

Exception in thread "main" java.lang.IllegalStateException: Must start with Java agent to use InstrumentationLoadTimeWeaver. See Spring documentation.

at org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver.addTransformer(InstrumentationLoadTimeWeaver.java:88)

at org.springframework.context.weaving.AspectJWeavingEnabler.postProcessBeanFactory(AspectJWeavingEnabler.java:69)

at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:553)

at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:536)

at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:362)

at org.springframework.context.support.ClassPathXmlApplicationContext.(ClassPathXmlApplicationContext.java:139)

at org.springframework.context.support.ClassPathXmlApplicationContext.(ClassPathXmlApplicationContext.java:83)

at com.shansun.multidemo.spring.Main.main(Main.java:25)

必需使用java agent么?仔細(xì)觀察異常的出處InstrumentationLoadTimeWeaver。再深入這個類的內(nèi)容,發(fā)現(xiàn)異常是由下述方法拋出的。

public void addTransformer(ClassFileTransformer transformer) {

Assert.notNull(transformer, "Transformer must not be null");

FilteringClassFileTransformer actualTransformer =

new FilteringClassFileTransformer(transformer, this.classLoader);

synchronized (this.transformers) {

if (this.instrumentation == null) {

throw new IllegalStateException(

"Must start with Java agent to use InstrumentationLoadTimeWeaver. See Spring documentation.");

}

this.instrumentation.addTransformer(actualTransformer);

this.transformers.add(actualTransformer);

}

}

不難發(fā)現(xiàn)它在校驗instrumentation是否為空的時候拋出的異常。有辦法啦,重寫InstrumentationLoadTimeWeaver的addTransformer方法,隱匿異常即可。

public class ExtInstrumentationLoadTimeWeaver extends

InstrumentationLoadTimeWeaver {

@Override

public void addTransformer(ClassFileTransformer transformer) {

try {

super.addTransformer(transformer);

} catch (Exception e) {}

}

}

這時,我們還需要做一件事,將Spring配置文件中的load-time-weaver入口設(shè)置為我們剛自定義的ExtInstrumentationLoadTimeWeaver即可。


http://www.springframework.org/schema/beans"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="Index of /schema/aop"

xmlns:context="Index of /schema/context" xmlns:tx="Index of /schema/tx"

xsi:schemaLocation="

Index of /schema/beans?http://www.springframework.org/schema/beans/spring-beans-2.5.xsd

Index of /schema/aop?http://www.springframework.org/schema/aop/spring-aop-2.5.xsd

Index of /schema/context?http://www.springframework.org/schema/context/spring-context-2.5.xsd

Index of /schema/tx?http://www.springframework.org/schema/tx/spring-tx-2.5.xsd">

再次運(yùn)行我們的main方法,發(fā)現(xiàn)只輸出了如下結(jié)果,切面沒有起作用。

Run

看到了么,同一份代碼、同一份配置,只需要在VM啟動參數(shù)中稍加變化,即可實現(xiàn)同一個應(yīng)用包在不同環(huán)境下可以自由選擇使用使用AOP功能。文章開頭提到的那個需求也就迎刃而解了。

這只是一種解決途徑,相信大家會有更好的方案。如果有,請您告訴我。J

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容