【修炼内功】[spring-framework] [6] Spring AOP的其他实现方式

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

spring-framework.jpg

林中小舍.png

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对目标类/方法进行织入,以便动态获取目标方法运行过程中的各种状态信息及监控信息

arthas

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


订阅号

阅读 232

推荐阅读
林中小舍
用户专栏

工作中的坑点及经验

51 人关注
41 篇文章
专栏主页