SpringBoot利用AOP巧妙記錄操作日誌
本篇要點
- 簡要回顧SpringAOP的相關知識點:關鍵術語,通知型別,切入點表示式等等。
- 介紹SpringBoot快速啟動測試AOP,巧妙列印日誌資訊。
簡單回顧SpringAOP的相關知識點
SpringAOP的相關的知識點包括原始碼解析,我已經在之前的文章中詳細說明,如果對AOP的概念還不是特別清晰的話,推薦先閱讀這篇文章:
為了加深印象,這邊再做一個簡短的回顧:
1、AOP關鍵術語
-
切面(Aspect):也就是我們定義的專注於提供輔助功能的模組,比如安全管理,日誌資訊等。
-
連線點(JoinPoint):切面程式碼可以通過連線點切入到正常業務之中,圖中每個方法的每個點都是連線點。
-
切入點(PointCut):一個切面不需要通知所有的連線點,而在連線點的基礎之上增加切入的規則,選擇需要增強的點,最終真正通知的點就是切入點。
-
通知方法(Advice):就是切面需要執行的工作,主要有五種通知:before,after,afterReturning,afterThrowing,around。
-
織入(Weaving):將切面應用到目標物件並建立代理物件的過程,SpringAOP選擇再目標物件的執行期動態建立代理對
-
引入(introduction):在不修改程式碼的前提下,引入可以在執行期為類動態地新增方法或欄位。
2、通知的五種型別
- 前置通知Before:目標方法呼叫之前執行的通知。
- 後置通知After:目標方法完成之後,無論如何都會執行的通知。
- 返回通知AfterReturning:目標方法成功之後呼叫的通知。
- 異常通知AfterThrowing:目標方法丟擲異常之後呼叫的通知。
- 環繞通知Around:可以看作前面四種通知的綜合。
3、切入點表示式
上面提到:連線點增加切入規則就相當於定義了切入點,當然切入點表示式分為很多種,這裡主要學習execution和annotation表示式。
execution
- 寫法:execution(訪問修飾符 返回值 包名.包名……類名.方法名(引數列表))
- 例:
execution(public void com.smday.service.impl.AccountServiceImpl.saveAccount())
- 訪問修飾符可以省略,返回值可以使用萬用字元*匹配。
- 包名也可以使用
*
匹配,數量代表包的層級,當前包可以使用..
標識,例如* *..AccountServiceImpl.saveAccount()
- 類名和方法名也都可以使用
*
匹配:* *..*.*()
- 引數列表使用
..
可以標識有無引數均可,且引數可為任意型別。
全通配寫法:
* *…*.*(…)
通常情況下,切入點應當設定再業務層實現類下的所有方法:* com.smday.service.impl.*.*(..)
。
@annotation
匹配連線點被它引數指定的Annotation註解的方法。也就是說,所有被指定註解標註的方法都將匹配。
@annotation(com.hyh.annotation.Log)
:指定Log註解方法的連線點。
4、AOP應用場景
- 記錄日誌
- 監控效能
- 許可權控制
- 事務管理
快速開始
引入依賴
如果你使用的是SpringBoot,那麼只需要引入:spring-boot-starter-aop
,框架已經將spring-aop
和aspectjweaver
整合進去。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
定義日誌資訊封裝
/**
* Controller層的日誌封裝
* @author Summerday
*/
@Data
@ToString
public class WebLog implements Serializable {
private static final long serialVersionUID = 1L;
// 操作描述
private String description;
// 操作時間
private Long startTime;
// 消耗時間
private Integer timeCost;
// URL
private String url;
// URI
private String uri;
// 請求型別
private String httpMethod;
// IP地址
private String ipAddress;
// 請求引數
private Object params;
// 請求返回的結果
private Object result;
// 操作型別
private String methodType;
}
自定義註解@Log
@Target({ElementType.PARAMETER,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {
/**
* 描述
*/
String description() default "";
/**
* 方法型別 INSERT DELETE UPDATE OTHER
*/
MethodType methodType() default MethodType.OTHER;
}
定義測試介面
@RestController
public class HelloController {
@PostMapping("/hello")
@Log(description = "hello post",methodType = MethodType.INSERT)
public String hello(@RequestBody User user) {
return "hello";
}
@GetMapping("/hello")
@Log(description = "hello get")
public String hello(@RequestParam("name") String username, String hobby) {
int a = 1 / 0;
return "hello";
}
}
定義切面Aspect與切點Pointcut
用@Aspect註解標註標識切面,用@PointCut定義切點。
/**
* 定義切面
* @author Summerday
*/
@Aspect
@Component
public class LogAspect {
private static final Logger log = LoggerFactory.getLogger(LogAspect.class);
/**
* web層切點
* 1. @Pointcut("execution(public * com.hyh.web.*.*(..))") web層的所有方法
* 2. @Pointcut("@annotation(com.hyh.annotation.Log)") Log註解標註的方法
*/
@Pointcut("@annotation(com.hyh.annotation.Log)")
public void webLog() {
}
}
定義通知方法Advice
這裡使用環繞通知,
/**
* 定義切面
* @author Summerday
*/
@Aspect
@Component
public class LogAspect {
private static final Logger log = LoggerFactory.getLogger(LogAspect.class);
/**
* web層切點
* 1. @Pointcut("execution(public * com.hyh.web.*.*(..))") web層的所有方法
* 2. @Pointcut("@annotation(com.hyh.annotation.Log)") Log註解標註的方法
*/
@Pointcut("@annotation(com.hyh.annotation.Log)")
public void webLog() {
}
/**
* 環繞通知
*/
@Around("webLog()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
//獲取請求物件
HttpServletRequest request = getRequest();
WebLog webLog = new WebLog();
Object result = null;
try {
log.info("=================前置通知=====================");
long start = System.currentTimeMillis();
result = joinPoint.proceed();
log.info("=================返回通知=====================");
long timeCost = System.currentTimeMillis() - start;
// 獲取Log註解
Log logAnnotation = getAnnotation(joinPoint);
// 封裝webLog物件
webLog.setMethodType(logAnnotation.methodType().name());
webLog.setDescription(logAnnotation.description());
webLog.setTimeCost((int) timeCost);
webLog.setStartTime(start);
webLog.setIpAddress(request.getRemoteAddr());
webLog.setHttpMethod(request.getMethod());
webLog.setParams(getParams(joinPoint));
webLog.setResult(result);
webLog.setUri(request.getRequestURI());
webLog.setUrl(request.getRequestURL().toString());
log.info("{}", JSONUtil.parse(webLog));
} catch (Throwable e) {
log.info("==================異常通知=====================");
log.error(e.getMessage());
throw new Throwable(e);
}finally {
log.info("=================後置通知=====================");
}
return result;
}
/**
* 獲取方法上的註解
*/
private Log getAnnotation(ProceedingJoinPoint joinPoint) {
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
return method.getAnnotation(Log.class);
}
/**
* 獲取引數 params:{"name":"天喬巴夏"}
*/
private Object getParams(ProceedingJoinPoint joinPoint) {
// 引數名
String[] paramNames = getMethodSignature(joinPoint).getParameterNames();
// 引數值
Object[] paramValues = joinPoint.getArgs();
// 儲存引數
Map<String, Object> params = new LinkedHashMap<>();
for (int i = 0; i < paramNames.length; i++) {
Object value = paramValues[i];
// MultipartFile物件以檔名作為引數值
if (value instanceof MultipartFile) {
MultipartFile file = (MultipartFile) value;
value = file.getOriginalFilename();
}
params.put(paramNames[i], value);
}
return params;
}
private MethodSignature getMethodSignature(ProceedingJoinPoint joinPoint) {
return (MethodSignature) joinPoint.getSignature();
}
private HttpServletRequest getRequest() {
ServletRequestAttributes requestAttributes =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
return requestAttributes.getRequest();
}
}
這裡處理webLog的方式有很多種,考慮效能,可以採用非同步方式存入資料庫,相應程式碼已經上傳至Gitee。
測試
POST http://localhost:8081/hello
Content-Type: application/json
{ "id" : 1, "username" : "天喬巴夏", "age": 18 }
結果如下:
=================前置通知=====================
=================返回通知=====================
{"ipAddress":"127.0.0.1","description":"hello post","httpMethod":"POST","params":{"user":{"id":1,"age":18,"username":"天喬巴夏"}},"uri":"/hello","url":"http://localhost:8081/hello","result":"hello","methodType":"INSERT","startTime":1605596028383,"timeCost":28}
=================後置通知=====================
原始碼下載
本文內容均為對優秀部落格及官方文件總結而得,原文地址均已在文中參考閱讀處標註。最後,文中的程式碼樣例已經全部上傳至Gitee:https://gitee.com/tqbx/springboot-samples-learn,另有其他SpringBoot的整合哦。