博客 / 詳情

返回

聊聊那些年我們實現java AOP幾種常見套路

前言

有一定開發經驗的同學對AOP應該很瞭解吧,如果不瞭解,可以先查看如下文章進行科普一下https://baike.baidu.com/item/AOP/1332219?fr=aladdin,再來閲讀本文。

示例前置準備

注: 本示例基於springboot進行演示

1、在項目pom引入aop的GAV
   <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
2、編寫業務服務
@Service
public class EchoService {

    @CostTimeRecoder
    public void echo(String message){
        System.out.println("echo ->" + message);
    }

}
3、編寫aspect切面
@Aspect
public class EchoAspect {

    @Before(value = "execution(* com.github.lybgeek.aop.service.EchoService.echo(..))")
    public void before(JoinPoint joinPoint){
        System.out.println("USE AOP BY ASPECT WITH ARGS: " + Arrays.toString(joinPoint.getArgs()));

    }
}

實現AOP的常見套路

1、在編譯期階段實現AOP

方法一:通過aspectj-maven-plugin插件在編譯期進行織入

在項目的pom引入如下內容

<build>
  <plugins>
    <plugin>
      <artifactId>maven-surefire-plugin</artifactId>
      <version>2.22.2</version>
    </plugin>
    <plugin>
      <groupId>com.nickwongdev</groupId>
      <artifactId>aspectj-maven-plugin</artifactId>
      <version>1.12.6</version>
      <configuration>
        <complianceLevel>${java.version}</complianceLevel>
        <source>${java.version}</source>
        <target>${java.version}</target>
        <encoding>${project.encoding}</encoding>
      </configuration>
      <executions>
        <execution>
          <goals>
            <goal>compile</goal>
          </goals>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>
<dependencies>
  <dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>1.9.5</version>
  </dependency>
</dependencies>

通過執行如下maven命令 ,進行項目編譯

mvn clean compile

執行測試類

public class AspectMavenPluginMainTest {

    public static void main(String[] args) {
      EchoService.echo("aspectMavenPlugin");
    }
}

bac6193ef0ac413a30efdc0a922ec929_5901e9a363cea836423e8f6c49b40e55.png

發現切面已經執行。我們在查看下生成的EchoService.class文件有沒有發生什麼變化

public class EchoService {
    public EchoService() {
    }

    public static final void echo(String message) {
        JoinPoint var1 = Factory.makeJP(ajc$tjp_0, (Object)null, (Object)null, message);
        EchoAspect.aspectOf().before(var1);
        System.out.println("echo ->" + message);
    }

    static {
        ajc$preClinit();
    }
}

發現多了一些切面的內容。

注: 本示例利用別人重新封裝的插件,而非Codehaus的官方提供的插件,Codehaus的官方提供的插件只能支持JDK8(包含JDK8)以下的版本,而本示例的插件可以支持到JDK13

本示例的插件github地址:https://github.com/nickwongdev/aspectj-maven-plugin

Codehaus的官方插件地址:https://github.com/mojohaus/aspectj-maven-plugin
以及相應介紹:https://www.mojohaus.org/aspectj-maven-plugin/index.html

方法二:利用APT + JavaPoet 在編譯期實現切面邏輯

如果對於APT不瞭解的小夥伴,可以查看我之前的文章聊聊如何運用JAVA註解處理器(APT)

而JavaPoet是JavaPoet 是生成 .java 源文件的 Java API,具體查看官方文檔
https://github.com/square/javapoet
或者查看此博文
https://weilu.blog.csdn.net/article/details/112429217

不過JavaPoet 只能生產新的代碼,無法對原有的代碼進行修改。因此在演示此方法時,本文就通過生成一個繼承EchoService的子類,來實現AOP功能

生成的子類如下

public final class LybGeekEchoServiceCostTimeRecord extends EchoService {
    public LybGeekEchoServiceCostTimeRecord() {
    }

    public final void echo(String message) {
        long startTime = System.currentTimeMillis();
        super.echo(message);
        long costTime = System.currentTimeMillis() - startTime;
        System.out.println("costTime : " + costTime + "ms");
    }
    }

注: 因為JavaPoet 是通過生成新代碼,而非進行在源代碼進行插樁,因此也不是很符合我們我要求

方法三:利用APT+AST在編譯期進行織入

AST抽象語法樹,可以在編譯期對字節碼進行修改,達到插樁的效果。因之前我有寫過一篇文章
聊聊如何通過APT+AST來實現AOP功能

本示例就不貼相應的代碼了

2、在JVM進行類加載時進行AOP

核心是用利用aspectjweaver在JVM進行類加載時進行織入。具體實現步驟如下

1、在項目的POM引入aspectjweaver GAV
<dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.5</version>
        </dependency>
2、創建切面類和需要被織入的目標類

即示例前置準備的內容

3、在src/main/resource目錄下創建META-INF/aop.xml文件
<aspectj>
    <weaver options="-XnoInline -Xset:weaveJavaxPackages=true -Xlint:ignore -verbose -XmessageHandlerClass:org.springframework.aop.aspectj.AspectJWeaverMessageHandler">
        <!--在編織時導入切面類和需要被切入的目標類-->
        <include within="com.github.lybgeek.aop.aspect.EchoAspect"/>
        <include within="com.github.lybgeek.aop.service.EchoService"/>
    </weaver>
    <aspects>
        <!--指定切面類-->
        <aspect name="com.github.lybgeek.aop.aspect.EchoAspect"/>
    </aspects>
</aspectj>
4、指定VM參數
-javaagent:aspectjweaver.jar的路徑
示例:
-javaagent:D:\repository\org\aspectj\aspectjweaver\1.9.5\aspectjweaver-1.9.5.jar
5、測試
public class AspectjweaverMainTest {

    public static void main(String[] args) {
        EchoService echoService = new EchoService();
        echoService.echo("Aspectjweaver");
    }
}

bdabe24aef9485b90be3d37f956c00d5_4aa2886af875783e98ef271085314864.png

查看控制枱

4d3f84f673ec05cbad77af762bdb0464_da305205202cea0c672f7ad7f9f269f3.png

3、在運行時進行AOP

我們以spring aop為例

1、手動代理(直接使用底層API)

主要是利用AspectJProxyFactory 、ProxyFactoryBean 、ProxyFactory

public class AopApiTest {

    @Test
    public void testAopByAspectJProxyFactory(){
        AspectJProxyFactory aspectJProxyFactory = new AspectJProxyFactory(new EchoService());
        aspectJProxyFactory.addAspect(EchoAspect.class);
        EchoService echoService = aspectJProxyFactory.getProxy();
        echoService.echo("AspectJProxyFactory");
    }

    @Test
    public void testAopByProxyFactoryBean(){
        ProxyFactoryBean proxyFactoryBean = new ProxyFactoryBean();
        proxyFactoryBean.setTarget(new EchoService());

        AspectJExpressionPointcutAdvisor aspectJExpressionPointcutAdvisor = new AspectJExpressionPointcutAdvisor();
        aspectJExpressionPointcutAdvisor.setExpression("execution(* com.github.lybgeek.aop.service.EchoService.echo(..))");
        aspectJExpressionPointcutAdvisor.setAdvice((MethodBeforeAdvice) (method, args, target) -> System.out.println("USE AOP BY ASPECT WITH ARGS: " + Arrays.toString(args)));
        proxyFactoryBean.addAdvisor(aspectJExpressionPointcutAdvisor);

        EchoService echoService = (EchoService) proxyFactoryBean.getObject();
        echoService.echo("ProxyFactoryBean");

    }

    @Test
    public void testAopByProxyFactory(){
        ProxyFactory proxyFactory = new ProxyFactory();
        proxyFactory.setTarget(new EchoService());

        AspectJExpressionPointcutAdvisor aspectJExpressionPointcutAdvisor = new AspectJExpressionPointcutAdvisor();
        aspectJExpressionPointcutAdvisor.setExpression("execution(* com.github.lybgeek.aop.service.EchoService.echo(..))");
        aspectJExpressionPointcutAdvisor.setAdvice((MethodBeforeAdvice) (method, args, target) -> System.out.println("USE AOP BY ASPECT WITH ARGS: " + Arrays.toString(args)));
        proxyFactory.addAdvisor(aspectJExpressionPointcutAdvisor);

        EchoService echoService = (EchoService) proxyFactory.getProxy();
        echoService.echo("ProxyFactory");



    }

2、自動代理

這個是我們平時用得最多的。自動代理常見實現手段就是在spring bean ioc階段的後置處理器階段進行增強

示例

@Configuration
public class AopConfig {

    @Bean
    public EchoAspect echoAspect(){
        return new EchoAspect();
    }


}

因為自動代理太常見了,java開發必備技能,就不多做介紹了

總結

本文主要從編譯期,JVM加載器期、運行期這三個環節,來講述如何進行AOP。如果對性能有強烈要求的話,推薦在編譯期或者JVM加載期進行織入。如果想對方法修飾符為final、static、private進行織入,也可以考慮在編譯期進行實現。不過在編譯期或者JVM加載期進行織入有個弊端就是,出現問題不好排查。如果不是對性能有極致要求的話,推薦在運行時,進行AOP進行切入,主要是出現問題,相對好排查。有時候基於業務角度而非技術角度,進行權衡,可能會得出意想不到的效果

demo鏈接

https://github.com/lyb-geek/springboot-learning/tree/master/springboot-aop

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.