【修煉內功】[spring-framework] [6] Spring AOP的其餘實現方式

本文已收錄 【修煉內功】躍遷之路

spring-framework.jpg

林中小舍.png

Spring AOP是如何代理的一文中介紹了Spring AOP的原理,瞭解到其經過JDK Proxy及CGLIB生成代理類來實現目標方法的切面(織入),這裏有一個比較重要的概念 - 織入(Weaving),本篇就來探討html

  • 什麼是織入?
  • 織入有哪些類型以及實現手段?
  • Spring分別是如何支持的?

什麼是織入

織入爲英文Weaving的直譯,可字面理解爲將額外代碼插入目標代碼邏輯中,實現對目標邏輯的加強、監控等,將不一樣的關注點進行解耦java

織入的手段有不少種,不一樣的手段也對應不一樣的織入時機git

  • Compile - Time Weaving (CTW)github

    編譯期織入web

    在源碼編譯階段,修改/新增源碼,生成預期內的字節碼文件spring

    如AspectJ、Lombok、MapStruct等express

    Lombok、MapStruct等使用了 Pluggable Annotation Processing API 技術實現對目標代碼的修改/新增

    AspectJ則直接使用了acj編譯器apache

  • Load - Time - Weaving (LTW)segmentfault

    裝載期織入tomcat

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


訂閱號

相關文章
相關標籤/搜索