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中,連接點總是表示方法的執行。通俗的講,連接點即表示類裏面可以被增強的方法
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
所謂通知是指攔截到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中的內容無論切入點是否拋出異常,都將執行
Spring AOP用法詳解