1. 程式人生 > >【Spring】每個程式設計師都使用Spring(四)——Aop+自定義註解做日誌攔截

【Spring】每個程式設計師都使用Spring(四)——Aop+自定義註解做日誌攔截

一、前言

      上一篇部落格向大家介紹了Aop的概念,對切面=切點+通知 、連線點、織入、目標物件、代理(jdk動態代理和CGLIB代理)有所瞭解了。理論很強,實用就在這篇部落格介紹。

      這篇部落格中,小編向大家介紹springAop很常見的使用方式——日誌攔截

二、實戰

2.1 全域性觀說明


這裡寫圖片描述

      說明:

      假如service出錯了,這樣錯誤會丟擲到controller,controller捕捉到後,丟擲自定義異常。然後@ControllerAdvice + @ExceptionHandler 全域性處理 Controller 層異常,捕獲controller丟擲的異常。在這個方法中為AOP的連線點,會觸發AOP的通知方法。通知方法捕獲request和response,打印出詳細的錯誤日誌資訊。

2.2 建立springboot專案 引入相關依賴

主要新增springmvc和springaop的依賴。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion
>
4.0.0</modelVersion> <groupId>com.wl</groupId> <artifactId>sbDemo</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>sbDemo</name> <description>Demo project for Spring Boot</description
>
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.10.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!--aop--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <!--springmvc--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>21.0</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.0</version> <scope>provided</scope> </dependency> <!--swagger2--> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.2.2</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.2.2</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>5.0.7.RELEASE</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>

2.3 建立 切面類

      使用@Aspect 和 @Component兩個註解,表示是切面類,並且可以被spring管理。

      在切面中,新增環繞通知,切點是 com.wl.sbDemo包路徑下的 且帶有ResponseBody或RequestMapping註解的 且帶有RequestLogging自定義註解的。

      當有同上滿足這三個條件的連線點觸發的時候,就會觸發環繞通知的方法。這個環繞通知的方法主要就是攔截request和response的資訊,列印日誌。

package com.wl.sbDemo.aspect;


import com.wl.sbDemo.aspect.config.RequestAttributeConst;
import com.wl.sbDemo.aspect.web.RequestDetailsLogger;
import com.wl.sbDemo.aspect.web.ResponseDetailsLogger;
import com.wl.sbDemo.aspect.web.ServletContextHolder;
import io.swagger.annotations.ApiOperation;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.time.OffsetDateTime;

/**
 * 本類設計為當有被@RequestBodyLogs修飾的@ControllerAdvice或者@Controller丟擲異常時記錄輸入輸出,
 * 其他情況僅記錄被標記的@RequestMapping或@ResponseBody方法
 *
 * @author soul
 * @see //RequestLogging
 * @see org.springframework.web.bind.annotation.ControllerAdvice
 */
@Aspect
@Component
public class RequestLoggingAspect  {
    private static final Logger LOGGER = LoggerFactory.getLogger(RequestLoggingAspect.class);

    @Around(value = "within(com.wl.sbDemo..*) " +
            "&& (@annotation(org.springframework.web.bind.annotation.ResponseBody)" +
            "|| @annotation(org.springframework.web.bind.annotation.RequestMapping)) " +
            "&& @annotation(com.wl.sbDemo.aspect.RequestLogging)")
    public Object aroundMethod(ProceedingJoinPoint joinPoint) throws Throwable {
        // 生成請求日誌
        RequestDetailsLogger requestLog = generateJsonRequestDetails();
        // 獲取Swagger上的API描述
        injectApiOperationDescription(joinPoint, requestLog);
        // 執行真實請求
        final Object proceed = joinPoint.proceed();
        // 當響應完成時, 列印完整的'request & response'資訊
        requestLog.setResponseTime(OffsetDateTime.now());
        LOGGER.info("RequestLoggingAspect#\r\nREQUEST->\r\n{}\r\nRESPONSE->\r\n {}", requestLog, ResponseDetailsLogger.with(proceed));
        // 放行
        return proceed;
    }

    /**
     * 建立通用的日誌輸出模式並繫結執行緒
     *
     * @return 日誌模型
     */
    private RequestDetailsLogger generateJsonRequestDetails() {
        RequestDetailsLogger logDetails = (RequestDetailsLogger) ServletContextHolder.getRequest().getAttribute(RequestAttributeConst.DETAILS_KEY);
        if (logDetails == null) {
            logDetails = new RequestDetailsLogger();
            ServletContextHolder.getRequest().setAttribute(RequestAttributeConst.DETAILS_KEY, logDetails);
        }
        return logDetails;
    }

    private void injectApiOperationDescription(ProceedingJoinPoint joinPoint, RequestDetailsLogger logDetails) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        final ApiOperation operate = method.getAnnotation(ApiOperation.class);
        if (operate != null) {
            logDetails.setApiDesc(operate.value());
        }
    }

}

      自定義註解:

      定義了自定義註解,用於標記連線點。標記出是切點。

  • @Retention– 定義該註解的生命週期,RUNTIME : 始終不會丟棄,執行期也保留該註解,因此可以使用反射機制讀取該註解的資訊。

  • @Target – 表示該註解用於什麼地方,METHOD:用於描述方法。

package com.wl.sbDemo.aspect;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @author soul
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface RequestLogging {
}

2.4 Controller

      就是一個普遍的controller類,這裡小編用於丟擲異常,丟擲指定的自定義異常。

package com.wl.sbDemo.controller;

import com.wl.sbDemo.common.StatusCode;
import com.wl.sbDemo.exception.Shift;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * Created by Ares on 2018/7/5.
 */
@RestController
@RequestMapping("/user")
public class UserController {

    @GetMapping("/findById")
    public int findById(@RequestParam("id") int id ){
        try {
            if (id>10){
                id = id /0;
            }
        } catch (Exception e) {
            Shift.fatal(StatusCode.INVALID_MODEL_FIELDS,e.getMessage());
        }

        return  id;
    }
}

      Shift丟擲異常類:

package com.wl.sbDemo.exception;

import com.wl.sbDemo.common.RestStatus;
import com.wl.sbDemo.model.ErrorEntity;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.util.Optional;

import static com.google.common.base.Preconditions.checkNotNull;

/**
 * @author soul
 */
public final class Shift {

    private Shift() {
    }

    /**
     * 丟擲具體的{@code RestStatus}異常
     *
     * @param status  自定義異常實體
     * @param details 額外新增至details欄位中的任意實體, 最終會被解析成JSON
     */
    public static void fatal(RestStatus status, Object... details) {
        checkNotNull(status);
        final ErrorEntity entity = new ErrorEntity(status);
        // inject details
        if (details.length > 0) {
            Optional.of(details).ifPresent(entity::setDetails);
        }
        // put it into request, details entity by Rest Status's name
        String errorCode = String.valueOf(status.code());
        bindStatusCodesInRequestScope(errorCode, entity);
        throw new RestStatusException(errorCode);
    }

    private static void bindStatusCodesInRequestScope(String key, ErrorEntity entity) {
        checkNotNull(entity);
        checkNotNull(key);
        final RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        if (requestAttributes != null) {
            ((ServletRequestAttributes) requestAttributes).getRequest().setAttribute(key, entity);
        }
    }
}

      自定義異常RestStatusException:

package com.wl.sbDemo.exception;

/**
 * @author soul
 */
public class RestStatusException extends RuntimeException {
    private static final long serialVersionUID = -8541311111016065562L;

    public RestStatusException(String message) {
        super(message);
    }

    public RestStatusException(String message, Throwable cause) {
        super(message, cause);
    }

    public RestStatusException(Throwable cause) {
        super(cause);
    }

    protected RestStatusException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }
}

2.5 Controller的全域性異常攔截類

      使用了@ControllerAdvice + @ExceptionHandler 全域性處理 Controller 層異常。

  • @ControllerAdvice : 定義全域性異常處理類

  • @ExceptionHandler : 宣告攔截指定異常的方法

      以本例中的restStatusException方法來說,開頭添加了@ResponseBody和@RequestLogging註解,並且這個方法也在com.wl.sbDemo包下,符合Springaop的連線點的條件。所以當這個方法觸發的時候就會觸發切面類中的通知方法。


package com.wl.sbDemo.controller.advice;

import com.google.common.collect.ImmutableMap;
import com.wl.sbDemo.aspect.RequestLogging;
import com.wl.sbDemo.aspect.config.RequestAttributeConst;
import com.wl.sbDemo.common.RestStatus;
import com.wl.sbDemo.common.StatusCode;
import com.wl.sbDemo.exception.IllegalValidateException;
import com.wl.sbDemo.exception.ReservationExpireException;
import com.wl.sbDemo.exception.RestStatusException;
import com.wl.sbDemo.model.ErrorEntity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.UnsatisfiedServletRequestParameterException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;

import javax.servlet.http.HttpServletRequest;

/**
 * @author soul
 */
@ControllerAdvice
public class FaultBarrier {

    private static final Logger LOGGER = LoggerFactory.getLogger(FaultBarrier.class);

    private static final ImmutableMap<Class<? extends Throwable>, RestStatus> EXCEPTION_MAPPINGS;

    static {
        final ImmutableMap.Builder<Class<? extends Throwable>, RestStatus> builder = ImmutableMap.builder();

        // HTTP Request Method不存在
        // 賬戶更新錯誤
        builder.put(ReservationExpireException.class, StatusCode.RESERVATION_EXPIRE);
        // 其他未被發現的異常
        // SpringMVC中引數型別轉換異常,常見於String找不到對應的ENUM而丟擲的異常
        builder.put(MethodArgumentTypeMismatchException.class, StatusCode.INVALID_PARAMS_CONVERSION);

        builder.put(UnsatisfiedServletRequestParameterException.class, StatusCode.INVALID_PARAMS_CONVERSION);

        builder.put(IllegalValidateException.class, StatusCode.INVALID_PARAMS_CONVERSION);

        builder.put(IllegalArgumentException.class, StatusCode.INVALID_PARAMS_CONVERSION);
        // HTTP Request Method不存在
        builder.put(HttpRequestMethodNotSupportedException.class, StatusCode.REQUEST_METHOD_NOT_SUPPORTED);
        // 要求有RequestBody的地方卻傳入了NULL
        builder.put(HttpMessageNotReadableException.class, StatusCode.HTTP_MESSAGE_NOT_READABLE);
        // 通常是操作過快導致DuplicateKey
        builder.put(DuplicateKeyException.class, StatusCode.DUPLICATE_KEY);
        // 其他未被發現的異常
        builder.put(Exception.class, StatusCode.SERVER_UNKNOWN_ERROR);
        EXCEPTION_MAPPINGS = builder.build();
    }

    /**
     * <strong>Request域取出對應錯誤資訊</strong>, 封裝成實體ErrorEntity後轉換成JSON輸出
     *
     * @param e       {@code StatusCode}異常
     * @param request HttpServletRequest
     * @return ErrorEntity
     * @see ErrorEntity
     * @see StatusCode
     */
    @ResponseBody
    @RequestLogging
    @ExceptionHandler(RestStatusException.class)
    public Object restStatusException(Exception e, HttpServletRequest request) {
        // 取出儲存在Shift設定在Request Scope中的ErrorEntity
        return request.getAttribute(e.getMessage());
    }


    /**
     * <strong>Request域取出對應錯誤資訊</strong>, 封裝成實體ErrorEntity後轉換成JSON輸出
     *
     * @param e       {@code IllegalValidateException}異常
     * @param request HttpServletRequest
     * @return ErrorEntity
     * @see ErrorEntity
     */
    @ResponseBody
    @RequestLogging
    @ExceptionHandler(IllegalValidateException.class)
    public Object illegalValidateException(Exception e, HttpServletRequest request) {
        LOGGER.error("request id: {}\r\nexception: {}", request.getAttribute(RequestAttributeConst.REQUEST_ID), e.getMessage());
        if (LOGGER.isDebugEnabled()) {
            e.printStackTrace();
        }
        // 取出儲存在Request域中的Map
        return request.getAttribute(e.getMessage());
    }

    @ResponseBody
    @RequestLogging
    @ExceptionHandler(Exception.class)
    public ErrorEntity exception(Exception e, HttpServletRequest request) {
        if (LOGGER.isDebugEnabled()) {
            e.printStackTrace();
        }
        LOGGER.error("request id: {}\r\nexception: {}", request.getAttribute(RequestAttributeConst.REQUEST_ID), e.getMessage());
        final RestStatus status = EXCEPTION_MAPPINGS.get(e.getClass());
        final ErrorEntity error;
        if (status != null) {
            error = new ErrorEntity(status);
        }
        else {
            error = new ErrorEntity(StatusCode.SERVER_UNKNOWN_ERROR);
        }
        return error;

    }


}

2.6 執行

      執行程式碼後,輸入http://localhost:8080/user/findById?id=sdfsdf,因為id接收的是int,所以傳入字串是肯定報錯的。在看我們的列印的日誌:

      將詳細資訊完美的打印出來,如果有ES日誌收集,更加方便我們檢視。


這裡寫圖片描述

三、小結

      這個實戰,主要用到了springaop,把日誌很好的攔截下來,使用也很方便。同上也用到了自定義註解,指明瞭在方法使用。還用到了springmvc的全域性異常處理類註解@ControllerAdvice 和@ExceptionHandler 更加準確的捕捉問題。