AOP,即面向切面程式設計是很常用的技術,特別是在Java Web開發中。而最流行的AOP框架分別是Spring AOP和AspectJ。
Spring AOP是基於Spring IoC實現的,它解決大部分常見的需求,但它並不是一個完整的AOP解決方案。對於非Spring容器管理的物件,它更沒有辦法了。而AspectJ旨在提供完整的AOP方案,因此也會更復雜。
兩者織入方式有極大的不同,這也是它們的本質區別,它們實現代理的方式不同。
AspectJ是在執行前織入的,分為三類:
因此需要AspectJ編譯器(ajc)的支援。
而Spring AOP是執行時織入的,主要使用了兩種技術:JDK動態代理和CGLIB代理。對於介面使用JDK Proxy,而繼承的使用CGLIB。
因為織入方式的區別,兩者所支援的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 |
編譯織入會比較執行時織入快很多,Spring AOP是使用代理模式在執行時才建立對應的代理類,效率沒有AspectJ高。
因為AspectJ比較強大,在專案中應用會更多,所以這裡只介紹它與Spring Boot的整合。
引入以下依賴,在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>
為了驗證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---");
}
}
設定切面如下:
@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
的。
因為是需要編譯時織入程式碼,所以需要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檔案。
可以看到有許多程式碼都不是我們寫的,而是織入生成。
編譯成功後,我們就執行程式碼。如果是通過IDEA來執行,則在執行前不需要再build了,因為已經通過maven build過了包。通過IDEA自帶的編譯器build,可能無法織入。或者選擇ajc作為編譯器。具體請參考:IDEA啟動Springboot但AOP失效
存取如下:
GET http://localhost:8080/test/hello
則紀錄檔如下,成功實現AOP功能:
遇到錯誤:
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>
AOP場景應用特別多,還是需要掌握的。
程式碼請看GitHub: https://github.com/LarryDpk/pkslow-samples