修炼内功springframework-6-Spring-AOP的其他实现方式

14次阅读

共计 11308 个字符,预计需要花费 29 分钟才能阅读完成。

本文已收录【修炼内功】跃迁之路

在 Spring AOP 是如何代理的一文中介绍了 Spring AOP 的原理,了解到其通过 JDK Proxy 及 CGLIB 生成代理类来实现目标方法的切面 (织入),这里有一个比较重要的概念 – 织入(Weaving),本篇就来探讨

  • 什么是织入?
  • 织入有哪些类型以及实现手段?
  • Spring 分别是如何支持的?

什么是织入

织入 为英文 Weaving 的直译,可字面理解为将额外代码插入目标代码逻辑中,实现对目标逻辑的增强、监控等,将不同的关注点进行解耦

织入的手段有很多种,不同的手段也对应不同的织入时机

  • Compile – Time Weaving (CTW)

    编译期织入

    在源码编译阶段,修改 / 新增源码,生成预期内的字节码文件

    如 AspectJ、Lombok、MapStruct 等

    Lombok、MapStruct 等使用了 Pluggable Annotation Processing API 技术实现对目标代码的修改 / 新增

    AspectJ 则直接使用了 acj 编译器

  • Load – Time – Weaving (LTW)

    装载期织入

    在 Class 文件的装载期,对将要装载的字节码文件进行修改,生成新的字节码进行替换

    如 AspectJ、Java Instrumentation 等

    Java Instrumentation 只提供了字节码替换 / 重装载的能力,字节码文件的修改还需要借助外部框架,如 javassist、asm 等

    javassist 的使用可以参考 Introduction to Javassist

  • Run – Time – Weaving (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.jarjavaagent启动参数

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 中设置 aspectjWeavingAUTODETECTENABLED 时,则会触发 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.jarjavaagent启动参数

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 的实现方式

  1. @EnableAspectJAutoProxy + @Aspect(Bean)

    Spring AOP 在运行时,通过解析 @Aspect 修饰的 Bean,生成 Advisor,并使用 JDK Proxy 及 CGLIB 生成代理类

  2. @EnableLoadTimeWeaving + AspectJWeaver

    Spring AOP 在运行时,通过 AspectJWeaver 直接修改目标字节码

  3. 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 的实现

小结

  1. @EnableAspectJAutoProxy + @Aspect(Bean)

    Spring AOP 在运行时,通过解析 @Aspect 修饰的 Bean,生成 Advisor,并使用 JDK Proxy 及 CGLIB 生成代理类

  2. @EnableAspectJAutoProxy + Advisor(Bean)

    直接注册 Advisor 类型的 Bean,并通过 JDK Proxy 及 CGLIB 生成代理类

  3. BeanPostProcessor + ProxyFacory

    通过 BeanPostProcessor#postProcessAfterInitialization,使用 ProxyFactory 直接生成代理类, 不依赖 @EnableAspectJAutoProxy

  4. @EnableLoadTimeWeaving + AspectJWeaver

    Spring AOP 在运行时,通过 AspectJWeaver 直接修改目标字节码

  5. 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…


正文完
 0