Demo Github仓库:https://github.com/cloudgyb/log-record.git
1、需求分析
有些系统需要审计日志功能,简单来说就是实现用户操作日志的记录。我们约定:一个接口功能足够单一只对应用户的一项功能。对于需要记录日志的接口能够根据配置做到日志的记录。将具体的需求总结如下:
- 日志记录功能不能影响具体的业务逻辑,即对业务代码无侵入性。
- 日志记录功能对业务开发者透明,即业务开发者无需知道日志记录的实现细节,就能使用。
- 日志记录功能可插拔,即能够灵活配置接口记录日志的启用和禁用。
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