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