去年年底終於幫公司的專案升級成 Spring Boot,下一個 wish list 就是把一些討厭的邏輯轉成 AOP 的方式插入,然後就可以去除掉一些討厭的關聯了。不過因為自己實際上還沒有真的玩過 AOP,所以這篇就紀錄一點基本的東西。
AOP
AOP 的主要概念就是橫向切入,或者換句話說是用外掛的方式,把本來不屬於程式碼的邏輯 “外掛” 到程式碼當中。這種作法最主要的優點就是,外掛上去的邏輯可以放在別的地方,不會直接跟主邏輯綁在一起,因此也不會影響到主邏輯自身的程式碼。舉例來說,原本的程式碼可能是 A -> B.method()
,表示 class A 的某一行去呼叫 class B 的 method()
方法。套上 AOP 時,在編譯期間,程式碼的呼叫會被編譯成 A -> aop.method() -> B.method()
,也就是說 A 跟 B 的呼叫過程被額外加入了 AOP 的邏輯。
外掛是怎麼做到的,就我目前的理解是有兩種作法,一種是直接用 code generation 的方式,在程式碼編譯期間把外掛的程式碼包進去。所以雖然原始碼中是寫 A 呼叫 B,但編譯後產生的中繼碼,實際上寫的是 A 呼叫 AOP、然後 AOP 呼叫 B。第二種作法是 runtime proxy,就是透過 reflection 的方式把 aop 掛到 A 和 B 之間。兩種作法的目的是一樣的,但第一種作法顯然執行效率會比較高,因為第二種作法是執行期間才動態做 proxy。依據 Spring AOP 的文件,第一種作法是在 Spring AOP 裡直接使用 AspectJ 的話,AspectJ 會使用的作法;而第二種作法則是 Spring AOP 預設採取的作法。
Spring AOP
Spring AOP 算是簡易版的 AspectJ,功能比較受限,例如 Spring AOP 只能做 method 的攔截,其他 field 攔截等等的是不支援的。詳細的說明可以參考 Spring AOP Capabilities and Goals。
不過整體來說,最重要的一件事是,Spring AOP 只能夠對 Spring 管理的 bean 做攔截,這點很重要~。所以如果發現 AOP 一直沒發生效果,第一個先確認想要攔截的對象是不是被 Spring 管理的 bean。
Spring AOP 簡易範例
題外話,話說我在實驗時,有先叫 ChatGPT 產生範例程式碼給我,但結果它產生的程式碼其實有不少細節是錯的….。看起來 ChatGPT 把 Spring AOP 跟 AspectJ 混在一起使用的樣子…。
回歸正題 XD。這裡用一個很簡單的範例先展示 Spring AOP 的用法。首先需要引入 AOP 相關的 dependency,在 Spring Boot 可以簡單地放入 spring-boot-starter-aop
就好。
implementation("org.springframework.boot:spring-boot-starter-aop")
這個範例裡,我建立了一個 CommandLineRunner
,作為 Spring Boot 的啟動對象。這個 Runner 裡面會隨機產生 10 個 Event,然後將 Event 餵給 FakeEventProcessor
,所以我最後要被 AOP 插入的對象是 FakeEventProcessor
。需要注意的是,這裡 FakeEventProcessor
是用注入的方式產生的,而不是自己呼叫 new
,因為上面提到了 Spring AOP 只能對 Spring 管理的 bean 發生效用。
@Slf4j
@Component
public class Run implements CommandLineRunner {
@Autowired
private FakeEventProcessor processor;
@Override
public void run(String... args) throws Exception {
int loop = 10;
while(loop-- > 0) {
var event = generateRandomEvent();
processor.receiveEvent(event);
}
}
}
@Builder
@Getter
@Accessors(fluent = true)
@ToString
public class Event {
private String eventType;
private String owner;
}
@Slf4j
@Component
public class FakeEventProcessor {
public void receiveEvent(Event event) {
log.info("Receive: {}", event);
}
}
接著宣告一個 Aspect,目的是我想要攔截 FakeEventProcessor
每次呼叫 receiveEvent()
的時候,都在它被呼叫前先呼叫我要插入的邏輯。
@Slf4j
@Aspect
@Component
public class TelemetryAspect {
public TelemetryAspect() {
log.info("Initiate aspect...");
}
@Before("execution(* tw.jimwayneyeh.example.otel.FakeEventProcessor.receiveEvent(..))")
public void before(JoinPoint joinPoint) {
Event event = (Event) joinPoint.getArgs()[0];
log.trace("join '{}()': {}", joinPoint.getSignature().getName(), event);
}
}
這裡我使用的攔截方式是 @Before
,表示在目標被執行前先執行攔截的邏輯,並且攔截的對象是 @Before
裡定義的 method tw.jimwayneyeh.example.otel.FakeEventProcessor.receiveEvent(..)
。而攔截後,我要拿出傳入的 event 並且印出 log,這樣我才能確認它攔截到的是什麼東西。
最後這段程式碼跑起來,會跑出以下的結果:
TRACE t.j.e.o.TelemetryAspect [main] join 'receiveEvent()': Event(eventType=delete, owner=owner-0)
INFO t.j.e.o.FakeEventProcessor [main] Receive: Event(eventType=delete, owner=owner-0)
TRACE t.j.e.o.TelemetryAspect [main] join 'receiveEvent()': Event(eventType=update, owner=owner-1)
INFO t.j.e.o.FakeEventProcessor [main] Receive: Event(eventType=update, owner=owner-1)
TRACE t.j.e.o.TelemetryAspect [main] join 'receiveEvent()': Event(eventType=update, owner=owner-6)
INFO t.j.e.o.FakeEventProcessor [main] Receive: Event(eventType=update, owner=owner-6)
...(ignored)
這裡只列了最開始三行,因為後面都差不多 XD。可以看到針對每個 event 的呼叫,攔截的 log 會先出現(join 開頭的 log),然後才出現原本 method 執行的寫 log(Receive 開頭的 log),並且兩個 log 印出來的 event 是完全一樣的,表示 Aspect 確實被插入到 method call 之前了。
沒有留言:
張貼留言