Table of contents
1 Spring AOP 簡介
AOP 即係 aspect-oriented programming,係一個畀我地從其他角度(如業務角度、功能角度)加插執行代碼既一個設計。簡單啲講,Spring AOP 就係一個做法,可以方便我地做到某啲特別既操作,亦唔需要改到原來既 code,而呢啲功能可能係需要針對一堆相類似既 classes,例如係 service layer 既 classes(帶 @Service
),去做一啲微細又或者毀滅性既操作。
1.1 Spring AOP 可以進行既操作
- 喺一個 Spring bean 既 method 被 call 之前做啲野
- 決定 call 啲乜野、點樣 call
- 唔 call 原先個 method
- call 原先個 method,但 supply 唔同既 argument values
- 喺一個 Spring bean 既 method 被 call 之後做啲野
1.2 Spring AOP 既應用
1.2.1 Spring Framework 本身
Spring 既 declarative transaction management 就正正係應用左 Spring AOP,亦即係我地寫 JPA/Hibernate 果陣用到既 @Transactional
,佢既背後就係由 Spring AOP 實現。
The Spring Framework's declarative transaction management is made possible with Spring aspect-oriented programming (AOP).
The most important concepts to grasp with regard to the Spring Framework's declarative transaction support are that this support is enabled
via AOP proxies and that the transactional advice is driven by metadata (currently XML- or annotation-based).
1.2.2 自定義應用場景
而其他應用場景有:
應用場景 | 描述 |
---|
Logging | 截取 input 及 output,用 logger log 低,方便 debug 或者 troubleshoot。 |
數據校驗 | 對特定參數進行校驗。 |
Mocking | 取締原來既 output。 |
至於做 authentication,即係去 check 下個 HTTP request 來自既用戶登入左未、session 係咪 valid,我地就可以用 filters,例如 OncePerRequestFilter
。
2 動手使用 Spring AOP 做 logging
2.1 用 Spring AOP 做既原因
試想像下而家有一個 Spring web application,裡面有好多唔同既 service classes,如果我地想對佢地加 logging,去 log 所有 public
既 service methods 被 call 果陣既 arguments 同埋 return 返出去既 value。
我地與其逐個 service method 咁改,倒不如將 logging 既 code 放喺同一個位度,除左方便,咁亦都可以令我地個系統既設計變得 modular,因為 logging 既 code 寫埋一邊,而 service classes 裡面就只有 business logic,河水不犯井水。
2.2 Maven dependencies
Spring 本身都已經有引入 Spring AOP,因為 Spring AOP 係 Spring 既核心功能,好多其他功能都係基於 Spring AOP,例如 JPA 既 transactions,所以唔需要特登加 spring-boot-starter-aop
既 dependency。
1<?xml version="1.0" encoding="UTF-8"?>
2<project xmlns="http://maven.apache.org/POM/4.0.0"
3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5 <modelVersion>4.0.0</modelVersion>
6
7 <groupId>com.michael</groupId>
8 <artifactId>Spring-Boot-2-Playground</artifactId>
9 <version>1.0.0</version>
10 <packaging>jar</packaging>
11
12 <name>Spring-Boot-2-Playground</name>
13 <description>Spring Boot 2 Playground</description>
14
15 <parent>
16 <groupId>org.springframework.boot</groupId>
17 <artifactId>spring-boot-starter-parent</artifactId>
18 <version>2.7.15</version>
19 </parent>
20
21 <properties>
22 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
23 <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
24 <javac.source>11</javac.source>
25 <javac.target>11</javac.target>
26 <maven.compiler.target>11</maven.compiler.target>
27 <maven.compiler.source>11</maven.compiler.source>
28 </properties>
29
30 <dependencies>
31 <dependency>
32 <groupId>org.springframework.boot</groupId>
33 <artifactId>spring-boot-starter-web</artifactId>
34 </dependency>
35 <dependency>
36 <groupId>org.springframework.boot</groupId>
37 <artifactId>spring-boot-starter-aop</artifactId>
38 </dependency>
39
40 <dependency>
41 <groupId>org.springframework.boot</groupId>
42 <artifactId>spring-boot-devtools</artifactId>
43 </dependency>
44 </dependencies>
45
46 <build>
47 <plugins>
48 <plugin>
49 <groupId>org.springframework.boot</groupId>
50 <artifactId>spring-boot-maven-plugin</artifactId>
51 <configuration>
52 <mainClass>code.MainApplication</mainClass>
53 </configuration>
54 <executions>
55 <execution>
56 <goals>
57 <goal>repackage</goal>
58 </goals>
59 </execution>
60 </executions>
61 </plugin>
62 </plugins>
63 </build>
64</project>
2.3 寫 Java code
Project structure:
src/main/java
/code
/controller
/service
HomeService.java
ServiceAspect.java
MainApplication.java
2.3.1 Application 入口
MainApplication.java
:
1@SpringBootApplication
2public class MainApplication {
3
4 public static void main(String[] args) {
5 SpringApplication.run(MainApplication.class, args);
6 }
7}
2.3.2 Controller layer
HomeController.java
:
1@RestController
2public class HomeController {
3
4 @Autowired
5 HomeService homeService;
6
7 @GetMapping("/{name}")
8 public String greet(@PathVariable("name") String name) {
9 return homeService.greet(name);
10 }
11}
2.3.3 Service layer
HomeService.java
:
1@Service
2public class HomeService {
3
4 public String greet(String name) {
5 return "Hi, " + name;
6 }
7}
2.3.4 AOP @Aspect
component
ServiceAspect.java
:
1@Slf4j
2@Aspect
3@Component
4public class ServiceAspect {
5
6 @Around("execution(* code.service.*.*(..))")
7 public Object logging(ProceedingJoinPoint joinPoint) throws Throwable {
8
9 final List<Object> args = Arrays.asList(joinPoint.getArgs());
10 final MethodSignature method = (MethodSignature) joinPoint.getSignature();
11 final Class<?> returnType = method.getReturnType();
12 final String methodName = method.getMethod().getName();
13 final String className = method.getDeclaringType().getSimpleName();
14
15 log.info("{}.{} is called. Arguments: {}", className, methodName, args);
16
17 try {
18 final Object returnValue = joinPoint.proceed();
19
20 log.info("{}.{} returns [class: {}]: {}", className, methodName, returnType.getCanonicalName(), returnValue);
21
22 return returnValue;
23 } catch (Throwable e) {
24 log.info("{}.{} throws exception [class: {}].", className, methodName, e.getClass(), e);
25 throw e;
26 }
27 }
28}
2.4 測試
2.4.1 啟動 Spring Boot web application
執行下面既 command,或者喺 IDE 裡面 run MainApplication
既 main
method:
mvn spring-boot:run
2.4.2 Call API
執行下面既 command,或者喺 Postman 裡面 call GET localhost:8080/mick
:
curl --request GET "localhost:8080/mick"
2.4.3 檢視結果 + 討論
留意 Spring Boot 既 console log,我地會見到:
HomeService.greet is called. Arguments: [mick]
HomeService.greet returns [class: java.lang.String]: Hi, mick
呢個就係 demo 緊我地點樣喺 service layer 被用到既時候,用 Spring AOP 黎 log 低 input 同 output。
同樣地,我地都可以針對 controller layer 或者 JPA 既 repository layer 去做一啲處理。我地只需要修改 @Around("execution(* code.xxxxxx.*.*(..))")
既 package,就可以對呢個 package 裡面既所有 Spring components 做相同既處理。
如果想寫一個 method 但係針對多過一個 package,咁我地可以用 ||
operator,將幾個 execution()
駁埋一齊:
1@Around(
2 "execution(* code.controller.*.*(..))"
3 + " || execution(* code.service.*.*(..))"
4)
5public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
6 // ...
7}