基本使用如下:
首先需要匯入套件,我使用的是Spring Boot 的 AOP,套件管理使用Maven,請在pom.xml匯入以下套件並reload。
package proj.java.spring.aop.controller;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.*;import proj.java.spring.aop.TxInfo;import proj.java.spring.aop.service.TxService;@RestControllerpublic class TxController { @Autowired private TxService txService; @PostMapping("/Tx") public String createTx(@RequestBody TxInfo txInfo) { return txService.createTx(txInfo); } @GetMapping("/Tx") public TxInfo getTx(@RequestParam("txnNo") String txnNo) { return txService.getTx(txnNo); } @PutMapping("/Tx") public String updateTx(@RequestBody TxInfo txInfo) { return txService.updateTx(txInfo); } @DeleteMapping("/Tx") public String deleteTx(@RequestParam("txnNo") String txnNo) { return txService.deleteTx(txnNo); }}Service層因為是範例,所以使用Map充當DB。
package proj.java.spring.aop.service;import lombok.extern.slf4j.Slf4j;import org.springframework.stereotype.Component;import proj.java.spring.aop.TxInfo;import java.util.HashMap;import java.util.List;@Component@Slf4jpublic class TxService { private HashMap
package proj.java.spring.aop;import lombok.Data;@Datapublic class TxInfo { private int id; private String txnNo; private String consumer; private String goods; private int amount;}以上是四個簡單的API,接下來我要介紹AOP的核心部分了。
package proj.java.spring.aop;import lombok.extern.slf4j.Slf4j;import org.aspectj.lang.JoinPoint;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.*;import org.springframework.stereotype.Component;import org.springframework.util.StringUtils;import org.springframework.web.context.request.RequestContextHolder;import org.springframework.web.context.request.ServletRequestAttributes;import javax.servlet.http.HttpServletRequest;@Component@Aspect@Slf4jpublic class LoggerAop { // 定義 ThreadLocal 變數來存儲開始時間 private ThreadLocal
接著在下方有著三個@Pointcut,Pointcut就是切入點,也就是說實際上程式你想要在哪一個位置做攔截。@Pointcut使用了Pointcut表示法,有關寫法可以參考良葛格的文章(Pointcut 表示式 (openhome.cc))簡單說明一下這三個Pointcut的意思,由上至下分別是
對TxController內的所有方法套用切入點。對TxController的getTx方法套用切入點。對 符合條件1 但不符合條件2的套用切入點,也就是說Txcontroller中getTx()以外的所有方法。OK,現在你知道怎麼寫Pointcut切入點了,但你實際上要做甚麼呢?
@Before(value = "pointcutController()") public void roleVerify(JoinPoint point) { startTime.set(System.currentTimeMillis()); HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); String role = request.getHeader("role"); // 抓取Header if (StringUtils.isEmpty(role)) { throw new IDNotCorrectException("不明確的來源訪問"); } }以上是一個範例,@Before代表在方法執行之前執行,他套用了 1切入點, 如果請求的header沒有包含角色,就拋出錯誤。此外,如果在@Before就拋出錯誤的話,程式就不會繼續做下去了。
@After("pointcutController()") public void logResponse() { // 紀錄 Response log.info("執行結束"); } @AfterThrowing(pointcut = "pointcutController()",throwing = "ex") public void logErrResponse(JoinPoint point,Throwable ex) { // 紀錄 Response HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); String role = request.getHeader("role"); // 抓取Header log.info("發生異常,使用者:" + role + ", Message = "+ex.getMessage()); } @AfterReturning(pointcut = "pointcutController()", returning = "response") public void logReturnResponse(Object response) { // 紀錄 Response log.info("執行成功結束,結果 = " + response); long endTime = System.currentTimeMillis(); long elapsedTime = endTime - startTime.get(); log.info("執行結束,消耗時間:" + elapsedTime + " 毫秒"); // 清除 ThreadLocal 變數,避免內存洩漏 startTime.remove(); }@After意思是在切入點執行後執行,上方的範例是只要沒有在Before被攔下來,就會印出執行結束字樣。@AfterReturning與@After很相似,只不過一個代表是唯有成功return才會進入,以及不管service層內有沒有拋出錯誤都會執行。這邊的範例是,如果成功執行就印出response,另外寫花費時間的log。@AfterThrowing這個是指,如果在service內的程式中拋出錯誤,可以在此AOP內部做一些處理或紀錄。沒有錯誤就不會進入。
基本的應用就是以上了,有了以上的基礎,你就可以說瞭解了基本AOP的運作了。也不是很困難。
@Around("pointcutController()") public Object aroundAdvice(ProceedingJoinPoint pjp) { Object[] params = pjp.getArgs(); int id = Integer.parseInt(String.valueOf(pjp.getArgs()[0])); String name = (String) pjp.getArgs()[1]; int height = Integer.parseInt(String.valueOf(pjp.getArgs()[2])); String methodName = pjp.getSignature().getName(); Object proceed = null; try {// @Before log.info("Request User ID = " + id + " name = " + name); if (id <= 0) { throw new IDNotCorrectException("ID start from zero"); } //目標方法invoke proceed = pjp.proceed(params); log.info("Response = "+proceed); } catch (Throwable e) { // @AfterThrowing log.info("Excpetion Happen! message = "+ e.getMessage()); if(e instanceof IDNotCorrectException) proceed = "ID設定不正確"; } finally { // @After log.info("Call API Complete"); } // @AfterReturning return proceed; }@Around 是一個綜合大成者,可以理解為它涵蓋了所有Before、After、AfterThrowing、AfterReturning等方法,是一個全面性的功能。
你問我那為甚麼不全部用@Around?當然是因為耦合度太高啊。為了提高重複利用性,功能設計得簡潔才是比較好的做法。ProceedingJoinPoint是Spring AOP提供的類別,他把實際請求封裝在裡面,所以要取出來做判斷。
以上,整理一下內容:
@Aspect Aspect指的是一個切面,透過宣告在類別上來讓Spring Boot 知道這是一個切面。
@Pointcut() Pointcut 指的是一個切入點,透過AOP的execution表達式,你可以設定程式中你想要設定的切入點的路徑。
@Before Before 指的是一個執行時機,當你定義切入點的Before,代表你會讓這個功能在切入點執行之前被執行。
@After After 也代表一個執行時機,當你定義切入點的After時,代表你會讓這個功能在切入點執行之後被執行。
@AfterReturning 與 @AfterThrowing 比較特殊的執行時機。我們可以在Before中定義例外處理,若我們在Before時就拋出例外的話,切入點就不會執行,AfterReturning也不會被執行。取而代之的是,AfterThrowing會被執行。
另外,無論有沒有拋出例外,@After 都會被執行
最後還有一個 @Around 事件,它的功能是綜合以上所有的執行時機處理,是一個相當強大的AOP事件。
參考來源:https://www.edureka.co/blog/spring-aop-tutorial/