支持SpEL表達(dá)式的自定義日志注解@SysLog介紹
序言
之前封裝過一個日志注解,打印方法執(zhí)行信息,功能較為單一不夠靈活,近來興趣來了,想重構(gòu)下,使其支持表達(dá)式語法,以應(yīng)對靈活的日志打印需求。
該注解是方法層面的日志打印,如需更細(xì)的粒度,還請手?jǐn)]log.xxx()。
預(yù)期
通過自定義注解,靈活的語法表達(dá)式,攔截自定義注解下的方法并打印日志
日志要支持以下內(nèi)容:
- 方法執(zhí)行時間
- 利用已知信息(入?yún)ⅰ⑴渲?、方法),書寫靈活的日志SpEL表達(dá)式
- 打印方法返回結(jié)果
- 按照指定日志類型打印日志
思路
定義自定義注解
攔截自定義注解方法完成以下動作
- a. 計算方法執(zhí)行時間
- b. 解析特定類型的表達(dá)式(這里不僅限于SpEL表達(dá)式)
- c. 獲取返回結(jié)果
- d. 按照日志類型進(jìn)行打印
特定類型表達(dá)式方案
- a. 屬性解析表達(dá)式(如:mybatis對屬性的解析,xxx${yyy.aaa}zzz或xxx#{yyy.bbb}zzz書寫方式 )
- b. SpEL表達(dá)式(如:${xxx}、#{‘xxx’+#yyy.ppp+aaa.mmm()})
問題:選屬性解析表達(dá)式、還是SpEL表達(dá)式
屬性解析表達(dá)式:
- a. 優(yōu)點:直觀、配置簡單
- b. 缺點:需要自行處理屬性為待解析對象(容易翻車)
SpEL表達(dá)式:
- a. 優(yōu)點:解析強(qiáng)大,性能優(yōu)良
- b. 缺點:配置復(fù)雜不直觀
過程
定義自定義注解@SysLog
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface SysLog { /** * 日志描述 * * @return 返回日志描述信息 */ String value(); /** * 日志等級(info、debug、trace、warn、error) * * @return 返回日志等級 */ String level() default "info"; /** * 打印方法返回結(jié)果 * * @return 返回打印方法返回結(jié)果 */ boolean printResult() default false; }
該類包含以下信息:
- 日志信息(支持動態(tài)表達(dá)式)
- 日志級別(info、debug、trace、warn、error)
- 是否打印方法返回的結(jié)果
走過的彎路1(PropertyParser)
采用MyBatis對XML解析的方式進(jìn)行解析,需要把攔截到的入?yún)ean內(nèi)的屬性轉(zhuǎn)換為Properties的方式進(jìn)行parse,遇到復(fù)雜對象就容易出錯,屬性無法進(jìn)行動態(tài)解析,具體就不詳細(xì)描述了,感興趣的可以看下這個類org.apache.ibatis.parsing.PropertyParser
走過的彎路2(ParserContext)
比使用MyBatis更加友好一丟丟,使用Spring自帶的ParserContext設(shè)定解析規(guī)則,結(jié)合解析類ExpressionParser進(jìn)行解析,也沒有解決上面遇到的問題,不用引用其它jar包或手?jǐn)]解析規(guī)則,具體就不詳細(xì)描述了,感興趣的可以看下這個類
org.springframework.expression.ParserContext
最后的定型方案:
切面攔截方法前后的入?yún)ⅰ⒊鰠?、異常?/p>
SpEL表達(dá)式解析,根據(jù)表達(dá)式去動態(tài)解析,語法比預(yù)想中強(qiáng)大;
為了確認(rèn)性能損耗,最后還做了個性能壓測
自定義注解切面類SysLogAspect(最終選型SpEL表達(dá)式方式)
/** * SysLog方法攔截打印日志類 * * @author lipengfei * @version 1.0 * @since 2019/3/29 10:49 AM */ @Aspect public class SysLogAspect { private static final Logger log = LoggerFactory.getLogger(SysLogAspect.class); private static final DefaultParameterNameDiscoverer DEFAULT_PARAMETER_NAME_DISCOVERER = new DefaultParameterNameDiscoverer(); private static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser(); private static final TemplateParserContext TEMPLATE_PARSER_CONTEXT = new TemplateParserContext(); private static final ThreadLocal<StandardEvaluationContext> StandardEvaluationContextThreadLocal = new ThreadLocal<>(); /** * 開始時間 */ private static final ThreadLocal<Long> START_TIME = new ThreadLocal<>(); @Pointcut("@annotation(net.zongfei.core.log.SysLog)") public void sysLogPointCut() { } /** * 處理完請求后執(zhí)行 * * @param joinPoint 切點 */ @SuppressWarnings("unused") @Before("sysLogPointCut()") public void doBeforeReturning(JoinPoint joinPoint) { // 設(shè)置請求開始時間 START_TIME.set(System.currentTimeMillis()); } /** * 處理完請求后執(zhí)行 * * @param joinPoint 切點 */ @AfterReturning( pointcut = "sysLogPointCut()", returning = "result" ) public void doAfterReturning(JoinPoint joinPoint, Object result) { printLog(joinPoint, result, null); } /** * 攔截異常操作 * * @param joinPoint 切點 * @param e 異常 */ @AfterThrowing( pointcut = "sysLogPointCut()", throwing = "e" ) public void doAfterThrowing(JoinPoint joinPoint, Exception e) { printLog(joinPoint, null, e); } /** * 打印日志 * * @param point 切點 * @param result 返回結(jié)果 * @param e 異常 */ protected void printLog(JoinPoint point, Object result, Exception e) { MethodSignature signature = (MethodSignature) point.getSignature(); String className = ClassUtils.getUserClass(point.getTarget()).getName(); String methodName = point.getSignature().getName(); Class<?>[] parameterTypes = signature.getMethod().getParameterTypes(); Method method; try { method = point.getTarget().getClass().getMethod(methodName, parameterTypes); } catch (NoSuchMethodException ex) { ex.printStackTrace(); return; } // 獲取注解相關(guān)信息 SysLog sysLog = method.getAnnotation(SysLog.class); String logExpression = sysLog.value(); String logLevel = sysLog.level(); boolean printResult = sysLog.printResult(); // 解析日志中的表達(dá)式 Object[] args = point.getArgs(); String[] parameterNames = DEFAULT_PARAMETER_NAME_DISCOVERER.getParameterNames(method); Map<String, Object> params = new HashMap<>(); if (parameterNames != null) { for (int i = 0; i < parameterNames.length; i++) { params.put(parameterNames[i], args[i]); } } // 解析表達(dá)式 String logInfo = parseExpression(logExpression, params); Long costTime = null; // 請求開始時間 Long startTime = START_TIME.get(); if (startTime != null) { // 請求耗時 costTime = System.currentTimeMillis() - startTime; // 清空開始時間 START_TIME.remove(); } // 如果發(fā)生異常,強(qiáng)制打印錯誤級別日志 if(e != null) { log.error("{}#{}(): {}, exception: {}, costTime: {}ms", className, methodName, logInfo, e.getMessage(), costTime); return; } // 以下為打印對應(yīng)級別的日志 if("info".equalsIgnoreCase(logLevel)){ if (printResult) { log.info("{}#{}(): {}, result: {}, costTime: {}ms", className, methodName, logInfo, result, costTime); } else { log.info("{}#{}(): {}, costTime: {}ms", className, methodName, logInfo, costTime); } } else if("debug".equalsIgnoreCase(logLevel)){ if (printResult) { log.debug("{}#{}(): {}, result: {}, costTime: {}ms", className, methodName, logInfo, result, costTime); } else { log.debug("{}#{}(): {}, costTime: {}ms", className, methodName, logInfo, costTime); } } else if("trace".equalsIgnoreCase(logLevel)){ if (printResult) { log.trace("{}#{}(): {}, result: {}, costTime: {}ms", className, methodName, logInfo, result, costTime); } else { log.trace("{}#{}(): {}, costTime: {}ms", className, methodName, logInfo, costTime); } } else if("warn".equalsIgnoreCase(logLevel)){ if (printResult) { log.warn("{}#{}(): {}, result: {}, costTime: {}ms", className, methodName, logInfo, result, costTime); } else { log.warn("{}#{}(): {}, costTime: {}ms", className, methodName, logInfo, costTime); } } else if("error".equalsIgnoreCase(logLevel)){ if (printResult) { log.error("{}#{}(): {}, result: {}, costTime: {}ms", className, methodName, logInfo, result, costTime); } else { log.error("{}#{}(): {}, costTime: {}ms", className, methodName, logInfo, costTime); } } } private String parseExpression(String template, Map<String, Object> params) { // 將ioc容器設(shè)置到上下文中 ApplicationContext applicationContext = SpringContextUtil.getContext(); // 線程初始化StandardEvaluationContext StandardEvaluationContext standardEvaluationContext = StandardEvaluationContextThreadLocal.get(); if(standardEvaluationContext == null){ standardEvaluationContext = new StandardEvaluationContext(applicationContext); standardEvaluationContext.addPropertyAccessor(new BeanFactoryAccessor()); StandardEvaluationContextThreadLocal.set(standardEvaluationContext); } // 將自定義參數(shù)添加到上下文 standardEvaluationContext.setVariables(params); // 解析表達(dá)式 Expression expression = EXPRESSION_PARSER.parseExpression(template, TEMPLATE_PARSER_CONTEXT); return expression.getValue(standardEvaluationContext, String.class); } }
該類按照上面思路中的邏輯進(jìn)行開發(fā),沒有特別復(fù)雜的邏輯
為了提高性能和線程安全,對一些類加了static和ThreadLocal
結(jié)果
使用方式:
@SysLog(value = “#{‘用戶登錄'}”) @SysLog(value = “#{'用戶登錄: method: ' + #loginRequest.username}”, printResult = true) @SysLog(value = “#{'用戶登錄: method: ' + #loginRequest.username + authBizService.test()}”, printResult = true) …
更多書寫方式參考SpEL表達(dá)式即可
/** * 用戶登錄接口 * * @param loginRequest 用戶登錄輸入?yún)?shù)類 * @return 返回用戶登錄結(jié)果輸出類 */ @ApiOperation("用戶登錄接口") @PostMapping(value = "/login") @SysLog(value = "#{'用戶登錄: username: ' + #loginRequest.username + authBizService.test()}", level = "debug", printResult = true) @Access(type = AccessType.LOGIN, description = "用戶登錄") public LoginResponse login( @ApiParam(value = "用戶登錄參數(shù)") @RequestBody @Valid LoginRequest loginRequest ) { // 業(yè)務(wù)代碼 }
結(jié)果打?。?/strong>
2021-09-01 22:04:05.713 ERROR 98511 CRM [2cab21fdd2469b2e--2cab21fdd2469b2e] [nio-8000-exec-2] n.z.m.a.SysLogAspect : net.zongfei.crm.api.AuthController#login(): 用戶登錄: username: lipengfei90@live.cn method: this is test method(), exception: [用戶模塊] - 用戶名或密碼錯誤, costTime: 261ms
壓測下來性能損耗較低(可忽略不計)
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
詳解springcloud之服務(wù)注冊與發(fā)現(xiàn)
本次分享的是關(guān)于springcloud服務(wù)注冊與發(fā)現(xiàn)的內(nèi)容,將通過分別搭建服務(wù)中心,服務(wù)注冊,服務(wù)發(fā)現(xiàn)來說明,非常具有實用價值,需要的朋友可以參考下2018-06-06Java連接SAP RFC實現(xiàn)數(shù)據(jù)抽取的示例詳解
這篇文章主要為大家學(xué)習(xí)介紹了Java如何連接SAP RFC實現(xiàn)數(shù)據(jù)抽取的功能,文中的示例代碼講解詳細(xì),具有一定的參考價值,需要的可以了解下2023-08-08Java實戰(zhàn)之圖書管理系統(tǒng)的實現(xiàn)
這篇文章主要介紹了如何利用Java語言編寫一個圖書管理系統(tǒng),文中采用的技術(shù)有Springboot、SpringMVC、MyBatis、ThymeLeaf 等,需要的可以參考一下2022-03-03