1.AOP基本概念
切面(Aspect):通知(advice)和切入點(pointcut)共同組成了切面(aspect)
切入點(Pointcut):匹配join point的謂詞,切面切向哪裏,某個類或者某一層包路徑
連接點(Joinpoint):aop攔截的類或者方法,例如方法被調用時、異常被拋出時。(在Spring中,所有的方法都可以認爲是joinpoint,但是我們不希望所有的方法都添加Advice,而pointcut的作用就是提供一組規則來匹配joinpoint,給滿足規則的joinpoint添加Advice。)
通知(Advice):切面何時使用,即注有@Around、@Before、@After等註解的方法
目標對象(Target Object):被通知的對象
AOP代理(AOP Proxy): AOP的代理有兩種,一種是JDK動態代理,一種是CGLIB代理。默認情況下,TargetObject實現了接口時,則採用JDK動態代理;反之,採用CGLIB代理
Joinpoint和Pointcut區別:在Spring AOP中,所有的方法執行都是joinpoint,而pointcut是一個描述信息,它修飾的是joinpoint,通過pointcut可以確定哪些jointpoint可以被Advice。
2.通知(Advice)類型說明
@Around:環繞通知,包圍一個連接點的通知,可以在覈心方法前後完成自定義的行爲。這是最常用最重要的。這個註解需要傳入參數ProceedingJoinPoint pjp,決定是否進入核心方法----調用pjp.proceed();如果不調的話將不進入核心方法!
@Before:前通知,核心代碼執行前通知
@After:後通知,連接點執行退出時通知(不論正常返回還是異常退出)
@AfterReturning:返回後通知,正常返回後通知
@AfterThrowing:拋出異常後通知,拋出異常時通知
注意:除了@Around傳的參數是ProceedingJoinPoint pjp外,其它都是傳的JoinPoint jp,也就是說能控制是否進入核心代碼的只有Around,因爲aop走到核心代碼就是通過調用ProceedingJoinPoint的proceed()方法,而JoinPoint沒有這個方法。
3.Advice順序:
情況一:一個方法只被一個Aspect類攔截
添加PointCut類 添加Aspect類 這兩個可以放在一起
package com.bob.hello.aspect;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
/**
* Description
*
* @author Bob
* @date 2020/6/2
**/
@Component
@Aspect
public class DemoAspect {
/**
* @description 攔截com.bob.hello包下所有方法
* @author Bob
* @date 2020/6/2
*/
@Pointcut(value = "within(com.bob.hello.controller.*)")
public void demoPointCut() {
}
@Before(value = "demoPointCut()")
public void before(JoinPoint joinPoint) {
System.out.println("[DemoAspect] before advice");
}
@Around(value = "demoPointCut()")
public void around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.println("[DemoAspect] around advice 1");
proceedingJoinPoint.proceed();
System.out.println("[DemoAspect] around advice 2");
}
@After(value = "demoPointCut()")
public void after(JoinPoint joinPoint) {
System.out.println("[DemoAspect] after advice");
}
@AfterThrowing(value = "demoPointCut()")
public void afterThrowing(JoinPoint joinPoint) {
System.out.println("[DemoAspect] afterThrowing advice");
}
@AfterReturning(value = "demoPointCut()")
public void afterReturning(JoinPoint joinPoint) {
System.out.println("[DemoAspect] afterReturning advice");
}
}
添加測試用的Controller
package com.bob.hello.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Description
*
* @author Bob
* @date 2020/5/26
**/
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello(){
System.out.println("hello");
return "hello";
}
}
測試 正常情況
瀏覽器直接輸入:http://localhost:8080/hello
輸出的結果:
[DemoAspect] around advice 1
[DemoAspect] before advice
hello
[DemoAspect] around advice 2
[DemoAspect] after advice
[DemoAspect] afterReturning advice
注意:如果用到了Spring Security,會先走登錄攔截
結論
在一個方法只被一個aspect類攔截時,aspect類內部的 advice 將按照以下的順序進行執行:
情況二:同一個方法被多個Aspect類攔截
aspect1和aspect2的執行順序是未知的,除非設置他們的Order順序,具體參見:Spring AOP @Before @Around @After 等 advice 的執行順序
4.定義Aspect(切面)
當使用@Aspect標註一個bean後,Spring框架會自動收集這些bean,並添加到Spring AOP中,例如:
@Component
@Aspect
public class DemoAspect {
}
注意:僅僅使用@Aspect 註解並不能將一個Java對象轉換爲bean,因此還需要類似@Component 之類的註解。
注意:如果一個類被@Aspect 標註,則這個類就不能是其它aspect的**advised object** 了,因爲使用@Aspect後,這個類就會別排除在auto-proxying機制之外。
5.聲明Pointcut(切入點)
在@AspectJ 風格的AOP中,這樣來描述pointcut,例如:
@Pointcut(value = "within(com.bob.hello.controller.*)")
public void demoPointCut() {
}
value裏面的值叫pointcut(切點)表達式,它由標識符和操作參數組成,within就是標識符,()裏的就是操作參數。
常見的切點表達式
匹配方法簽名
// 匹配指定包中的所有的方法
execution(* com.xys.service.*(..))
// 匹配當前包中的指定類的所有方法
execution(* UserService.*(..))
// 匹配指定包中的所有 public 方法
execution(public * com.xys.service.*(..))
// 匹配指定包中的所有 public 方法, 並且返回值是 int 類型的方法
execution(public int com.xys.service.*(..))
// 匹配指定包中的所有 public 方法, 並且第一個參數是 String, 返回值是 int 類型的方法
execution(public int com.xys.service.*(String name, ..))
匹配類型簽名
// 匹配指定包中的所有的方法, 但不包括子包
within(com.xys.service.*)
// 匹配指定包中的所有的方法, 包括子包
within(com.xys.service..*)
// 匹配當前包中的指定類中的方法
within(UserService)
// 匹配一個接口的所有實現類中的實現的方法
within(UserDao+)
匹配Bean名字
// 匹配以指定名字結尾的 Bean 中的所有方法
bean(*Service)
切點表達式組合
// 匹配以 Service 或 ServiceImpl 結尾的 bean
bean(*Service || *ServiceImpl)
// 匹配名字以 Service 開頭, 並且在包 com.xys.service 中的 bean
bean(*Service) && within(com.xys.service.*)
6.聲明Advice(通知)
示例中的@Before @Around等
7.Spring AOP實戰
HTTP接口鑑權
核心代碼:
@Component
@Aspect
public class HttpAopAdviseDefine {
// 定義一個 Pointcut, 使用 切點表達式函數 來描述對哪些 Join point 使用 advise.
@Pointcut("@annotation(com.xys.demo1.AuthChecker)")
public void pointcut() {
}
// 定義 advise
@Around("pointcut()")
public Object checkAuth(ProceedingJoinPoint joinPoint) throws Throwable {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
.getRequest();
// 檢查用戶所傳遞的 token 是否合法
String token = getUserToken(request);
if (!token.equalsIgnoreCase("123456")) {
return "錯誤, 權限不合法!";
}
return joinPoint.proceed();
}
private String getUserToken(HttpServletRequest request) {
Cookie[] cookies = request.getCookies();
if (cookies == null) {
return "";
}
for (Cookie cookie : cookies) {
if (cookie.getName().equalsIgnoreCase("user_token")) {
return cookie.getValue();
}
}
return "";
}
}
方法調用日誌
核心代碼:
@Component
@Aspect
public class LogAopAdviseDefine {
private Logger logger = LoggerFactory.getLogger(getClass());
// 定義一個 Pointcut, 使用 切點表達式函數 來描述對哪些 Join point 使用 advise.
@Pointcut("within(NeedLogService)")
public void pointcut() {
}
// 定義 advise
@Before("pointcut()")
public void logMethodInvokeParam(JoinPoint joinPoint) {
logger.info("---Before method {} invoke, param: {}---", joinPoint.getSignature().toShortString(), joinPoint.getArgs());
}
@AfterReturning(pointcut = "pointcut()", returning = "retVal")
public void logMethodInvokeResult(JoinPoint joinPoint, Object retVal) {
logger.info("---After method {} invoke, result: {}---", joinPoint.getSignature().toShortString(), joinPoint.getArgs());
}
@AfterThrowing(pointcut = "pointcut()", throwing = "exception")
public void logMethodInvokeException(JoinPoint joinPoint, Exception exception) {
logger.info("---method {} invoke exception: {}---", joinPoint.getSignature().toShortString(), exception.getMessage());
}
}
方法耗時統計
核心代碼:
@Component
@Aspect
public class ExpiredAopAdviseDefine {
private Logger logger = LoggerFactory.getLogger(getClass());
// 定義一個 Pointcut, 使用 切點表達式函數 來描述對哪些 Join point 使用 advise.
@Pointcut("within(SomeService)")
public void pointcut() {
}
// 定義 advise
// 定義 advise
@Around("pointcut()")
public Object methodInvokeExpiredTime(ProceedingJoinPoint pjp) throws Throwable {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
// 開始
Object retVal = pjp.proceed();
stopWatch.stop();
// 結束
// 上報到公司監控平臺
reportToMonitorSystem(pjp.getSignature().toShortString(), stopWatch.getTotalTimeMillis());
return retVal;
}
public void reportToMonitorSystem(String methodName, long expiredTime) {
logger.info("---method {} invoked, expired time: {} ms---", methodName, expiredTime);
//
}
}
參考:
Spring AOP @Before @Around @After 等 advice 的執行順序
spring aop的@Before,@Around,@After,@AfterReturn,@AfterThrowing的理解