一、aop介紹
(一)前言
一般的后端開發流程是縱向開發,就是controller(控制層)->service(業務層)->mapper(數據持久層),Spring采用動態代理技術可以在程序的運行過程中對每一層進行增強,也就是面向切面編程(Aspect Oriented Programming)。
(二)使用場景
(1)想要在每類業務處理的時候保存日志,包含業務處理的具體數據、具體時間、具體用戶;
(2)如果業務方法是異步的,可以監控業務方法是否報錯;
(3)可以實現數據庫事務;
(三)相關術語
1、Joinpoint(連接點)
類里面可以被增強的方法即是連接點;
2、Pointcut(切入點)
對連接點進行攔截的定義即是切入點。
切入點的實現方式有兩種:
(1)使用@Pointcut注解定義切入點;
(2)自定義注解作為連接點,對注解進行攔截;
3、Advice(通知)
攔截到切入點之后要做的事情
通知分為以下幾類:
(1)前置通知(@Before):目標方法執行之前執行;
(2)后置通知(@After):目標方法執行之后執行,無論連接點是否出現異常,都會執行;
(3)異常通知(@AfterThrowing):連接點出現異常后才會執行;
(4)返回通知(@AfterReturning):連接點成功執行后,執行返回通知方法,如果連接點方法出現異常,該通知不執行;
(5)環繞通知(@Around):以上四種通知可以通過環繞通知實現;
4、Aspect(切面)
切入點和通知的結合
5、Target(目標對象)
要增強的類
二、代碼實現
(一)準備工作
1、控制層(Controller)
package com.xiaobai.aroundtest.controller;import com.xiaobai.aroundtest.entity.Article;
import com.xiaobai.aroundtest.service.IAroundTestService;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;/*** @author wangtw* @date 2023/12/6 0:20* @description 切面測試控制器*/
@AllArgsConstructor
@RestController
public class AroundTestController {private final IAroundTestService aroundTestService;@PostMapping("aroundTest")public void aroundTest(@RequestParam String name, @RequestBody Article article) {aroundTestService.aroundTest(name, article);}
}
2、業務層(Service)
(1)業務層接口
package com.xiaobai.aroundtest.service;import com.xiaobai.aroundtest.annotation.AroundTest;
import com.xiaobai.aroundtest.entity.Article;/*** @author wangtw* @date 2023/12/6 0:39* @description 環繞通知測試服務類*/
public interface IAroundTestService {void aroundTest(String name, Article article);
}
(2)業務層實現類
package com.xiaobai.aroundtest.service.impl;import com.xiaobai.aroundtest.annotation.AroundTest;
import com.xiaobai.aroundtest.entity.Article;
import com.xiaobai.aroundtest.service.IAroundTestService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;/*** @author wangtw* @date 2023/12/6 0:42* @description*/
@Slf4j
@Service
public class AroundTestServiceImpl implements IAroundTestService {@AroundTest // 自定義注解@Overridepublic void aroundTest(String name, Article article) {log.info("保存文章");}
}
(二)使用@Pointcut定義切入點
1、前言
@Pointcut 可以使用 annotation、within、execution 等方式將 方法(method)、類(class)、接口(interface)、包(package) 等作為切入點,
以下代碼使用的是execution指定切入點。
2、execution語法
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
(1)修飾符匹配(modifier-pattern?)
(2)返回值匹配(ret-type-pattern)可以為*表示任何返回值,全路徑的類名等
(3)類路徑匹配(declaring-type-pattern?)
(4)方法名匹配(name-pattern)可以指定方法名 或者 代表所有, set 代表以set開頭的所有方法
(5)參數匹配((param-pattern))可以指定具體的參數類型,多個參數間用“,”隔開,各個參數也可以用“”來表示匹配任意類型的參數,如(String)表示匹配一個String參數的方法;(,String) 表示匹配有兩個參數的方法,第一個參數可以是任意類型,而第二個參數是String類型;可以用(…)表示零個或多個任意參數
(6)異常類型匹配(throws-pattern?)
其中后面跟著“?”的是可選項
3、定義切面
以下表達式指定的是com.xiaobai及其子包下service.impl的所有方法
execution(* com.xiaobai..*.service.impl..*.*(..))
切面類需要使用@Aspect指定,使用@Component注解將切面交由Spring容器管理,切面類代碼如下:
package com.xiaobai.aroundtest.aspect;import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;/*** @author wangtw* @date 2023/12/6 0:47* @description service切面*/
@Aspect
@Component
@Slf4j
public class TransactionalAspect {@Pointcut("execution(* com.xiaobai..*.service.impl..*.*(..))")private void pointCut() {}/*** 開啟事務(前置通知)*/@Before("pointCut()")public void beginTransaction(){try {log.info("開啟事務");}catch (Exception e){e.printStackTrace();}}/*** 提交事務(后置返回通知)*/@AfterReturning("pointCut()")public void commit(){try {log.info("提交事務");}catch (Exception e){e.printStackTrace();}}/*** 回滾事務(異常通知)*/@AfterThrowing("pointCut()")public void rollback(){try {log.info("回滾事務");}catch (Exception e){e.printStackTrace();}}/*** 釋放連接(后置通知)*/@After("pointCut()")public void release(){try {log.info("釋放連接");}catch (Exception e){e.printStackTrace();}}
}
(三)通過攔截注解的方式實現切面
1、自定義注解
(1)使用@Target指定目標元素,這個注解是用于攔截方法,使用ElementType.METHOD
(2)使用@Retention指定注解的生命周期: RetentionPolicy.RUNTIME 表示此注解被保存到class文件中,jvm加載class文件后,此注解仍存在;RetentionPolicy.SOURCE 表示此注解只會保留到源文件中,文件編譯成class文件后,此注解就會消失;RetentionPolicy.CLASS 表示此注解會被保存到class文件中,但jvm加載class文件后,此注解會被遺棄;
package com.xiaobai.aroundtest.annotation;import java.lang.annotation.*;/*** @author wangtw* @date 2023/12/6 0:45* @description*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AroundTest {/*** 實體類屬性名* @return*/String name() default "name";
}
2、定義切面
@Around 用于實現環繞通知,使用 @annotation(aroundTest) 指定需要攔截的注解,環繞通知需要攜帶 ProceedingJoinPoint 類型的參數。
可以使用 (MethodSignature) ProceedingJoinPoint.getSignature() 獲取方法參數,具體代碼如下:
// 獲取參數名稱
MethodSignature methodSignature = (MethodSignature) point.getSignature();
String[] parameterNames = methodSignature.getParameterNames();
使用 ProceedingJoinPoint.getArgs() 方法獲取參數值
// 獲取參數
Object[] args = point.getArgs();
切面代碼如下:
package com.xiaobai.aroundtest.aspect;import com.xiaobai.aroundtest.annotation.AroundTest;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Component;import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;/*** @author wangtw* @date 2023/12/6 0:47* @description*/
@Aspect
@Component
@Slf4j
public class AroundTestAspect {@Around("@annotation(aroundTest)")public Object around(ProceedingJoinPoint point, AroundTest aroundTest) {// 獲取參數名稱MethodSignature methodSignature = (MethodSignature) point.getSignature();String[] parameterNames = methodSignature.getParameterNames();// 獲取參數Object[] args = point.getArgs();for (int i = 0; i < parameterNames.length; i++) {if (parameterNames[i].equals(aroundTest.name())) {log.info("保存{}為{}的文章", parameterNames[i], args[i]);}}String info = "";for (Object arg : args) {if (arg instanceof String) {} else {// 獲取文章信息PropertyDescriptor[] propertyDescriptors = BeanUtils.getPropertyDescriptors(arg.getClass());for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {if (!String.class.isAssignableFrom(propertyDescriptor.getPropertyType())) {continue;}Method readMethod = propertyDescriptor.getReadMethod();if (readMethod == null) {continue;}Object propertyValue = null;try {propertyValue = readMethod.invoke(arg, null);} catch (IllegalAccessException e) {e.printStackTrace();} catch (InvocationTargetException e) {e.printStackTrace();}info = info.concat(propertyDescriptor.getName()).concat(":").concat(propertyValue == null ? "" : propertyValue.toString()).concat(",");}}}Object result = null;try {result = point.proceed(point.getArgs());} catch (Throwable e) {e.printStackTrace();log.error("方法異常,文章信息:{}", info);}log.info("修改成功, 文章信息:{}", info);return result;}
}
三、總結
1、Spring使用Cglib增強了service,注入到controller的service實際上是個代理對象
2、使用postman測試controller接口,輸出結果如下:
保存name為java核心技術的文章
開啟事務
保存文章
提交事務
釋放連接
修改成功, 文章信息:description:囊括了Java平臺標準版(JavaSE/J2SE)的全部基礎知識,提供了大量完整且具有實際意義的應用實例,詳細介紹了Java語言基礎知識、面向對象程序設計、接口與內部類、事件監聽器模型、swing圖形用戶界面程序設計、打包應用程序、異常處理、登錄與調試、泛型程序設計、集合框架、多線程等內容,name:java核心技術,
參考
SpringBoot+Vue全棧開發實戰 王松 清華大學出版社
@Retention注解詳解 自由的棉花
Spring AOP中@Pointcut切入點表達式使用介紹 Roc Lau
【Spring AOP】@Aspect結合案例詳解(一): @Pointcut使用@annotation + 五種通知Advice注解(已附源碼) 天罡gg
AspectJ 切面注解中五種通知注解:@Before、@After、@AfterReturning、@AfterThrowing、@Around 蒼鷹蛟龍
Spring——面向切面編程(AOP) 行者無疆_ty