支持SpEL表达式的自定义日志注解@SysLog介绍
作者:小p同学90 发布时间:2023-08-27 09:38:42
序言
之前封装过一个日志注解,打印方法执行信息,功能较为单一不够灵活,近来兴趣来了,想重构下,使其支持表达式语法,以应对灵活的日志打印需求。
该注解是方法层面的日志打印,如需更细的粒度,还请手撸log.xxx()。
预期
通过自定义注解,灵活的语法表达式,拦截自定义注解下的方法并打印日志
日志要支持以下内容:
方法执行时间
利用已知信息(入参、配置、方法),书写灵活的日志SpEL表达式
打印方法返回结果
按照指定日志类型打印日志
思路
定义自定义注解
拦截自定义注解方法完成以下动作
a. 计算方法执行时间
b. 解析特定类型的表达式(这里不仅限于SpEL表达式)
c. 获取返回结果
d. 按照日志类型进行打印
特定类型表达式方案
a. 属性解析表达式(如:mybatis对属性的解析,xxx${yyy.aaa}zzz或xxx#{yyy.bbb}zzz书写方式 )
b. SpEL表达式(如:${xxx}、#{‘xxx’+#yyy.ppp+aaa.mmm()})
问题:选属性解析表达式、还是SpEL表达式
属性解析表达式:
a. 优点:直观、配置简单
b. 缺点:需要自行处理属性为待解析对象(容易翻车)
SpEL表达式:
a. 优点:解析强大,性能优良
b. 缺点:配置复杂不直观
过程
定义自定义注解@SysLog
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SysLog {
/**
* 日志描述
*
* @return 返回日志描述信息
*/
String value();
/**
* 日志等级(info、debug、trace、warn、error)
*
* @return 返回日志等级
*/
String level() default "info";
/**
* 打印方法返回结果
*
* @return 返回打印方法返回结果
*/
boolean printResult() default false;
}
该类包含以下信息:
日志信息(支持动态表达式)
日志级别(info、debug、trace、warn、error)
是否打印方法返回的结果
走过的弯路1(PropertyParser)
采用MyBatis对XML解析的方式进行解析,需要把拦截到的入参Bean内的属性转换为Properties的方式进行parse,遇到复杂对象就容易出错,属性无法进行动态解析,具体就不详细描述了,感兴趣的可以看下这个类org.apache.ibatis.parsing.PropertyParser
走过的弯路2(ParserContext)
比使用MyBatis更加友好一丢丢,使用Spring自带的ParserContext设定解析规则,结合解析类ExpressionParser进行解析,也没有解决上面遇到的问题,不用引用其它jar包或手撸解析规则,具体就不详细描述了,感兴趣的可以看下这个类
org.springframework.expression.ParserContext
最后的定型方案:
切面拦截方法前后的入参、出参、异常,
SpEL表达式解析,根据表达式去动态解析,语法比预想中强大;
为了确认性能损耗,最后还做了个性能压测
自定义注解切面类SysLogAspect(最终选型SpEL表达式方式)
/**
* 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() {
}
/**
* 处理完请求后执行
*
* @param joinPoint 切点
*/
@SuppressWarnings("unused")
@Before("sysLogPointCut()")
public void doBeforeReturning(JoinPoint joinPoint) {
// 设置请求开始时间
START_TIME.set(System.currentTimeMillis());
}
/**
* 处理完请求后执行
*
* @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 返回结果
* @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;
}
// 获取注解相关信息
SysLog sysLog = method.getAnnotation(SysLog.class);
String logExpression = sysLog.value();
String logLevel = sysLog.level();
boolean printResult = sysLog.printResult();
// 解析日志中的表达式
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]);
}
}
// 解析表达式
String logInfo = parseExpression(logExpression, params);
Long costTime = null;
// 请求开始时间
Long startTime = START_TIME.get();
if (startTime != null) {
// 请求耗时
costTime = System.currentTimeMillis() - startTime;
// 清空开始时间
START_TIME.remove();
}
// 如果发生异常,强制打印错误级别日志
if(e != null) {
log.error("{}#{}(): {}, exception: {}, costTime: {}ms", className, methodName, logInfo, e.getMessage(), costTime);
return;
}
// 以下为打印对应级别的日志
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容器设置到上下文中
ApplicationContext applicationContext = SpringContextUtil.getContext();
// 线程初始化StandardEvaluationContext
StandardEvaluationContext standardEvaluationContext = StandardEvaluationContextThreadLocal.get();
if(standardEvaluationContext == null){
standardEvaluationContext = new StandardEvaluationContext(applicationContext);
standardEvaluationContext.addPropertyAccessor(new BeanFactoryAccessor());
StandardEvaluationContextThreadLocal.set(standardEvaluationContext);
}
// 将自定义参数添加到上下文
standardEvaluationContext.setVariables(params);
// 解析表达式
Expression expression = EXPRESSION_PARSER.parseExpression(template, TEMPLATE_PARSER_CONTEXT);
return expression.getValue(standardEvaluationContext, String.class);
}
}
该类按照上面思路中的逻辑进行开发,没有特别复杂的逻辑
为了提高性能和线程安全,对一些类加了static和ThreadLocal
结果
使用方式:
@SysLog(value = “#{‘用户登录'}”)
@SysLog(value = “#{'用户登录: method: ' + #loginRequest.username}”, printResult = true)
@SysLog(value = “#{'用户登录: method: ' + #loginRequest.username + authBizService.test()}”, printResult = true)
…
更多书写方式参考SpEL表达式即可
/**
* 用户登录接口
*
* @param loginRequest 用户登录输入参数类
* @return 返回用户登录结果输出类
*/
@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 = "用户登录参数") @RequestBody @Valid LoginRequest loginRequest
) {
// 业务代码
}
结果打印:
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
压测下来性能损耗较低(可忽略不计)
来源:https://blog.csdn.net/weixin_30865665/article/details/120044388


猜你喜欢
- 在协程启动模式中已经知道async是可以返回结果的,但是只返回一个,那么在复杂场景下就会不够用了,所以Channel就出现了。1.认识Cha
- 概念:LruCache什么是LruCache?LruCache实现原理是什么?这两个问题其实可以作为一个问题来回答,知道了什么是 LruCa
- 桶排序桶排序是计数排序的升级,计数排序可以看成每个桶只存储相同元素,而桶排序每个桶存储一定范围的元素,通过函数的某种映射关系,将待排序数组中
- 可以不用经过 Html.fromHtml 因为我的数据里面含有一点 html的标签。所以经过html转换了。 实现方法: TextView
- 基于IDEA生成可执行jar包1.编写class的代码,注意一定要有main()方法才可以生成jar包,main()方法可以没有内容。例如:
- 其实这个比较简单,子线程怎么通知主线程,就是让子线程做完了自己的事儿就去干主线程的转回去干主线程的事儿。那么怎么让子线程去做主线程的事儿呢,
- 使用java语言用集合存储数据实现学生信息管理系统,在控制台上编译执行可以实现基本的学生信息增加、删除、修改、查询功能IO版可以参考我的另外
- Collections工具类Java里关于聚合的工具类,包含有各种有关集合操作的静态多态方法,不能实例化(把构造函数私有化)public c
- 在SpringMVC的入门学习中,我发现@GetMapping注解的使用要注意路径冲突问题,在网上都没找到类似我这样的情况,所以我在这里将问
- springboot+mybatis整合过程中,开启控制台sql语句打印的多种方式:方法一1>(spring+mybatis)在myb
- 强制下线是需要关闭所有的活动,先创建一个类来管理所有的活动。class ActivityCollector { //var ac
- springboot Jpa通用接口,公共方法de 简单使用 pom文件加入jpa这是我的例子使用的依赖。jpa必须当
- 在Android Studio 2.1 Preview 3之后,官方开始支持双向绑定了。可惜目前Google并没有在Data Binding
- refresh()该方法是 Spring Bean 加载的核心,它是 ClassPathXmlApplicationContext 的父类
- java 基础之final、finally和finalize的区别1.final可以修饰类,不能被继承;可以修饰方法,不能被重写;可以修饰变
- 目录不含return的执行顺序finally子句含return的执行顺序返回类型是对象类型时值的变化结论不含return的执行顺序执行顺序为
- 这篇文章主要介绍了springboot日期转换器实现实例解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需
- 本文实例为大家分享了Java实现学生管理系统的具体代码,供大家参考,具体内容如下package BookDemo_1; import jav
- 1.1 JDK 14详细概述JDK 8 已经在 2014年 3月 18日正式可用,JDK 8作为长期支持(Long-Term-Support
- 本文实例讲述了C#实现的MD5加密功能与用法。分享给大家供大家参考,具体如下:1、创建MD5Str.cs加密处理类public class