寫了個特牛的日誌切面,以後甩鍋更方便了

作者:何甜甜在嗎

最近專案進入聯調階段,服務層的介面需要和協議層進行互動,協議層需要將入參[json字串]組裝成服務層所需的json字串,組裝的過程中很容易出錯。入參出錯導致介面除錯失敗問題在聯調中出現很多次,因此就想寫一個請求日誌切面把入參資訊列印一下,同時協議層呼叫服務層介面名稱對不上也出現了幾次,透過請求日誌切面就可以知道上層是否有沒有發起呼叫,方便前後端甩鍋還能拿出證據

寫在前面

本篇文章是實戰性的,對於切面的原理不會講解,只會簡單介紹一下切面的知識點

切面介紹

面向切面程式設計是一種程式設計正規化,它作為 OOP 面向物件程式設計的一種補充,用於處理系統中分佈於各個模組的橫切關注點,比如

事務管理

許可權控制

快取控制

日誌列印

等等。

AOP 把軟體的功能模組分為兩個部分:核心關注點和橫切關注點。業務處理的主要功能為核心關注點,而非核心、需要拓展的功能為橫切關注點。AOP 的作用在於分離系統中的各種關注點,將核心關注點和橫切關注點進行分離,使用切面有以下好處:

集中處理某一關注點 / 橫切邏輯

可以很方便的新增 / 刪除關注點

侵入性少,增強程式碼可讀性及可維護性 因此當想列印請求日誌時很容易想到切面,對控制層程式碼 0 侵入

切面的使用【基於註解】

@Aspect => 宣告該類為一個註解類

切點註解:

@Pointcut => 定義一個切點,可以簡化程式碼

通知註解:

@Before => 在切點之前執行程式碼

@After => 在切點之後執行程式碼

@AfterReturning => 切點返回內容後執行程式碼,可以對切點的返回值進行封裝

@AfterThrowing => 切點丟擲異常後執行

@Around => 環繞,在切點前後執行程式碼

動手寫一個請求日誌切面

使用 @Pointcut 定義切點

@Pointcut(“execution(* your_package。controller。。*(。。))”) public void requestServer() { }

@Pointcut 定義了一個切點,因為是請求日誌切邊,因此切點定義的是 Controller 包下的所有類下的方法。定義切點以後在通知註解中直接使用 requestServer 方法名就可以了

使用 @Before 再切點前執行

@Before(“requestServer()”) public void doBefore(JoinPoint joinPoint) { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder。getRequestAttributes(); HttpServletRequest request = attributes。getRequest(); LOGGER。info(“===============================Start========================”); LOGGER。info(“IP : {}”, request。getRemoteAddr()); LOGGER。info(“URL : {}”, request。getRequestURL()。toString()); LOGGER。info(“HTTP Method : {}”, request。getMethod()); LOGGER。info(“Class Method : {}。{}”, joinPoint。getSignature()。getDeclaringTypeName(), joinPoint。getSignature()。getName()); }

在進入 Controller 方法前,打印出呼叫方 IP、請求 URL、HTTP 請求型別、呼叫的方法名

使用 @Around 列印進入控制層的入參

@Around(“requestServer()”) public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { long start = System。currentTimeMillis(); Object result = proceedingJoinPoint。proceed(); LOGGER。info(“Request Params : {}”, getRequestParams(proceedingJoinPoint)); LOGGER。info(“Result : {}”, result); LOGGER。info(“Time Cost : {} ms”, System。currentTimeMillis() - start); return result; }

列印了入參、結果以及耗時

getRquestParams 方法

private Map getRequestParams(ProceedingJoinPoint proceedingJoinPoint) { Map requestParams = new HashMap<>(); //引數名 String[] paramNames = ((MethodSignature)proceedingJoinPoint。getSignature())。getParameterNames(); //引數值 Object[] paramValues = proceedingJoinPoint。getArgs(); for (int i = 0; i < paramNames。length; i++) { Object value = paramValues[i]; //如果是檔案物件 if (value instanceof MultipartFile) { MultipartFile file = (MultipartFile) value; value = file。getOriginalFilename(); //獲取檔名 } requestParams。put(paramNames[i], value); } return requestParams; }

透過 @PathVariable 以及 @RequestParam 註解傳遞的引數無法打印出引數名,因此需要手動拼接一下引數名,同時對檔案物件進行了特殊處理,只需獲取檔名即可

@After 方法呼叫後執行

@After(“requestServer()”) public void doAfter(JoinPoint joinPoint) { LOGGER。info(“===============================End========================”); }

沒有業務邏輯只是列印了 End

完整切面程式碼

@Component @Aspect public class RequestLogAspect { private final static Logger LOGGER = LoggerFactory。getLogger(RequestLogAspect。class); @Pointcut(“execution(* your_package。controller。。*(。。))”) public void requestServer() { } @Before(“requestServer()”) public void doBefore(JoinPoint joinPoint) { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder。getRequestAttributes(); HttpServletRequest request = attributes。getRequest(); LOGGER。info(“===============================Start========================”); LOGGER。info(“IP : {}”, request。getRemoteAddr()); LOGGER。info(“URL : {}”, request。getRequestURL()。toString()); LOGGER。info(“HTTP Method : {}”, request。getMethod()); LOGGER。info(“Class Method : {}。{}”, joinPoint。getSignature()。getDeclaringTypeName(), joinPoint。getSignature()。getName()); } @Around(“requestServer()”) public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { long start = System。currentTimeMillis(); Object result = proceedingJoinPoint。proceed(); LOGGER。info(“Request Params : {}”, getRequestParams(proceedingJoinPoint)); LOGGER。info(“Result : {}”, result); LOGGER。info(“Time Cost : {} ms”, System。currentTimeMillis() - start); return result; } @After(“requestServer()”) public void doAfter(JoinPoint joinPoint) { LOGGER。info(“===============================End========================”); } /** * 獲取入參 * @param proceedingJoinPoint * * @return * */ private Map getRequestParams(ProceedingJoinPoint proceedingJoinPoint) { Map requestParams = new HashMap<>(); //引數名 String[] paramNames = ((MethodSignature)proceedingJoinPoint。getSignature())。getParameterNames(); //引數值 Object[] paramValues = proceedingJoinPoint。getArgs(); for (int i = 0; i < paramNames。length; i++) { Object value = paramValues[i]; //如果是檔案物件 if (value instanceof MultipartFile) { MultipartFile file = (MultipartFile) value; value = file。getOriginalFilename(); //獲取檔名 } requestParams。put(paramNames[i], value); } return requestParams; } }

高併發下請求日誌切面

寫完以後對自己的程式碼很滿意,但是想著可能還有完善的地方就和朋友交流了一下。emmmm

寫了個特牛的日誌切面,以後甩鍋更方便了

果然還有繼續最佳化的地方 每個資訊都列印一行,在高併發請求下確實會出現請求之間列印日誌序列的問題,因為測試階段請求數量較少沒有出現序列的情況,果然生產環境才是第一發展力,能夠遇到更多 bug,寫更健壯的程式碼 解決日誌序列的問題只要將多行列印資訊合併為一行就可以了,因此構造一個物件

RequestInfo。java

@Data public class RequestInfo { private String ip; private String url; private String httpMethod; private String classMethod; private Object requestParams; private Object result; private Long timeCost; }

環繞通知方法體

@Around(“requestServer()”) public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { long start = System。currentTimeMillis(); ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder。getRequestAttributes(); HttpServletRequest request = attributes。getRequest(); Object result = proceedingJoinPoint。proceed(); RequestInfo requestInfo = new RequestInfo(); requestInfo。setIp(request。getRemoteAddr()); requestInfo。setUrl(request。getRequestURL()。toString()); requestInfo。setHttpMethod(request。getMethod()); requestInfo。setClassMethod(String。format(“%s。%s”, proceedingJoinPoint。getSignature()。getDeclaringTypeName(), proceedingJoinPoint。getSignature()。getName())); requestInfo。setRequestParams(getRequestParamsByProceedingJoinPoint(proceedingJoinPoint)); requestInfo。setResult(result); requestInfo。setTimeCost(System。currentTimeMillis() - start); LOGGER。info(“Request Info : {}”, JSON。toJSONString(requestInfo)); return result; }

將 url、http request 這些資訊組裝成 RequestInfo 物件,再序列化列印物件

列印

序列化

物件結果而不是直接列印物件是因為序列化有更直觀、更清晰,同時可以藉助線上解析工具對結果進行解析

寫了個特牛的日誌切面,以後甩鍋更方便了

是不是還不錯

在解決高併發下請求序列問題的同時添加了對

異常請求資訊的列印

,透過使用 @AfterThrowing 註解對丟擲異常的方法進行處理

RequestErrorInfo。java

@Data public class RequestErrorInfo { private String ip; private String url; private String httpMethod; private String classMethod; private Object requestParams; private RuntimeException exception; }

異常通知環繞體

@AfterThrowing(pointcut = “requestServer()”, throwing = “e”) public void doAfterThrow(JoinPoint joinPoint, RuntimeException e) { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder。getRequestAttributes(); HttpServletRequest request = attributes。getRequest(); RequestErrorInfo requestErrorInfo = new RequestErrorInfo(); requestErrorInfo。setIp(request。getRemoteAddr()); requestErrorInfo。setUrl(request。getRequestURL()。toString()); requestErrorInfo。setHttpMethod(request。getMethod()); requestErrorInfo。setClassMethod(String。format(“%s。%s”, joinPoint。getSignature()。getDeclaringTypeName(), joinPoint。getSignature()。getName())); requestErrorInfo。setRequestParams(getRequestParamsByJoinPoint(joinPoint)); requestErrorInfo。setException(e); LOGGER。info(“Error Request Info : {}”, JSON。toJSONString(requestErrorInfo)); }

對於異常,耗時是沒有意義的,因此不統計耗時,而是添加了異常的列印

最後放一下完整日誌請求切面程式碼:

@Component @Aspect public class RequestLogAspect { private final static Logger LOGGER = LoggerFactory。getLogger(RequestLogAspect。class); @Pointcut(“execution(* your_package。controller。。*(。。))”) public void requestServer() { } @Around(“requestServer()”) public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { long start = System。currentTimeMillis(); ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder。getRequestAttributes(); HttpServletRequest request = attributes。getRequest(); Object result = proceedingJoinPoint。proceed(); RequestInfo requestInfo = new RequestInfo(); requestInfo。setIp(request。getRemoteAddr()); requestInfo。setUrl(request。getRequestURL()。toString()); requestInfo。setHttpMethod(request。getMethod()); requestInfo。setClassMethod(String。format(“%s。%s”, proceedingJoinPoint。getSignature()。getDeclaringTypeName(), proceedingJoinPoint。getSignature()。getName())); requestInfo。setRequestParams(getRequestParamsByProceedingJoinPoint(proceedingJoinPoint)); requestInfo。setResult(result); requestInfo。setTimeCost(System。currentTimeMillis() - start); LOGGER。info(“Request Info : {}”, JSON。toJSONString(requestInfo)); return result; } @AfterThrowing(pointcut = “requestServer()”, throwing = “e”) public void doAfterThrow(JoinPoint joinPoint, RuntimeException e) { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder。getRequestAttributes(); HttpServletRequest request = attributes。getRequest(); RequestErrorInfo requestErrorInfo = new RequestErrorInfo(); requestErrorInfo。setIp(request。getRemoteAddr()); requestErrorInfo。setUrl(request。getRequestURL()。toString()); requestErrorInfo。setHttpMethod(request。getMethod()); requestErrorInfo。setClassMethod(String。format(“%s。%s”, joinPoint。getSignature()。getDeclaringTypeName(), joinPoint。getSignature()。getName())); requestErrorInfo。setRequestParams(getRequestParamsByJoinPoint(joinPoint)); requestErrorInfo。setException(e); LOGGER。info(“Error Request Info : {}”, JSON。toJSONString(requestErrorInfo)); } /** * 獲取入參 * @param proceedingJoinPoint * * @return * */ private Map getRequestParamsByProceedingJoinPoint(ProceedingJoinPoint proceedingJoinPoint) { //引數名 String[] paramNames = ((MethodSignature)proceedingJoinPoint。getSignature())。getParameterNames(); //引數值 Object[] paramValues = proceedingJoinPoint。getArgs(); return buildRequestParam(paramNames, paramValues); } private Map getRequestParamsByJoinPoint(JoinPoint joinPoint) { //引數名 String[] paramNames = ((MethodSignature)joinPoint。getSignature())。getParameterNames(); //引數值 Object[] paramValues = joinPoint。getArgs(); return buildRequestParam(paramNames, paramValues); } private Map buildRequestParam(String[] paramNames, Object[] paramValues) { Map requestParams = new HashMap<>(); for (int i = 0; i < paramNames。length; i++) { Object value = paramValues[i]; //如果是檔案物件 if (value instanceof MultipartFile) { MultipartFile file = (MultipartFile) value; value = file。getOriginalFilename(); //獲取檔名 } requestParams。put(paramNames[i], value); } return requestParams; } @Data public class RequestInfo { private String ip; private String url; private String httpMethod; private String classMethod; private Object requestParams; private Object result; private Long timeCost; } @Data public class RequestErrorInfo { private String ip; private String url; private String httpMethod; private String classMethod; private Object requestParams; private RuntimeException exception; } }

趕緊給你們的應用加上吧【如果沒加的話】,沒有日誌的話,總懷疑上層出錯,但是卻拿不出證據

寫了個特牛的日誌切面,以後甩鍋更方便了

關於 traceId 跟蹤定位,可以根據 traceId 跟蹤整條呼叫鏈,以 log4j2 為例介紹如何加入 traceId

新增攔截器

public class LogInterceptor implements HandlerInterceptor { private final static String TRACE_ID = “traceId”; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String traceId = java。util。UUID。randomUUID()。toString()。replaceAll(“-”, “”)。toUpperCase(); ThreadContext。put(“traceId”, traceId); return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { ThreadContext。 remove(TRACE_ID); } }

在呼叫前透過 ThreadContext 加入 traceId,呼叫完成後移除

修改日誌配置檔案 在原來的日誌格式中

新增 traceId 的佔位符

[TRACEID:%X{traceId}] %d{HH:mm:ss。SSS} %-5level %class{-1}。%M()/%L - %msg%xEx%n

執行效果

寫了個特牛的日誌切面,以後甩鍋更方便了

日誌跟蹤更方便

DMC 是配置 logback 和 log4j 使用的,使用方式和 ThreadContext 差不多,將 ThreadContext。put 替換為 MDC。put 即可,同時修改日誌配置檔案。

推薦使用 log4j2,為什麼推薦使用 log4j2 可以看下這篇文章:日誌框架,選擇 Logback Or Log4j2?

log4j2 也是可以配合 MDC 一起使用的

寫了個特牛的日誌切面,以後甩鍋更方便了

MDC 是 slf4j 包下的,其具體使用哪個日誌框架與我們的依賴有關。

連結:https://juejin。im/post/6844904087964614670 來源:掘金