Spring之LoadTimeWeaver——一個需求引發的思考

最近有個需求——記錄應用中某些接口被調用的軌跡,說白了,記錄下入參、出參等即可。

我選用ApsectJ解決這個問題,前期討論說在接口層埋點,但這樣有個問題,代碼侵入比較嚴重,需要修改每個需要關注的接口實現類。經過一番討論,決定使用AOP攔截所有這樣的接口。

後面又有個新的要求——沙箱環境攔截,生產環境不予攔截。

這樣就有個眼前的問題需要我們解決,就是同一份應用包如何區分沙箱環境和生產環境並執行不同的行爲。同事提醒我可以考慮Spring的LTW,即Load Time Weaving。

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


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

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

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

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

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

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

public class DemoBean {
public void run() {
System.out.println("Run");
}
}

接着,我們編寫分析方法執行效率的切面。

<span style="font-family: 'Courier New'; color: #646464; font-size: x-small;">@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() {}
}</span>

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

<span style="font-family: 'Courier New'; color: #008080; font-size: x-small;"><!DOCTYPE aspectj PUBLIC "-//AspectJ//DTD//EN" "http://www.eclipse.org/aspectj/dtd/aspectj.dtd">
<aspectj>
<weaver>
<include within="com.shansun..*" />
</weaver>
<aspects>
<!-- weave in just this aspect -->
<aspect name="com.shansun.multidemo.spring.ltw.ProfilingAspect" />
</aspects>
</aspectj></span>

目前爲止,本次切面的“攻”和“受”都準備好了,我們還需要一箇中間媒介——LoadTimeWeaver。我們將Spring的配置文件添加紅色標識內容。

<?xml version="1.0" encoding="GBK"?>

<beans xmlns="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-weaver aspectj-weaving="autodetect" />

<context:component-scan base-package="com.shansun.multidemo"></context:component-scan>
</beans>

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

<span style="font-family: 'Courier New'; color: #7f0055; font-size: x-small;"><strong>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);
}
}</strong></span>

一切都準備就緒——切面類、aop.xml、Spring的配置,我們就創建一個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演示上述代碼爲例,需要在運行參數中稍作設置,即添加前文提到的-javaagent,來取代默認的類加載器。

輸出結果如下:

Run

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

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

ms % Task name

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

0001 100% run

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

<span style="font-family: 'Courier New'; color: #ff0000; font-size: x-small;">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.<init>(ClassPathXmlApplicationContext.java:139)
at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:83)
at com.shansun.multidemo.spring.Main.main(Main.java:25)</span>

必需使用java agent麼?仔細觀察異常的出處InstrumentationLoadTimeWeaver。再深入這個類的內容,發現異常是由下述方法拋出的。

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);
}
}

不難發現它在校驗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入口設置爲我們剛自定義的ExtInstrumentationLoadTimeWeaver即可。

<?xml version="1.0" encoding="GBK"?>

<beans xmlns="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">
<context:load-time-weaver weaver-class="com.shansun.multidemo.spring.ExtInstrumentationLoadTimeWeaver" aspectj-weaving="autodetect" />

<context:component-scan base-package="com.shansun.multidemo"></context:component-scan>
</beans>

再次運行我們的main方法,發現只輸出了如下結果,切面沒有起作用。

Run

看到了麼,同一份代碼、同一份配置,只需要在VM啓動參數中稍加變化,即可實現同一個應用包在不同環境下可以自由選擇使用使用AOP功能。文章開頭提到的那個需求也就迎刃而解了。

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

在這裏給大家提供一個學習交流的平臺,java交流羣: 558787436

具有1-5工作經驗的,面對目前流行的技術不知從何下手,需要突破技術瓶頸的可以加羣。

在公司待久了,過得很安逸,但跳槽時面試碰壁。需要在短時間內進修、跳槽拿高薪的可以加羣。

如果沒有工作經驗,但基礎非常紮實,對java工作機制,常用設計思想,常用java開發框架掌握熟練的可以加羣。
 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章