基于Redis实现的日志记录组件——超实用(4)AOP和SPEL实现

收录于墨的2020~2021开发经验总结

上一篇 基于Redis实现的日志记录组件——超实用(3)核心类描述

四、AOP实现

以下是日志记录的核心切面代码:

Slf4j
@Aspect
@Order(0)   // 越小越先执行
public class RedisLogOptAspect {

    @Resource
    private RedisLogSpelHandler redisLogSpelHandler;

    @Resource
    private RedisLogService redisLogService;

    @Pointcut("@annotation(cn.hengyumo.dawn.core.log.redisLogger.RedisLogOpt)")
    public void redisLogOptPointCut(){}

    @Around("redisLogOptPointCut()")
    public Object aroundRedisLogOptPointCur(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        Throwable exception = null;
        Object result = null;
        long start = System.currentTimeMillis();
        long timeUseMillis;
        try {
            result = proceedingJoinPoint.proceed();
        } catch (Throwable throwable) {
            exception = throwable;
        } finally {
            timeUseMillis = System.currentTimeMillis() - start;
        }
        try {
            MethodSignature methodSignature = (MethodSignature) proceedingJoinPoint.getSignature();
            Object target = proceedingJoinPoint.getTarget();
            Method method = methodSignature.getMethod();
            RedisLogOpt redisLogOpt = AnnotationUtils.getAnnotation(method, RedisLogOpt.class);
            Assert.notNull(redisLogOpt, "never null");
            String component = redisLogOpt.component();
            if (redisLogService.checkComponent(component)) {
                RedisLogComponent redisLogComponent = redisLogService.getComponent(component);
                String opt = redisLogOpt.opt();
                String spel = redisLogOpt.spel();
                String description = redisLogOpt.description();
                HttpServletRequest request = getRequest();
                HttpServletResponse response = getResponse();
                Assert.notNull(request, "request is null");
                Assert.notNull(response, "response is null");
                Map<String, Object> params = getParams(proceedingJoinPoint);
                if (StringUtils.hasLength(spel)) {
                    // spel 优先
                    EvaluationContext context = redisLogSpelHandler.buildContext(
                            redisLogComponent,
                            redisLogOpt,
                            request,
                            response,
                            target,
                            method,
                            params,
                            result,
                            timeUseMillis,
                            exception
                    );
                    description = redisLogSpelHandler.parseLogSpel(context, spel);
                }
                // 加入通用前缀
                String pre = redisLogService.createNormalLogPre(request, response, timeUseMillis);
                redisLogService.log(component, opt, pre + description);
            } else {
                log.warn("找不到RedisLoggerComponent:{},请先注册", component);
            }
        } catch (Throwable throwable) {
            log.error("RedisLog 日志记录失败!");
            throwable.printStackTrace();
        }
        if (exception != null) {
            throw exception;
        }
        return result;
    }

    /**
     * 获取当前请求
     *
     * @return 当前请求
     */
    private HttpServletRequest getRequest() {
        RequestAttributes ra = RequestContextHolder.getRequestAttributes();
        ServletRequestAttributes sra = (ServletRequestAttributes) ra;
        if (sra != null) {
            return sra.getRequest();
        }
        return null;
    }

    /**
     * 获取当前响应
     *
     * @return 当前请求
     */
    private HttpServletResponse getResponse() {
        RequestAttributes ra = RequestContextHolder.getRequestAttributes();
        ServletRequestAttributes sra = (ServletRequestAttributes) ra;
        if (sra != null) {
            return sra.getResponse();
        }
        return null;
    }

    /**
     * 构造方法的参数Map
     *
     * @param proceedingJoinPoint 过程切点
     * @return Map<String, Object>
     */
    private Map<String, Object> getParams(ProceedingJoinPoint proceedingJoinPoint) {
        Map<String, Object> map = new HashMap<>();
        MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
        String[] paramsNames = signature.getParameterNames();
        if (paramsNames != null && paramsNames.length > 0) {
            Object[] args = proceedingJoinPoint.getArgs();
            for (int i = 0; i < paramsNames.length; i++) {
                map.put(paramsNames[i], args[i]);
            }
        }
        return map;
    }
}

1、@Order(0) 是设置切面的执行顺序,当多个切面应用到同一个方法时。Order顺序小的优先被包围在方法的外层。日志记录应该在方法返回的最后一个执行。故而Order的值要小于其它切面的值。
2、当同时声明日志内容和spel表达式时,原则是spel表达式优先。
3、AOP拦截时,同时还会获取其它重要信息,如请求内容,响应内容,请求耗时,目标方法,目标Bean等,这些参数可以用于SPEL表达式生成日志信息。
4、AOP不主动实现具体日志记录,而是通过调用redisLogService里的方法。

五、SPEL表达式实现

在AOP中截取了程序运行的一系列重要信息,通过SPEL表达式既可以利用这些信息来生成日志,以下是spel表达式处理的核心代码:

@Slf4j
@Component
public class RedisLogSpelHandler {

    public EvaluationContext buildContext(RedisLogComponent redisLogComponent,
                                          RedisLogOpt redisLogOpt,
                                          HttpServletRequest request,
                                          HttpServletResponse response,
                                          Object target,
                                          Method method,
                                          Map<String, Object> params,
                                          Object result,
                                          long timeUseMillis,
                                          Throwable throwable) {
        EvaluationContext context = new StandardEvaluationContext();
        context.setVariable("request", request);
        context.setVariable("url", request.getRequestURL());
        context.setVariable("httpMethod", request.getMethod());
        context.setVariable("ip", request.getRemoteAddr());
        context.setVariable("userAgent", request.getHeader(HttpHeaders.USER_AGENT));
        context.setVariable("cookie", request.getHeader(HttpHeaders.COOKIE));
        context.setVariable("contentType", request.getHeader(HttpHeaders.CONTENT_TYPE));
        context.setVariable("session", request.getSession());

        context.setVariable("user", null);
        context.setVariable("userName", null);
        context.setVariable("userId", null);

        context.setVariable("this", target);
        context.setVariable("method", method);
        context.setVariable("redisLogOpt", redisLogOpt);
        context.setVariable("redisLogComponent", redisLogComponent);
        context.setVariable("params", params);
        context.setVariable("time", DateUtil.getDateTime(Calendar.getInstance().getTime()));

        context.setVariable("response", response);
        context.setVariable("status", response.getStatus());

        context.setVariable("result", result);
        context.setVariable("timeUseMillis", timeUseMillis);

        context.setVariable("throwable", throwable);

        return context;
    }

    // 111.7.96.181 - - [23/Jun/2021:20:34:09 +0800] "HEAD / HTTP/1.1" 301 0 "-" "Chrome/54.0 (Windows NT 10.0)"

    public String parseLogSpel(EvaluationContext context, String spel) {
        ExpressionParser parser = new SpelExpressionParser();
        Expression expression = parser.parseExpression(spel);
        Object obj = expression.getValue(context);
        if (obj == null) {
            log.warn("parse null: {}, context: {}", expression, context);
            return null;
        }
        return obj.toString();
    }
}

1、spel 表达式的解析,有两个过程,一个是构建上下文,buildContext的目的就是根据传入的参数构建spel语句运行的上下文。主要是将各种参数设入上下文中。后一个则是根据传入的spel语句配合上下文执行spel表达式,生成目标结果。
2、spel表达式使用简单,例如:

spel = "'查询产品' + #params.toString() + ',结果:' + #result.toString()"
spel = "'添加一个产品' + #params['product'].toString()"
spel = "'根据ID删除产品-' + #params['id']"

未完待续~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值