原文地址:http://www.myexception.cn/software-architecture-design/602651.htmlhtml
Spring之LoadTimeWeaver——一個需求引起的思考java
最近有個需求——記錄應用中某些接口被調用的軌跡,說白了,記錄下入參、出參等便可。spring
我選用ApsectJ解決這個問題,前期討論說在接口層埋點,但這樣有個問題,代碼侵入比較嚴重,須要修改每一個須要關注的接口實現類。通過一番討論,決定使用AOP攔截全部這樣的接口。app
後面又有個新的要求——沙箱環境攔截,生產環境不予攔截。eclipse
這樣就有個眼前的問題須要咱們解決,就是同一份應用包如何區分沙箱環境和生產環境並執行不一樣的行爲。同事提醒我能夠考慮Spring的LTW,即Load Time Weaving。ide
在Java 語言中,從織入切面的方式上來看,存在三種織入方式:編譯期織入、類加載期織入和運行期織入。編譯期織入是指在Java編譯期,採用特殊的編譯器,將切面織入到Java類中;而類加載期織入則指經過特殊的類加載器,在類字節碼加載到JVM時,織入切面;運行期織入則是採用CGLib工具或JDK動態代理進行切面的織入。 工具
AspectJ採用編譯期織入和類加載期織入的方式織入切面,是語言級的AOP實現,提供了完備的AOP支持。它用AspectJ語言定義切面,在編譯期或類加載期將切面織入到Java類中。 post
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"); } }
接着,咱們編寫分析方法執行效率的切面。
@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織入到應用的哪些類中。
<!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>
目前爲止,本次切面的「攻」和「受」都準備好了,咱們還須要一箇中間媒介——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="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd http://www.springframework.org/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-weaver aspectj-weaving="on" /> 使 spring 開啓 loadtimeweaver, 注意aspectj-weaving 有三個選項 : on, off, auto-detect, 若是設置爲 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); } }
一切都準備就緒——切面類、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這麼個東東,若是不加會如何呢?試試看。
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)
必需使用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="http://www.springframework.org/schema/aop" xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd http://www.springframework.org/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 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
參考文檔:
一、使用AspectJ LTW(Load Time Weaving)
二、Spring LoadTimeWeaver 的那些事兒
三、在Spring應用中使用AspectJ