Spring AOP用法詳解
什麼是AOP
AOP:Aspect Oriented Programming,中文翻譯為”面向切面程式設計“。面向切面程式設計是一種程式設計正規化,它作為OOP面向物件程式設計的一種補充,用於處理系統中分佈於各個模組的橫切關注點,比如事務管理、許可權控制、快取控制、日誌列印等等。AOP採取橫向抽取機制,取代了傳統縱向繼承體系的重複性程式碼
AOP把軟體的功能模組分為兩個部分: 核心關注點 和 橫切關注點 。業務處理的主要功能為核心關注點,而非核心、需要拓展的功能為橫切關注點。AOP的作用在於分離系統中的各種關注點,將核心關注點和橫切關注點進行分離
使用AOP有諸多好處,如:
1.集中處理某一關注點/橫切邏輯
2.可以很方便的新增/刪除關注點
3.侵入性少,增強程式碼可讀性及可維護性
AOP的術語
1.Join point(連線點)
Spring 官方文件的描述:
A point during the execution of a program, such as the execution of a method or the handling of an exception. In Spring AOP, a join point always represents a method execution.
程式執行過程中的一個點,如方法的執行或異常的處理。在Spring AOP中,連線點總是表示方法的執行。通俗的講,連線點即表示類裡面可以被增強的方法
2.Pointcut(切入點)
Pointcut are expressions that is matched with join points to determine whether advice needs to be executed or not. Pointcut uses different kinds of expressions that are matched with the join points and Spring framework uses the AspectJ pointcut expression language
切入點是與連線點匹配的表示式,用於確定是否需要執行通知。切入點使用與連線點匹配的不同型別的表示式,Spring框架使用AspectJ切入點表示式語言。我們可以將切入點理解為需要被攔截的Join point
3.Advice(增強/通知)
所謂通知是指攔截到Joinpoint之後所要做的事情就是通知,通知分為前置通知、後置通知、異常通知、最終通知和環繞通知(切面要完成的功能)
4.Aspect(切面)
Aspect切面表示Pointcut(切入點)和Advice(增強/通知)的結合
Spring AOP用法
示例程式碼
/** * 設定登入使用者名稱 */ public class CurrentUserHolder { private static final ThreadLocal<String> holder = new ThreadLocal<>(); public static String get() { return holder.get(); } public static void set(String user) { holder.set(user); } } /** * 校驗使用者許可權 */ @Service("authService") public class AuthServiceImpl implements AuthService { @Override public void checkAccess() { String user = CurrentUserHolder.get(); if(!"admin".equals(user)) { throw new RuntimeException("該使用者無此許可權!"); } } } /** * 業務邏輯類 */ @Service("productService") public class ProductServiceImpl implements ProductService { @Autowired private AuthService authService; @Override public Long deleteProductById(Long id) { System.out.println("刪除商品id為" + id + "的商品成功!"); return id; } @Override public void deleteProductByName(String name) { System.out.println("刪除商品名稱為" + name + "的商品成功!"); } @Override public void selectProduct(Long id) { if("100".equals(id.toString())) { System.out.println("查詢商品成功!"); } else { System.out.println("查詢商品失敗!"); throw new RuntimeException("該商品不存在!"); } } }
1.使用within表示式匹配包型別
//匹配ProductServiceImpl類裡面的所有方法 @Pointcut("within(com.aop.service.impl.ProductServiceImpl)") public void matchType() {} //匹配com.aop.service包及其子包下所有類的方法 @Pointcut("within(com.aop.service..*)") public void matchPackage() {}
2.使用this、target、bean表示式匹配物件型別
//匹配AOP物件的目標物件為指定型別的方法,即ProductServiceImpl的aop代理物件的方法 @Pointcut("this(com.aop.service.impl.ProductServiceImpl)") public void matchThis() {} //匹配實現ProductService介面的目標物件 @Pointcut("target(com.aop.service.ProductService)") public void matchTarget() {} //匹配所有以Service結尾的bean裡面的方法 @Pointcut("bean(*Service)") public void matchBean() {}
3.使用args表示式匹配引數
//匹配第一個引數為Long型別的方法 @Pointcut("args(Long, ..) ") public void matchArgs() {}
4.使用@annotation、@within、@target、@args匹配註解
//匹配標註有AdminOnly註解的方法 @Pointcut("@annotation(com.aop.annotation.AdminOnly)") public void matchAnno() {} //匹配標註有Beta的類底下的方法,要求annotation的Retention級別為CLASS @Pointcut("@within(com.google.common.annotations.Beta)") public void matchWithin() {} //匹配標註有Repository的類底下的方法,要求annotation的Retention級別為RUNTIME @Pointcut("@target(org.springframework.stereotype.Repository)") public void matchTarget() {} //匹配傳入的引數類標註有Repository註解的方法 @Pointcut("@args(org.springframework.stereotype.Repository)") public void matchArgs() {}
5.使用execution表示式
execution表示式是我們在開發過程中最常用的,它的語法如下:

modifier-pattern:用於匹配public、private等訪問修飾符
ret-type-pattern:用於匹配返回值型別,不可省略
declaring-type-pattern:用於匹配包型別
modifier-pattern(param-pattern):用於匹配類中的方法,不可省略
throws-pattern:用於匹配丟擲異常的方法
程式碼示例:
@Component @Aspect public class SecurityAspect { @Autowired private AuthService authService; //匹配com.aop.service.impl.ProductServiceImpl類下的方法名以delete開頭、引數型別為Long的public方法 @Pointcut("execution(public * com.aop.service.impl.ProductServiceImpl.delete*(Long))") public void matchCondition() {} //使用matchCondition這個切入點進行增強 @Before("matchCondition()") public void before() { System.out.println("before 前置通知......"); authService.checkAccess(); } }
單元測試:
@RunWith(SpringRunner.class) @SpringBootTest public class SpringBootApplicationTests { @Autowired private ProductService productService; @Test public void contextLoads() { //設定使用者名稱 CurrentUserHolder.set("hello"); productService.selectProduct(100L); productService.deleteProductByName("衣服"); productService.deleteProductById(100L); } }
執行結果(只有deleteProductById方法攔截成功):
查詢商品成功! 刪除商品名稱為衣服的商品成功! before 前置通知...... java.lang.RuntimeException: 該使用者無此許可權! at com.aop.service.impl.AuthServiceImpl.checkAccess(AuthServiceImpl.java:15) at com.aop.security.SecurityAspect.before(SecurityAspect.java:50)
可以在多個表示式之間使用連線符匹配多個條件, 如使用||表示“或”,使用 &&表示“且”
//匹配com.aop.service.impl.ProductServiceImpl類下方法名以select或delete開頭的所有方法 @Pointcut("execution(* com.aop.service.impl.ProductServiceImpl.select*(..)) || " + "execution(* com.aop.service.impl.ProductServiceImpl.delete*(..))") public void matchCondition() {} //使用matchCondition這個切入點進行增強 @Before("matchCondition()") public void before() { System.out.println("before 前置通知......"); authService.checkAccess(); }
單元測試:
@Test public void contextLoads() { CurrentUserHolder.set("admin"); productService.selectProduct(100L); productService.deleteProductByName("衣服"); productService.deleteProductById(100L); }
執行結果(所有方法均攔截成功):
before 前置通知...... 查詢商品成功! before 前置通知...... 刪除商品名稱為衣服的商品成功! before 前置通知...... 刪除商品id為100的商品成功!
6.Advice註解
Advice註解一共有五種,分別是:
1.@Before前置通知
前置通知在切入點執行前執行,不會影響切入點的邏輯
2.@After後置通知
後置通知在切入點正常執行結束後執行,如果切入點丟擲異常,則在丟擲異常前執行
3.@AfterThrowing異常通知
異常通知在切入點丟擲異常前執行,如果切入點正常執行(未丟擲異常),則不執行
4.@AfterReturning返回通知
返回通知在切入點正常執行結束後執行,如果切入點丟擲異常,則不執行
5.@Around環繞通知
環繞通知是功能最強大的通知,可以在切入點執行前後自定義一些操作。環繞通知需要負責決定是繼續處理join point(呼叫ProceedingJoinPoint的proceed方法)還是中斷執行
示例程式碼:
//匹配com.aop.service.impl.ProductServiceImpl類下面的所有方法 @Pointcut("execution(* com.aop.service.impl.ProductServiceImpl.*(..))") public void matchAll() {} @Around("matchAll()") public Object around(ProceedingJoinPoint joinPoint) { Object result = null; authService.checkAccess(); System.out.println("befor 在切入點執行前執行"); try{ result = joinPoint.proceed(joinPoint.getArgs());//獲取引數 System.out.println("after 在切入點執行後執行,result = " + result); } catch (Throwable e) { System.out.println("after 在切入點執行後丟擲exception執行"); e.printStackTrace(); } finally { System.out.println("finally......"); } return result; }
單元測試:
@Test public void contextLoads() { CurrentUserHolder.set("admin"); productService.deleteProductById(100L); productService.selectProduct(10L); }
執行結果:
before 在切入點執行前執行 刪除商品id為100的商品成功! after 在切入點執行後執行,result = 100 finally...... before 在切入點執行前執行 查詢商品失敗! after 在切入點執行後丟擲exception執行 java.lang.RuntimeException: 該商品不存在! at com.aop.service.impl.ProductServiceImpl.selectProduct(ProductServiceImpl.java:41) at com.aop.service.impl.ProductServiceImpl$$FastClassBySpringCGLIB$$f17a76a2.invoke(<generated>) finally......
在執行ProceedingJoinPoint物件的proceed方法前相當於Before前置通知;執行proceed方法相當於執行切入點(同時可以獲取引數);在方法執行之後相當於After後置通知,如果執行切入點丟擲異常,則catch中的內容相當於AfterThrowing異常通知;finally中的內容無論切入點是否丟擲異常,都將執行