如何實現Spring MVC Web項目的日誌記錄?

  Demo Github倉庫:https://github.com/cloudgyb/log-record.git

1、需求分析

       有些系統需要審計日誌功能,簡單來說就是實現用戶操作日誌的記錄。我們約定:一個接口功能足夠單一隻對應用戶的一項功能。對於需要記錄日誌的接口能夠根據配置做到日誌的記錄。將具體的需求總結如下:

  1. 日誌記錄功能不能影響具體的業務邏輯,即對業務代碼無侵入性。
  2. 日誌記錄功能對業務開發者透明,即業務開發者無需知道日誌記錄的實現細節,就能使用。
  3. 日誌記錄功能可插拔,即能夠靈活配置接口記錄日誌的啓用和禁用。

2、設計思想和實現思路

    主要用到的編程思想爲AOP。

    藉助Spring MVC已經實現的AOP功能來實現。

    用到的Spring相關功能有處理器攔截器(HandlerInterceptor)、@ControllerAdvice@RestControllerAdvice(用於全局異常處理)和org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice。

實現HandlerInterceptor的自定義攔截器,向request的屬性列表中設置實際的請求處理器(HandlerMethod)。

藉助@RestControllerAdvice的實現的全局異常處理器,對異常進行捕獲並轉換成統一pojo類。

實現org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice接口來實現具體的日誌記錄功能。

使用自定義的註解@RecordLog來標記那些接口需要記錄日誌。

 

3、代碼實現:

控制器攔截器ControllerLogInterceptor,用於向request的attribute中設置hadnler(HandlerMethod,請求處理方法)。在後面的LogRecorder中用到,具體往下看。

import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Enumeration;

public class ControllerLogInterceptor implements HandlerInterceptor {

    /**
     * 將實際的處理器(HandlerMethod)設置到request的屬性列表中,
     * 用於後續的日誌記錄器獲取處理器方法的註解,以此來判斷是否需要記錄日誌
     * @see LogRecorder
     */
    @Override
    public boolean preHandle(javax.servlet.http.HttpServletRequest request, javax.servlet.http.HttpServletResponse response, Object handler) {
        request.setAttribute("handler", handler);
        return true;
    }

    /**
     * 經過全局的{@link GlobalExceptionHandler}異常處理器處理之後這裏的入參ex
     * 總是null,所以不要在該方法中試圖通過ex來捕獲和處理程序的錯誤
     * @param ex 處理器拋出的異常,總是null。
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        if (ex != null) {
            System.out.println("失敗!");
        }
        System.out.println(request);
        Enumeration<String> attributeNames = request.getAttributeNames();
    }
}

全局異常處理器GlobalExceptionHandler,這兒實現簡單了一點,所有的異常都走exceptionHandle方法,根據需要可以細分異常處理。最終將異常包裝成ResponseResult.

import org.gyb.ssh.pojo.ResponseResult;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

/**
 * 全局異常處理器,捕獲和處理所有的異常,統一包裝異常爲ResponseResult
 *
 * @see ResponseResult
 */
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ResponseResult exceptionHandle(Exception e) {
        return ResponseResult.of(e);
    }
}
統一結果ResponseResult 

public class ResponseResult {
    private int code;
    private String msg;
    private Object data;

    public ResponseResult(int code, String msg, Object data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }
    public static ResponseResult of(int code,String msg,Object data){
        return new ResponseResult(code,msg,data);
    }

    public static ResponseResult of(Exception e){
        String msg="";
        if(e.getMessage()!= null)
            msg = e.getMessage();
        else msg = e.toString();
        return new ResponseResult(500,msg,null);
    }

    public static ResponseResult ok(Object data){
        return of(200,"successful!",data);
    }
    public static ResponseResult ok(String msg ,Object data){
        return of(200,msg,data);
    }
    public static ResponseResult error(Object data){
        return of(500,"failed!",null);
    }
    public static ResponseResult error(String msg, Object data){
        return of(500,"failed!",data);
    }
    //省略了getter和setter
}
LogRecorder實現了ResponseBodyAdvice接口,在ResponseBody寫之前調用beforeBodyWrite方法,實現日誌記錄的功能。 

import org.gyb.ssh.pojo.ResponseResult;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

import javax.servlet.http.HttpServletRequest;

@ControllerAdvice
public class LogRecorder implements ResponseBodyAdvice {
    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        //只支持處理json數據
        return converterType.getName().equals("org.springframework.http.converter.json.MappingJackson2HttpMessageConverter");
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        //如果body被統一包裝成ResponseResult,則進行下面的處理
        if(body instanceof ResponseResult && request instanceof ServletServerHttpRequest){
            ResponseResult respData = (ResponseResult) body;
            ServletServerHttpRequest req = (ServletServerHttpRequest)request;
            HttpServletRequest servletRequest = req.getServletRequest();
            //獲取處理該請求對應的處理器方法,即Controller的具體Method
            Object handler = servletRequest.getAttribute("handler");
            HandlerMethod method = (HandlerMethod) handler;
            RecordLog recordLog = method.getMethodAnnotation(RecordLog.class);
            if(recordLog !=null){
                String content = recordLog.content();
                int code = respData.getCode();
                if(code != 200){
                   //這兒可以是具體的日誌存庫動作,暫且用輸出代替了
                    System.out.println(content+"失敗了!");
                }else{
                    System.out.println(content+"成功!");
                }
            }
        }
        return body;
    }
}

 用於記錄日誌標記的註解@RecordLog

import java.lang.annotation.*;

/**
 * 用於標識使用記錄日誌的註解,只能標註在Controller類的方法上,標註在其他地方無意義。
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Inherited
public @interface RecordLog {
    /**
     * 具體的日誌內容
     */
    @AliasFor("content")
    String value() default "";
    @AliasFor("value")
    String content() default "";
}

一個實例:只需要在需求記錄日誌的接口上標註 @RecordLog(content = "獲取用戶列表")即可實現日誌的記錄。

@RestController
@RequestMapping("/user")
public class UserManageController {

    private UserService userService;

    @Autowired
    public UserManageController(UserService userService) {
        this.userService = userService;
    }

    /**
     * 正常處理
     */
    @RecordLog(content = "獲取用戶列表")
    @GetMapping("/list")
    public List<User> listUser(){
        return userService.getAll();
    }

    /**
     * 拋出了一個異常
     */
    @RecordLog(content = "獲取用戶列表")
    @GetMapping("/listWithException")
    public List<User> listUserWithException(){
        if (true)
            throw new RuntimeException();
        return userService.getAll();
    }
}

上面的實現基本滿足需求! 

  Demo Github倉庫:https://github.com/cloudgyb/log-record.git

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章