SpringBoot利用AOP巧妙記錄操作日志


本篇要點

  • 簡要回顧SpringAOP的相關知識點:關鍵術語,通知類型,切入點表達式等等。
  • 介紹SpringBoot快速啟動測試AOP,巧妙打印日志信息。

簡單回顧SpringAOP的相關知識點

SpringAOP的相關的知識點包括源碼解析,我已經在之前的文章中詳細說明,如果對AOP的概念還不是特別清晰的話,推薦先閱讀這篇文章:SpringAOP+源碼解析,切就完事了

為了加深印象,這邊再做一個簡短的回顧:

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-aopaspectjweaver整合進去。

        <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的整合哦。

參考閱讀


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM