文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

支持SpEL表达式的自定义日志注解@SysLog介绍

2024-04-02 19:55

关注

序言

之前封装过一个日志注解,打印方法执行信息,功能较为单一不够灵活,近来兴趣来了,想重构下,使其支持表达式语法,以应对灵活的日志打印需求。

该注解是方法层面的日志打印,如需更细的粒度,还请手撸log.xxx()。

预期

通过自定义注解,灵活的语法表达式,拦截自定义注解下的方法并打印日志

日志要支持以下内容:

思路

定义自定义注解

拦截自定义注解方法完成以下动作

特定类型表达式方案

问题:选属性解析表达式、还是SpEL表达式

属性解析表达式:

SpEL表达式:

过程

定义自定义注解@SysLog 

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SysLog {
    
    String value();
    
    String level() default "info";
    
    boolean printResult() default false;
}

该类包含以下信息:

走过的弯路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表达式方式)


@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() {
    }
    
    @SuppressWarnings("unused")
    @Before("sysLogPointCut()")
    public void doBeforeReturning(JoinPoint joinPoint) {
        // 设置请求开始时间
        START_TIME.set(System.currentTimeMillis());
    }
    
    @AfterReturning(
            pointcut = "sysLogPointCut()",
            returning = "result"
    )
    public void doAfterReturning(JoinPoint joinPoint, Object result) {
        printLog(joinPoint, result, null);
    }
    
    @AfterThrowing(
            pointcut = "sysLogPointCut()",
            throwing = "e"
    )
    public void doAfterThrowing(JoinPoint joinPoint, Exception e) {
        printLog(joinPoint, null, 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表达式即可

	
    @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

压测下来性能损耗较低(可忽略不计)

以上为个人经验,希望能给大家一个参考,也希望大家多多支持编程网。

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯