本文已收录【修炼内功】跃迁之路
在Spring AOP是如何代理的一文中介绍了Spring AOP的原理,了解到其通过JDK Proxy及CGLIB生成代理类来实现目标方法的切面(织入),这里有一个比较重要的概念 - 织入(Weaving),本篇就来探讨
- 什么是织入?
- 织入有哪些类型以及实现手段?
- Spring分别是如何支持的?
什么是织入
织入为英文Weaving的直译,可字面理解为将额外代码插入目标代码逻辑中,实现对目标逻辑的增强、监控等,将不同的关注点进行解耦
织入的手段有很多种,不同的手段也对应不同的织入时机
-
C
ompile -T
imeW
eaving (CTW)编译期织入
在源码编译阶段,修改/新增源码,生成预期内的字节码文件
如AspectJ、Lombok、MapStruct等
Lombok、MapStruct等使用了 Pluggable Annotation Processing API 技术实现对目标代码的修改/新增
AspectJ则直接使用了acj编译器
-
L
oad -T
ime -W
eaving (LTW)装载期织入
在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());
JDK中的Load-Time Weaving
JVM提供了两种agent包加载能力:static agent load、dynamic agent load,可分别在启动时(main函数运行之前)、运行时(main函数运行之后)加载agent包,并执行内部逻辑
Java Instrumentation用于对目标逻辑的织入,结合Java Agent可实现在启动时织入以及在运行时动态织入
Java Agent及Java Instrumentation的使用示例可以参考Guide to Java Instrumentation
Static Load
在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
Dynamic Load
在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还能实现什么有意思的工具?
Spring对Load-Time Weaving的支持
只需要添加注解@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给开发者提供了哪些(默认)扩展
Spring AOP的Load-Time Weaving织入方式
EnableLoadTimeWeaving
借助于@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启动参数
AspectJ Agent
既然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
Spring AOP的其他实现方式
以上,介绍了几种AOP的实现方式
-
@EnableAspectJAutoProxy + @Aspect(Bean)
Spring AOP在运行时,通过解析@Aspect修饰的Bean,生成Advisor,并使用JDK Proxy及CGLIB生成代理类
-
@EnableLoadTimeWeaving + AspectJWeaver
Spring AOP在运行时,通过AspectJWeaver直接修改目标字节码
-
AspectJ Directly
AspectJ同时支持Complie-Time Weaving、Load-Time Weaving、Run-Time Weaving
除以上三种方式之外,Spring中还存在哪些方法实现AOP?
注册Advisor
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的实现
使用ProxyFactory
利用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的实现
小结
-
@EnableAspectJAutoProxy + @Aspect(Bean)
Spring AOP在运行时,通过解析@Aspect修饰的Bean,生成Advisor,并使用JDK Proxy及CGLIB生成代理类
-
@EnableAspectJAutoProxy + Advisor(Bean)
直接注册Advisor类型的Bean,并通过JDK Proxy及CGLIB生成代理类
-
BeanPostProcessor + ProxyFacory
通过BeanPostProcessor#postProcessAfterInitialization,使用ProxyFactory直接生成代理类,不依赖@EnableAspectJAutoProxy
-
@EnableLoadTimeWeaving + AspectJWeaver
Spring AOP在运行时,通过AspectJWeaver直接修改目标字节码
-
AspectJ Directly
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...
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。