共计 11308 个字符,预计需要花费 29 分钟才能阅读完成。
本文已收录【修炼内功】跃迁之路
在 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…