本文已收錄 【修煉內功】躍遷之路
在Spring AOP是如何代理的一文中介紹了Spring AOP的原理,瞭解到其經過JDK Proxy及CGLIB生成代理類來實現目標方法的切面(織入),這裏有一個比較重要的概念 - 織入(Weaving),本篇就來探討html
- 什麼是織入?
- 織入有哪些類型以及實現手段?
- Spring分別是如何支持的?
織入爲英文Weaving的直譯,可字面理解爲將額外代碼插入目標代碼邏輯中,實現對目標邏輯的加強、監控等,將不一樣的關注點進行解耦java
織入的手段有不少種,不一樣的手段也對應不一樣的織入時機git
C
ompile - T
ime W
eaving (CTW)github
編譯期織入web
在源碼編譯階段,修改/新增源碼,生成預期內的字節碼文件spring
如AspectJ、Lombok、MapStruct等express
Lombok、MapStruct等使用了 Pluggable Annotation Processing API 技術實現對目標代碼的修改/新增AspectJ則直接使用了acj編譯器apache
L
oad - T
ime - W
eaving (LTW)segmentfault
裝載期織入tomcat
在Class文件的裝載期,對將要裝載的字節碼文件進行修改,生成新的字節碼進行替換
如AspectJ、Java Instrumentation等
Java Instrumentation只提供了字節碼替換/重裝載的能力,字節碼文件的修改還須要藉助外部框架,如javassist、asm等javassist的使用能夠參考 Introduction to Javassist
R
un - T
ime - W
eaving (RTW)運行時織入
在程序運行階段,利用代理或者Copy目標邏輯的方式,生成新的Class並加載
如AspectJ、JDK Proxy、CGLIB等
在Spring AOP是如何代理的一文中所介紹的Spring AOP既是運行時織入
以javassist爲例,Copy目標邏輯並加強(統計接口耗時),生成新的Class
該示例可應用到 裝載期織入(使用新的Class替換目標Class)或 運行時織入(直接使用新的Class)
// 目標代碼邏輯 public interface Animal { default String barkVoice() { return "bark bark"; } } public class Dog implements Animal { private final Random r = new Random(); @Override @Statistics("doggie") public String barkVoice() { try { Thread.sleep(Math.abs(r.nextInt()) % 3000); } catch (InterruptedException e) { e.printStackTrace(); } return "汪~汪~"; } }
// 使用javassist,在目標代碼基礎上添加耗時統計邏輯,生成新的class並加載 ClassPool classPool = ClassPool.getDefault(); // Copy目標字節碼 CtClass dogClass = classPool.get(Dog.class.getName()); // 設置新的類名 dogClass.setName("proxy.Doggie"); // 獲取目標方法,並在其基礎上加強 CtMethod barkVoice = dogClass.getDeclaredMethod("barkVoice"); barkVoice.addLocalVariable("startTime", CtClass.longType); barkVoice.insertBefore("startTime = System.currentTimeMillis();"); barkVoice.insertAfter("System.out.println(\"The Dog bark in \" + (System.currentTimeMillis() - startTime) + \"ms\");"); // 生成新的class (因爲module機制的引入,在JDK9以後已不建議使用該方法) Class<?> doggieClass = dogClass.toClass(); // 使用新的class建立對象 Animal doggie = (Animal)doggieClass.getDeclaredConstructor().newInstance(); // 輸出 // > The Dog bark in 2453ms // > 汪~汪~ System.out.println(doggie.barkVoice());
JVM提供了兩種agent包加載能力:static agent load、dynamic agent load,可分別在啓動時(main函數運行以前)、運行時(main函數運行以後)加載agent包,並執行內部邏輯
Java Instrumentation用於對目標邏輯的織入,結合Java Agent可實如今啓動時織入以及在運行時動態織入
Java Agent及Java Instrumentation的使用示例能夠參考Guide to Java Instrumentation
在JVM啓動時(main函數執行以前)加載指定的agent,並執行agent中的邏輯
在一些項目中會發現,java的啓動參數中會存在javaagent參數(java -javaagent:MyAgent.jar -jar MyApp.jar
),其做用即是在啓動時加載指定的agent
這裏須要實現public static void premain(String agentArgs, Instrumentation inst)
方法,並在META-INF/MANIFEST.ME
文件中指定Premain-Class的完整類路徑
Premain-Class: com.manerfan.demo.agent.JavaAgentDemo
public class JavaAgentDemo { public static void premain(String agentArgs, Instrumentation inst) { /* main函數執行前執行該邏輯 */} }
使用示例見Guide to Java Instrumentation
在JVM運行時(main函數執行以後)加載指定的agent,並執行agent中的邏輯
這裏的神奇之處在於,即便JVM已經在運行,依然有能力讓JVM加載agent包,並對已經load的Class文件進行修改後,從新load
這裏須要實現public static void agentmain(String agentArgs, Instrumentation inst)
方法,並在META-INF/MANIFEST.ME
文件中指定Agent-Class的完整類路徑
Agent-Class: com.manerfan.demo.agent.JavaAgentDemo Can-Redefine-Classes: true Can-Retransform-Classes: true
public class JavaAgentDemo { public static void agentmain(String agentArgs, Instrumentation inst) { /* 可在jvm運行過程當中執行該邏輯 */} }
使用示例見Guide to Java Instrumentation
Dynamic Load (Agent-Main) 使用了Java Attach技術,使用VirtualMachine#attach與目標JVM進程創建鏈接,並經過VirtualMachine#loadAgent通知目標JVM進行加載指定的agent包,並執行定義好的 agentmain方法
利用Java Agent/Instrumentation的織入能力能夠作監控、信息收集、信息統計、等等,但僅僅如此麼?除了織入的能力是否還能夠利用agent加載的能力作一些其餘的事情?答案顯而易見,最多見的如Tomcat、GlassFish、JBoss等容器對Java Agent/Instrumentation的使用
另外不得不提的即是Java診斷利器Arthas,其利用Java Agent在目標JVM進程中啓動了一個Arthas Server,以便Arthas Client與之通訊,實如今Client端獲取目標JVM內的各類信息,同時使用Java Instrumentation對目標類/方法進行織入,以便動態獲取目標方法運行過程當中的各類狀態信息及監控信息
Q: JVM attach是什麼?使用Java Agent/Instrumentation還能實現什麼有意思的工具?
只須要添加註解@EnableLoadTimeWeaving
,Spring便會自動註冊LoadTimeWeaver,Spring運行在不一樣的容器中會有不一樣的LoadTimeWeaver實現,其奧祕在@EnableLoadTimeWeaving註解所引入的LoadTimeWeavingConfiguration,源碼比較簡單,再也不作分析
Runtime Environment | LoadTimeWeaver implementation |
---|---|
Running in Apache Tomcat | TomcatLoadTimeWeaver |
Running in GlassFish (limited to EAR deployments) | GlassFishLoadTimeWeaver |
Running in Red Hat’s JBoss AS or WildFly | JBossLoadTimeWeaver |
Running in IBM’s WebSphere | WebSphereLoadTimeWeaver |
Running in Oracle’s WebLogic | WebLogicLoadTimeWeaver |
JVM started with Spring InstrumentationSavingAgent (java -javaagent:path/to/spring-instrument.jar ) |
InstrumentationLoadTimeWeaver |
Fallback, expecting the underlying ClassLoader to follow common conventions (namely addTransformer and optionally a getThrowawayClassLoader method) |
ReflectiveLoadTimeWeaver |
須要注意的是,若是Spring並未運行在上述的幾大容器中,則須要添加spring-instrument.jar爲javaagent啓動參數
spring-instrument.jar中的惟一源碼InstrumentationSavingAgent的實現很是簡單,在JVM加載完spring-instrument.jar以後獲取到Instrumentation並暫存起來,以便LoadTimeWeavingConfiguration中獲取並封裝爲InstrumentationLoadTimeWeaver (LoadTimeWeaver)
public final class InstrumentationSavingAgent { private static volatile Instrumentation instrumentation; private InstrumentationSavingAgent() {} public static void premain(String agentArgs, Instrumentation inst) { instrumentation = inst; } public static void agentmain(String agentArgs, Instrumentation inst) { instrumentation = inst; } public static Instrumentation getInstrumentation() { return instrumentation; } }
LoadTimeWeaver的使用也很是簡單
@Component public class LtwComponent implements LoadTimeWeaverAware { private LoadTimeWeaver loadTimeWeaver; @Override public void setLoadTimeWeaver(LoadTimeWeaver loadTimeWeaver) { this.loadTimeWeaver = loadTimeWeaver; } @PostConstruct public void init() { loadTimeWeaver.addTransformer( /* A ClassFileTransformer */); } }
ApplicationContext的refresh邏輯中對LoadTimeWeaver作了判斷,若是Spring容器中註冊了LoadTimeWeaver,則會同時註冊 LoadTimeWeaverAware的處理器 LoadTimeWeaverAwareProcessor,參考 ApplicationContext給開發者提供了哪些(默認)擴展
藉助於@EnableLoadTimeWeaving,Spring在註冊LoadTimeWeaver的同時,還處理了AspectJ的Weaving
// org.springframework.context.annotation.LoadTimeWeavingConfiguration @Bean(name = ConfigurableApplicationContext.LOAD_TIME_WEAVER_BEAN_NAME) @Role(BeanDefinition.ROLE_INFRASTRUCTURE) public LoadTimeWeaver loadTimeWeaver() { // ... loadTimeWeaver的生成 if (this.enableLTW != null) { AspectJWeaving aspectJWeaving = this.enableLTW.getEnum("aspectjWeaving"); switch (aspectJWeaving) { case DISABLED: // AJ weaving is disabled -> do nothing break; case AUTODETECT: if (this.beanClassLoader.getResource(AspectJWeavingEnabler.ASPECTJ_AOP_XML_RESOURCE) == null) { // No aop.xml present on the classpath -> treat as 'disabled' break; } // aop.xml is present on the classpath -> enable AspectJWeavingEnabler.enableAspectJWeaving(loadTimeWeaver, this.beanClassLoader); break; case ENABLED: AspectJWeavingEnabler.enableAspectJWeaving(loadTimeWeaver, this.beanClassLoader); break; } } return loadTimeWeaver; }
在@EnableLoadTimeWeaving中設置aspectjWeaving爲AUTODETECT或ENABLED時,則會觸發AspectJWeaving(AspectJWeavingEnabler#enableAspectJWeaving的邏輯也較簡單,再也不深刻分析)
這裏,@Aspect註解修飾的類再也不須要註冊爲Bean,但因爲直接使用了AspectJ,須要依賴aop.xml配置文件,AspectJ配置文件的使用參考 LoadTime Weaving Configuration
Spring AOP LoadTimeWeaving示例,能夠查看 Load-time Weaving with AspectJ in the Spring Framework
若是直接運行Spring,須要添加 spring-instrument.jar爲 javaagent啓動參數
既然Spring的@EnableLoadTimeWeaving配置了AspectJWeaving,實際上是能夠直接使用AspectJ的,而無需侷限於Spring,而且AspectJ同時支持Complie-Time Weaving、Load-Time Weaving、Run-Time Weaving
AspectJ的使用示例能夠參考 Intro to AspectJ
AspectJ的完整使用文檔參見 AspectJ Development Guide | AspectJ LoadTime Weaving
以上,介紹了幾種AOP的實現方式
Spring AOP在運行時,經過解析@Aspect修飾的Bean,生成Advisor,並使用JDK Proxy及CGLIB生成代理類
Spring AOP在運行時,經過AspectJWeaver直接修改目標字節碼
AspectJ同時支持Complie-Time Weaving、Load-Time Weaving、Run-Time Weaving
除以上三種方式以外,Spring中還存在哪些方法實現AOP?
Spring AOP是如何代理的一文中介紹過,Spring在獲取全部Advisor時,除了解析@Aspect修飾的Bean以外,還會獲取全部註冊爲Advisor類型的Bean
上文有述,Advisor中包含Pointcut及Advise,前者用來匹配哪些方法須要被代理,後者用來定義代理的邏輯,Advisor已經具有Spring AOP對方法切入的完備條件,直接註冊Advisor類型的Bean一樣會被Spring AOP識別
@Component public class AnimalAdvisor extends AbstractPointcutAdvisor { private Pointcut pointcut; private Advice advice; public AnimalAdvisor() { this.pointcut = buildPointcut(); this.advice = buildAdvice(); } // 構建Pointcut("within(com.manerfan.demo..*) && @annotation(com.manerfan.demo.proxy.Statistics)") private Pointcut buildPointcut() { AbstractExpressionPointcut expressionPointcut = new AspectJExpressionPointcut(); expressionPointcut.setExpression("within(com.manerfan.demo..*)"); MethodMatcher methodMatcher = new AnnotationMethodMatcher(Statistics.class); // within(com.manerfan.demo..*) && @annotation(com.manerfan.demo.proxy.Statistics) return new ComposablePointcut(expressionPointcut).intersection(methodMatcher); } // 構建AroundAdvice,統計方法耗時 private Advice buildAdvice() { return (MethodInterceptor)invocation -> { StopWatch sw = new StopWatch(invocation.getMethod().getDeclaringClass().getName()); sw.start(invocation.getMethod().getName()); Object result = invocation.proceed(); sw.stop(); System.out.println(sw.prettyPrint()); return result; }; } // 設置Advisor的優先級 @Override public int getOrder() { return Ordered.HIGHEST_PRECEDENCE; } // ... getters }
@SpringBootApplication @EnableAspectJAutoProxy public class SpringDemoApplication { @Bean public Dog dog() { return new Dog(); } public static void main(String[] args) throws InterruptedException { ApplicationContext ctx = SpringApplication.run(SpringDemoApplication.class, args); Dog dog = ctx.getBean(Dog.class); System.out.println(dog.barkVoice()); } }
輸出
> StopWatch 'com.manerfan.demo.proxy.Dog': running time = 1161646370 ns --------------------------------------------- ns % Task name --------------------------------------------- 1161646370 100% barkVoice > 汪~汪~
Spring-Retry
能夠參考Spring-Retry的實現,以@EnableRetry爲入口,查看RetryConfiguration的實現
利用BeanPostProcessor,使用ProxyFactory直接對目標類生成代理
@Component public class AnimalAdvisingPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor { public AnimalAdvisingPostProcessor() { this.setProxyTargetClass(true); this.advisor = new AnimalAdvisor(); } }
這樣,能夠徹底不依賴@EnableAspectJAutoProxy註解,其自己模擬了@EnableAspectJAutoProxy的能力(AnnotationAwareAspectJAutoProxyCreator)
@SpringBootApplication public class SpringDemoApplication { @Bean public Dog dog() { return new Dog(); } public static void main(String[] args) throws InterruptedException { ApplicationContext ctx = SpringApplication.run(SpringDemoApplication.class, args); Dog dog = ctx.getBean(Dog.class); System.out.println(dog.barkVoice()); } }
輸出
> StopWatch 'com.manerfan.demo.proxy.Dog': running time = 1863050881 ns --------------------------------------------- ns % Task name --------------------------------------------- 1863050881 100% barkVoice > 汪~汪~
AbstractBeanFactoryAwareAdvisingPostProcessor的邏輯與 AnnotationAwareAspectJAutoProxyCreator十分類似,再也不深刻分析
Spring-Async
能夠參考Spring-Async的實現,以@EnableAsync爲入口,查看ProxyAsyncConfiguration的實現
Spring AOP在運行時,經過解析@Aspect修飾的Bean,生成Advisor,並使用JDK Proxy及CGLIB生成代理類
直接註冊Advisor類型的Bean,並經過JDK Proxy及CGLIB生成代理類
經過BeanPostProcessor#postProcessAfterInitialization,使用ProxyFactory直接生成代理類,不依賴@EnableAspectJAutoProxy
Spring AOP在運行時,經過AspectJWeaver直接修改目標字節碼
AspectJ同時支持Complie-Time Weaving、Load-Time Weaving、Run-Time Weaving
參考:
Spring AOP是如何代理的: https://segmentfault.com/a/11...
ApplicationContext給開發者提供了哪些(默認)擴展: https://segmentfault.com/a/11...
Introduction to Javassist: https://www.baeldung.com/java...
Guide to Java Instrumentation: https://www.baeldung.com/java...
Java診斷利器Arthas: https://github.com/alibaba/ar...
Load-time Weaving with AspectJ in the Spring Framework: https://docs.spring.io/spring...
LoadTime Weaving Configuration: https://www.eclipse.org/aspec...
Intro to AspectJ: https://www.baeldung.com/aspectj
AspectJ LoadTime Weaving: https://www.eclipse.org/aspec...
AspectJ Development Guide: https://www.eclipse.org/aspec...