SpringBoot AOP的使用
AOP:面向切面程式設計,相對於OOP面向物件程式設計 Spring的AOP的存在目的是為了解耦。AOP可以讓一組類共享相同的行為。在OOP中只能繼承和實現介面,且類繼承只能單繼承,阻礙更多行為新增到一組類上,AOP彌補了OOP的不足。
還有就是為了清晰的邏輯,讓業務邏輯關注業務本身,不用去關心其它的事情,比如事務。
Spring的AOP是通過JDK的動態代理和CGLIB實現的。
一、AOP的術語:
aop 有一堆術語,非常難以理解,簡單說一下
-
通知(有的地方叫增強)(Advice)
需要完成的工作叫做通知,就是你寫的業務邏輯中需要比如事務、日誌等先定義好,然後需要的地方再去用
-
連線點(Join point)
就是spring中允許使用通知的地方,基本上每個方法前後拋異常時都可以是連線點
-
切點(Poincut)
其實就是篩選出的連線點,一個類中的所有方法都是連線點,但又不全需要,會篩選出某些作為連線點做為切點。如果說通知定義了切面的動作或者執行時機的話,切點則定義了執行的地點
-
切面(Aspect)
其實就是通知和切點的結合,通知和切點共同定義了切面的全部內容,它是幹什麼的,什麼時候在哪執行
-
引入(Introduction)
在不改變一個現有類程式碼的情況下,為該類新增屬性和方法,可以在無需修改現有類的前提下,讓它們具有新的行為和狀態。其實就是把切面(也就是新方法屬性:通知定義的)用到目標類中去
-
目標(target)
被通知的物件。也就是需要加入額外程式碼的物件,也就是真正的業務邏輯被組織織入切面。
-
織入(Weaving)
把切面加入程式程式碼的過程。切面在指定的連線點被織入到目標物件中,在目標物件的生命週期裡有多個點可以進行織入:
- 編譯期:切面在目標類編譯時被織入,這種方式需要特殊的編譯器
- 類載入期:切面在目標類載入到JVM時被織入,這種方式需要特殊的類載入器,它可以在目標類被引入應用之前增強該目標類的位元組碼
- 執行期:切面在應用執行的某個時刻被織入,一般情況下,在織入切面時,AOP容器會為目標物件動態建立一個代理物件,Spring AOP就是以這種方式織入切面的。
例:
public class UserService{ void save(){} List list(){} .... } 複製程式碼
在UserService中的save()方法前需要開啟事務,在方法後關閉事務,在拋異常時回滾事務。
那麼,UserService中的所有方法都是連線點(JoinPoint),save()方法就是切點(Poincut)。需要在save()方法前後執行的方法就是通知(Advice),切點和通知合起來就是一個切面(Aspect)。save()方法就是目標(target)。把想要執行的程式碼動態的加入到save()方法前後就是織入(Weaving)。
有的地方把通知稱作增強是有道理的,在業務方法前後加上其它方法,其實就是對該方法的增強。
二、常用AOP通知(增強)型別
- before(前置通知):在方法開始執行前執行
- after(後置通知):在方法執行後執行
- afterReturning(返回後通知):在方法返回後執行
- afterThrowing(異常通知): 在丟擲異常時執行
- around(環繞通知):在方法執行前和執行後都會執行
三、執行順序
around > before > around > after > afterReturning
四、先說一下SpringAop非常霸道又用的非常少的功能 --引入(Introduction)
- 配置類
@Aspect @Component public class IntroductionAop { @DeclareParents(value = "com.jiuxian..service..*", defaultImpl = DoSthServiceImpl.class) public DoSthService doSthService; } 複製程式碼
- service程式碼
public interface DoSthService { void doSth(); } @Service public class DoSthServiceImpl implements DoSthService { @Override public void doSth() { System.out.println("do sth ...."); } } public interface UserService { void testIntroduction(); } @Service public class UserServiceImpl implements UserService { @Override public void testIntroduction() { System.out.println("do testIntroduction"); } } 複製程式碼
- 測試程式碼
@Test public void testIntroduction() { userService.testIntroduction(); //Aop 讓UserService方法擁有 DoSthService的方法 DoSthService doSthService = (DoSthService) userService; doSthService.doSth(); } 複製程式碼
- 結果
do testIntroduction do sth .... 複製程式碼
五、五種通知(增強)程式碼實現
- 配置類
(1) 對方法
@Aspect @Component public class TransactionAop { @Pointcut("execution(* com.jiuxian..service.*.*(..))") public void pointcut() { } @Before("pointcut()") public void beginTransaction() { System.out.println("before beginTransaction"); } @After("pointcut()") public void commit() { System.out.println("after commit"); } @AfterReturning("pointcut()", returning = "returnObject") public void afterReturning(JoinPoint joinPoint, Object returnObject) { System.out.println("afterReturning"); } @AfterThrowing("pointcut()") public void afterThrowing() { System.out.println("afterThrowing afterThrowingrollback"); } @Around("pointcut()") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { try { System.out.println("around"); return joinPoint.proceed(); } catch (Throwable e) { e.printStackTrace(); throw e; } finally { System.out.println("around"); } } } 複製程式碼
(2) 對註解
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Log { String value() default ""; } 複製程式碼
@Aspect @Component public class AnnotationAop { @Pointcut(value = "@annotation(log)", argNames = "log") public void pointcut(Log log) { } @Around(value = "pointcut(log)", argNames = "joinPoint,log") public Object around(ProceedingJoinPoint joinPoint, Log log) throws Throwable { try { System.out.println(log.value()); System.out.println("around"); return joinPoint.proceed(); } catch (Throwable throwable) { throw throwable; } finally { System.out.println("around"); } } } 複製程式碼
- service 方法實現
public interface UserService { String save(String user); void testAnnotationAop(); } @Service public class UserServiceImpl implements UserService { @Override public String save(String user) { System.out.println("儲存使用者資訊"); if ("a".equals(user)) { throw new RuntimeException(); } return user; } @Log(value = "test") @Override public void testAnnotationAop() { System.out.println("testAnnotationAop"); } } 複製程式碼
- 測試類
@RunWith(SpringRunner.class) @SpringBootTest public class SpringbootAopApplicationTests { @Resource private UserService userService; @Test public void testAop1() { userService.save("張三"); Assert.assertTrue(true); } @Test public void testAop2() { userService.save("a"); } @Test public void testAop3() { userService.testAnnotationAop(); } } 複製程式碼
- 結果
- 執行testAop1時
around before beginTransaction 儲存使用者資訊 around after commit afterReturning :: 張三 複製程式碼
- 執行testAop2時
around before beginTransaction 儲存使用者資訊 around after commit afterThrowingrollback 複製程式碼
- 執行testAop3時
test around testAnnotationAop around 複製程式碼
- pom檔案
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> 複製程式碼
六、最常用的execution解釋
例: execution(* com.jiuxian..service.*.*(..))
- execution 表示式的主體
- 第一個* 代表任意的返回值
- com.jiuxian aop所橫切的包名
- 包後面.. 表示當前包及其子包
- 第二個* 表示類名,代表所有類
- .*(..) 表示任何方法,括號代表引數 .. 表示任意引數
例: execution(* com.jiuxian..service.*Service.add*(String))
表示: com.jiuxian 包及其子包下的service包下,類名以Service結尾,方法以add開頭,引數型別為String的方法的切點。
七、特別的用法
@Pointcut("execution(public * *(..))") private void anyPublicOperation() {} @Pointcut("within(com.xyz.someapp.trading..*)") private void inTrading() {} @Pointcut("anyPublicOperation() && inTrading()") private void tradingOperation() {} 複製程式碼
可以使用 &&, ||, ! 運算子來定義切點