Spring AOP與AspectJ的對比及應用

2023-02-07 21:00:27

1 簡介

AOP,即面向切面程式設計是很常用的技術,特別是在Java Web開發中。而最流行的AOP框架分別是Spring AOP和AspectJ。

2 Spring AOP vs AspectJ

Spring AOP是基於Spring IoC實現的,它解決大部分常見的需求,但它並不是一個完整的AOP解決方案。對於非Spring容器管理的物件,它更沒有辦法了。而AspectJ旨在提供完整的AOP方案,因此也會更復雜。

2.1 織入方式

兩者織入方式有極大的不同,這也是它們的本質區別,它們實現代理的方式不同。

AspectJ是在執行前織入的,分為三類:

  • 編譯時織入
  • 編譯後織入
  • 載入時織入

因此需要AspectJ編譯器(ajc)的支援。

而Spring AOP是執行時織入的,主要使用了兩種技術:JDK動態代理和CGLIB代理。對於介面使用JDK Proxy,而繼承的使用CGLIB。

2.2 Joinpoints

因為織入方式的區別,兩者所支援的Joinpoint也是不同的。像final的方法和靜態方法,無法通過動態代理來改變,所以Spring AOP無法支援。但AspectJ是直接在執行前織入實際的程式碼,所以功能會強大很多。

Joinpoint Spring AOP Supported AspectJ Supported
Method Call No Yes
Method Execution Yes Yes
Constructor Call No Yes
Constructor Execution No Yes
Static initializer execution No Yes
Object initialization No Yes
Field reference No Yes
Field assignment No Yes
Handler execution No Yes
Advice execution No Yes

2.3 效能

編譯織入會比較執行時織入快很多,Spring AOP是使用代理模式在執行時才建立對應的代理類,效率沒有AspectJ高。

3 Spring Boot使用AspectJ

因為AspectJ比較強大,在專案中應用會更多,所以這裡只介紹它與Spring Boot的整合。

3.1 引入依賴

引入以下依賴,在Spring Boot基礎上加了Lombok和aspectj:

<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>${aspectj.version}</version>
  </dependency>
  <dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>${aspectj.version}</version>
  </dependency>
  <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>${lombok.version}</version>
    <scope>provided</scope>
  </dependency>
</dependencies>

3.2 被AOP的物件

為了驗證AOP的功能,我們新增一個TestController,它有一個處理Get請求的方法,同時會呼叫private的成員方法和靜態方法:

@RestController
@RequestMapping("/test")
@Slf4j
public class TestController {
    @GetMapping("/hello")
    public String hello() {
        log.info("------hello() start---");
        test();
        staticTest();
        log.info("------hello() end---");
        return "Hello, pkslow.";
    }

    private void test() {
        log.info("------test() start---");
        log.info("test");
        log.info("------test() end---");
    }

    private static void staticTest() {
        log.info("------staticTest() start---");
        log.info("staticTest");
        log.info("------staticTest() end---");
    }
}

3.3 設定Aspect

設定切面如下:

@Aspect
@Component
@Slf4j
//@EnableAspectJAutoProxy
public class ControllerAspect {

    @Pointcut("execution(* com.pkslow.springboot.controller..*.*(..))")
    private void testControllerPointcut() {

    }

    @Before("testControllerPointcut()")
    public void doBefore(JoinPoint joinPoint){
        log.info("------pkslow aop doBefore start------");
        String method = joinPoint.getSignature().getName();
        String declaringTypeName = joinPoint.getSignature().getDeclaringTypeName();
        log.info("Method: {}.{}" ,declaringTypeName, method);
        log.info("------pkslow aop doBefore end------");
    }

    @Around("testControllerPointcut()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("------pkslow aop doAround start------");
        long start = System.nanoTime();
        Object obj = joinPoint.proceed();
        long end = System.nanoTime();
        log.info("Execution Time: " + (end - start) + " ns");
        log.info("------pkslow aop doAround end------");

        return obj;
    }
}

@Pointcut定義哪些類和方法會被捕抓來代理,這裡設定的是controller下的所有方法。

@Before@Around則定義了一些處理邏輯。@Before是列印了方法名,而@Around是做了一個計時。

注意:是不需要設定@EnableAspectJAutoProxy的。

3.4 maven外掛

因為是需要編譯時織入程式碼,所以需要maven外掛的支援:https://github.com/mojohaus/aspectj-maven-plugin

設定好pom.xml檔案即可。

然後執行命令打包:

mvn clean package

這時會顯示一些織入資訊,大致如下:

[INFO] Join point 'method-execution(java.lang.String com.pkslow.springboot.controller.TestController.hello())' in Type 'com.pkslow.springboot.controller.TestController' (TestController.java:14) advised by around advice from 'com.pkslow.springboot.aop.ControllerAspect' (ControllerAspect.class(from ControllerAspect.java))
[INFO] Join point 'method-execution(java.lang.String com.pkslow.springboot.controller.TestController.hello())' in Type 'com.pkslow.springboot.controller.TestController' (TestController.java:14) advised by before advice from 'com.pkslow.springboot.aop.ControllerAspect' (ControllerAspect.class(from ControllerAspect.java))
[INFO] Join point 'method-execution(void com.pkslow.springboot.controller.TestController.test())' in Type 'com.pkslow.springboot.controller.TestController' (TestController.java:22) advised by around advice from 'com.pkslow.springboot.aop.ControllerAspect' (ControllerAspect.class(from ControllerAspect.java))
[INFO] Join point 'method-execution(void com.pkslow.springboot.controller.TestController.test())' in Type 'com.pkslow.springboot.controller.TestController' (TestController.java:22) advised by before advice from 'com.pkslow.springboot.aop.ControllerAspect' (ControllerAspect.class(from ControllerAspect.java))
[INFO] Join point 'method-execution(void com.pkslow.springboot.controller.TestController.staticTest())' in Type 'com.pkslow.springboot.controller.TestController' (TestController.java:28) advised by around advice from 'com.pkslow.springboot.aop.ControllerAspect' (ControllerAspect.class(from ControllerAspect.java))
[INFO] Join point 'method-execution(void com.pkslow.springboot.controller.TestController.staticTest())' in Type 'com.pkslow.springboot.controller.TestController' (TestController.java:28) advised by before advice from 'com.pkslow.springboot.aop.ControllerAspect' (ControllerAspect.class(from ControllerAspect.java))

看到以上資訊,說明成功織入了程式碼,具體可以檢視生成的class檔案。

可以看到有許多程式碼都不是我們寫的,而是織入生成。

3.5 執行及測試

編譯成功後,我們就執行程式碼。如果是通過IDEA來執行,則在執行前不需要再build了,因為已經通過maven build過了包。通過IDEA自帶的編譯器build,可能無法織入。或者選擇ajc作為編譯器。具體請參考:IDEA啟動Springboot但AOP失效

存取如下:

GET http://localhost:8080/test/hello

則紀錄檔如下,成功實現AOP功能:

3.6 一些遇到的錯誤

遇到錯誤:

ajc Syntax error, annotations are only available if source level is 1.5 or greater

需要設定外掛:

<complianceLevel>${java.version}</complianceLevel>
<source>${java.version}</source>
<target>${java.version}</target>

可能還會遇到無法識別Lombok的錯誤,設定如下則解決該問題:

<plugin>
  <groupId>org.codehaus.mojo</groupId>
  <artifactId>aspectj-maven-plugin</artifactId>
  <version>1.14.0</version>
  <configuration>
    <complianceLevel>${java.version}</complianceLevel>
    <source>${java.version}</source>
    <target>${java.version}</target>
    <proc>none</proc>
    <showWeaveInfo>true</showWeaveInfo>
    <forceAjcCompile>true</forceAjcCompile>
    <sources/>
    <weaveDirectories>
      <weaveDirectory>${project.build.directory}/classes</weaveDirectory>
    </weaveDirectories>
  </configuration>
  <executions>
    <execution>
      <goals>
        <goal>compile</goal>
      </goals>
    </execution>
  </executions>
</plugin>

4 總結

AOP場景應用特別多,還是需要掌握的。

程式碼請看GitHub: https://github.com/LarryDpk/pkslow-samples