如何实现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

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