要是說 AOP 這個概念在哪裡發光發熱,我第一個想到的就是 Spring,在處理繁瑣重複的 Web 服務時實在太需要 AOP 的輔助來降低複雜度了,所以我們今天會把討論了好久的樹狀搜尋問題搬到 Spring 上,借助工具展示 AOP 功能有多強大。
今天會有比較多環境設定的部分,可以直接跳到實作章節
<!-- AOP 工具包 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- Web 工具包 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 前面使用過的 arrow-kt,強化錯誤處理 -->
<dependency>
<groupId>io.arrow-kt</groupId>
<artifactId>arrow-core</artifactId>
<version>1.2.4</version>
</dependency>
<dependency>
<groupId>io.arrow-kt</groupId>
<artifactId>arrow-fx-coroutines</artifactId>
<version>1.2.4</version>
</dependency>
<!-- 使 spring 支援 kotlin coroutine -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-reactor</artifactId>
<version>1.6.0</version>
</dependency>
將 D15 - 樹狀搜尋問題 非同步版 Kotlin 篇 的內容包裝成 Spring Service
package com.example.demo
import arrow.core.*
import arrow.fx.coroutines.parMap
import org.springframework.stereotype.Service
data class Node(
val id: String,
val text: String,
val childrenIds: List<String>,
)
class NodeNotFoundError(
val id: String,
) : Exception()
@Service
class NodeService {
private suspend fun getNode(id:String): Either<NodeNotFoundError, Node> {
return when (id) {
"a" -> Node("a", "a", childrenIds = listOf("a1", "a2", "a33")).right()
"a1" -> Node("a1", "a1", childrenIds = listOf("a11")).right()
"a11" -> Node("a11", "a11", childrenIds = listOf()).right()
"a2" -> Node("a2", "a2", childrenIds = listOf()).right()
"a3" -> Node("a3", "a3", childrenIds = listOf()).right()
else -> NodeNotFoundError(id).left()
}
}
suspend fun getNameById(
id: String,
target: String,
): String =
getNode(id)
.fold(
ifRight = { result ->
if (result.id == target) {
result.text
} else {
result.childrenIds
.parMap { getNameById(it, target) }
.find { it == target }
}
},
ifLeft = { null },
).toOption().getOrElse { "not found" }
}
建立一個控制器,讓我們能透過 HTTP GET 請求觸發 getNameById 服務。
package com.example.demo
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
@RestController
class NodeController {
private val nodeService = NodeService()
@GetMapping("getNode")
suspend fun getNode(
@RequestParam("id")
id: String,
@RequestParam("target")
target: String
): String {
return nodeService.getNameById(id,target)
}
}
實際建立一個切面類別,利用 @Before
ˋ指定切入時機、利用 "execution(* com.example.demo.*.*(..))"
指定切入點是 demo package 中的所有 method
package com.example.demo
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.Before
import org.springframework.stereotype.Component
@Component
@Aspect
class LogAspect {
@Before("execution(* com.example.demo.*.*(..))")
fun logBeforeMethod() {
println("A method is being called...")
}
}
有了 Spring 的完整支援在整個 AOP 中,我們唯一所需要學習的就是,如何選擇要切入時機與切入點
切入時機可以選擇的不多,包含以下這些 Declaring Advice
切入點就複雜多了,建議參考這篇文章 Declaring a Pointcut