作者:何甜甜在嗎
最近專案進入聯調階段,服務層的介面需要和協議層進行互動,協議層需要將入參[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
透過 @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
高併發下請求日誌切面
寫完以後對自己的程式碼很滿意,但是想著可能還有完善的地方就和朋友交流了一下。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
趕緊給你們的應用加上吧【如果沒加的話】,沒有日誌的話,總懷疑上層出錯,但是卻拿不出證據
關於 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 的佔位符
執行效果
日誌跟蹤更方便
DMC 是配置 logback 和 log4j 使用的,使用方式和 ThreadContext 差不多,將 ThreadContext。put 替換為 MDC。put 即可,同時修改日誌配置檔案。
推薦使用 log4j2,為什麼推薦使用 log4j2 可以看下這篇文章:日誌框架,選擇 Logback Or Log4j2?
log4j2 也是可以配合 MDC 一起使用的
MDC 是 slf4j 包下的,其具體使用哪個日誌框架與我們的依賴有關。
連結:https://juejin。im/post/6844904087964614670 來源:掘金