zoukankan      html  css  js  c++  java
  • Spring aop+自定义注解统一记录用户行为日志

    写在前面

    本文不涉及过多的Spring aop基本概念以及基本用法介绍,以实际场景使用为主。

    场景

    我们通常有这样一个需求:打印后台接口请求的具体参数,打印接口请求的最终响应结果,以及记录哪个用户在什么时间点,访问了哪些接口,接口响应耗时多长时间等等。这样做的目的是为了记录用户的访问行为,同时便于跟踪接口调用情况,以便于出现问题时能够快速定位问题所在。

    最简单的做法是这样的:

    1    @GetMapping(value = "/info")
    2    public BaseResult userInfo() {
    3        //1.打印接口入参日志信息,标记接口访问时间戳
    4        BaseResult result = mUserService.userInfo();
    5        //2.打印/入库 接口响应信息,响应时间等
    6        return result;
    7    }

    这种做法没毛病,但是稍微比较敏感的同学就会发觉有以下缺点:

    • 每个接口都充斥着重复的代码,有没有办法提取这部分代码,做到统一管理呢?答案是使用 Spring aop 面向切面执行这段公共代码。
    • 充斥着 硬编码 的味道,有些场景会要求在接口响应结束后,打印日志信息,保存到数据库,甚至要把日志记录到elk日志系统等待,同时这些操作要做到可控,有没有什么操作可以直接声明即可?答案是使用自定义注解,声明式的处理访问日志。

    自定义注解

    新增日志注解类,注解作用于方法级别,运行时起作用。

     1@Target({ElementType.METHOD}) //注解作用于方法级别
    2@Retention(RetentionPolicy.RUNTIME) //运行时起作用
    3public @interface Loggable {
    4
    5    /**
    6     * 是否输出日志
    7     */

    8    boolean loggable() default true;
    9
    10    /**
    11     * 日志信息描述,可以记录该方法的作用等信息。
    12     */

    13    String descp() default "";
    14
    15    /**
    16     * 日志类型,可能存在多种接口类型都需要记录日志,比如dubbo接口,web接口
    17     */

    18    LogTypeEnum type() default LogTypeEnum.WEB;
    19
    20    /**
    21     * 日志等级
    22     */

    23    String level() default "INFO";
    24
    25    /**
    26     * 日志输出范围,用于标记需要记录的日志信息范围,包含入参、返回值等。
    27     * ALL-入参和出参, BEFORE-入参, AFTER-出参
    28     */

    29    LogScopeEnum scope() default LogScopeEnum.ALL;
    30
    31    /**
    32     * 入参输出范围,值为入参变量名,多个则逗号分割。不为空时,入参日志仅打印include中的变量
    33     */

    34    String include() default "";
    35
    36    /**
    37     * 是否存入数据库
    38     */

    39    boolean db() default true;
    40
    41    /**
    42     * 是否输出到控制台
    43     *
    44     * @return
    45     */

    46    boolean console() default true;
    47}

    日志类型枚举类:

     1public enum LogTypeEnum {
    2
    3    WEB("-1"), DUBBO("1"), MQ("2");
    4
    5    private final String value;
    6
    7    LogTypeEnum(String value) {
    8        this.value = value;
    9    }
    10
    11    public String value() {
    12        return this.value;
    13    }
    14}

    日志作用范围枚举类:

     1public enum LogScopeEnum {
    2
    3    ALL, BEFORE, AFTER;
    4
    5    public boolean contains(LogScopeEnum scope) {
    6        if (this == ALL) {
    7            return true;
    8        } else {
    9            return this == scope;
    10        }
    11    }
    12
    13    @Override
    14    public String toString() {
    15        String str = "";
    16        switch (this) {
    17            case ALL:
    18                break;
    19            case BEFORE:
    20                str = "REQUEST";
    21                break;
    22            case AFTER:
    23                str = "RESPONSE";
    24                break;
    25            default:
    26                break;
    27        }
    28        return str;
    29    }
    30}

    相关说明已在代码中注释,这里不再说明。

    使用 Spring aop 重构

    引入依赖:

     1    <dependency>
    2            <groupId>org.aspectj</groupId>
    3            <artifactId>aspectjweaver</artifactId>
    4            <version>1.8.8</version>
    5        </dependency>
    6        <dependency>
    7            <groupId>org.aspectj</groupId>
    8            <artifactId>aspectjrt</artifactId>
    9            <version>1.8.13</version>
    10        </dependency>
    11        <dependency>
    12            <groupId>org.javassist</groupId>
    13            <artifactId>javassist</artifactId>
    14            <version>3.22.0-GA</version>
    15    </dependency>

    配置文件启动aop注解,基于类的代理,并且在 spring 中注入 aop 实现类。

     1<?xml version="1.0" encoding="UTF-8"?>
    2<beans xmlns="http://www.springframework.org/schema/beans"
    3    .....省略部分代码">
    4
    5    <!-- 扫描controller -->
    6    <context:component-scan base-package="
    **.*controller"/>
    7    <context:annotation-config/>
    8
    9    <!-- 启动aop注解基于类的代理(这时需要cglib库),如果proxy-target-class属值被设置为false或者这个属性被省略,那么标准的JDK 基于接口的代理将起作用 -->
    10    <aop:config proxy-target-class="
    true"/>
    11
    12     <!-- web层日志记录AOP实现 -->
    13    <bean class="
    com.easywits.common.aspect.WebLogAspect"/>
    14</beans>
    15

    新增 WebLogAspect 类实现

      1/**
    2 * 日志记录AOP实现
    3 * create by zhangshaolin on 2018/5/1
    4 */

    5@Aspect
    6@Component
    7public class WebLogAspect {
    8
    9    private static final Logger LOGGER = LoggerFactory.getLogger(WebLogAspect.class);
    10
    11    // 开始时间
    12    private long startTime = 0L;
    13
    14    // 结束时间
    15    private long endTime = 0L;
    16
    17    /**
    18     * Controller层切点
    19     */

    20    @Pointcut("execution(* *..controller..*.*(..))")
    21    public void controllerAspect() {
    22    }
    23
    24    /**
    25     * 前置通知 用于拦截Controller层记录用户的操作
    26     *
    27     * @param joinPoint 切点
    28     */

    29    @Before("controllerAspect()")
    30    public void doBeforeInServiceLayer(JoinPoint joinPoint) {
    31    }
    32
    33    /**
    34     * 配置controller环绕通知,使用在方法aspect()上注册的切入点
    35     *
    36     * @param point 切点
    37     * @return
    38     * @throws Throwable
    39     */

    40    @Around("controllerAspect()")
    41    public Object doAround(ProceedingJoinPoint point) throws Throwable {
    42        // 获取request
    43        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
    44        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;
    45        HttpServletRequest request = servletRequestAttributes.getRequest();
    46
    47        //目标方法实体
    48        Method method = ((MethodSignature) point.getSignature()).getMethod();
    49        boolean hasMethodLogAnno = method
    50                .isAnnotationPresent(Loggable.class);
    51        //没加注解 直接执行返回结果
    52        if (!hasMethodLogAnno) {
    53            return point.proceed();
    54        }
    55
    56        //日志打印外部开关默认关闭
    57        String logSwitch = StringUtils.equals(RedisUtil.get(BaseConstants.CACHE_WEB_LOG_SWITCH), BaseConstants.YES) ? BaseConstants.YES : BaseConstants.NO;
    58
    59        //记录日志信息
    60        LogMessage logMessage = new LogMessage();
    61
    62        //方法注解实体
    63        Loggable methodLogAnnon = method.getAnnotation(Loggable.class);
    64
    65        //处理入参日志
    66        handleRequstLog(point, methodLogAnnon, request, logMessage, logSwitch);
    67
    68        //执行目标方法内容,获取执行结果
    69        Object result = point.proceed();
    70
    71        //处理接口响应日志
    72        handleResponseLog(logSwitch, logMessage, methodLogAnnon, result);
    73        return result;
    74    }
    75
    76    /**
    77     * 处理入参日志
    78     *
    79     * @param point           切点
    80     * @param methodLogAnnon  日志注解
    81     * @param logMessage      日志信息记录实体
    82     */

    83    private void handleRequstLog(ProceedingJoinPoint point, Loggable methodLogAnnon, HttpServletRequest request,
    84                                 LogMessage logMessage, String logSwitch)
     throws Exception 
    {
    85
    86        String paramsText = "";
    87        //参数列表
    88        String includeParam = methodLogAnnon.include();
    89        Map<String, Object> methodParamNames = getMethodParamNames(
    90                point.getTarget().getClass(), point.getSignature().getName(), includeParam);
    91        Map<String, Object> params = getArgsMap(
    92                point, methodParamNames);
    93        if (params != null) {
    94            //序列化参数列表
    95            paramsText = JSON.toJSONString(params);
    96        }
    97        logMessage.setParameter(paramsText);
    98        //判断是否输出日志
    99        if (methodLogAnnon.loggable()
    100                && methodLogAnnon.scope().contains(LogScopeEnum.BEFORE)
    101                && methodLogAnnon.console()
    102                && StringUtils.equals(logSwitch, BaseConstants.YES)) {
    103            //打印入参日志
    104            LOGGER.info("【{}】 接口入参成功!, 方法名称:【{}】, 请求参数:【{}】", methodLogAnnon.descp().toString(), point.getSignature().getName(), paramsText);
    105        }
    106        startTime = System.currentTimeMillis();
    107        //接口描述
    108        logMessage.setDescription(methodLogAnnon.descp().toString());
    109
    110        //...省略部分构造logMessage信息代码
    111    }
    112
    113    /**
    114     * 处理响应日志
    115     *
    116     * @param logSwitch         外部日志开关,用于外部动态开启日志打印
    117     * @param logMessage        日志记录信息实体
    118     * @param methodLogAnnon    日志注解实体
    119     * @param result           接口执行结果
    120     */

    121    private void handleResponseLog(String logSwitch, LogMessage logMessage, Loggable methodLogAnnon, Object result) {
    122        endTime = System.currentTimeMillis();
    123        //结束时间
    124        logMessage.setEndTime(DateUtils.getNowDate());
    125        //消耗时间
    126        logMessage.setSpendTime(endTime - startTime);
    127        //是否输出日志
    128        if (methodLogAnnon.loggable()
    129                && methodLogAnnon.scope().contains(LogScopeEnum.AFTER)) {
    130            //判断是否入库
    131            if (methodLogAnnon.db()) {
    132                //...省略入库代码
    133            }
    134            //判断是否输出到控制台
    135            if (methodLogAnnon.console() 
    136                    && StringUtils.equals(logSwitch, BaseConstants.YES)) {
    137                //...省略打印日志代码
    138            }
    139        }
    140    }
    141    /**
    142     * 获取方法入参变量名
    143     *
    144     * @param cls        触发的类
    145     * @param methodName 触发的方法名
    146     * @param include    需要打印的变量名
    147     * @return
    148     * @throws Exception
    149     */

    150    private Map<String, Object> getMethodParamNames(Class cls,
    151                                                    String methodName, String include)
     throws Exception 
    {
    152        ClassPool pool = ClassPool.getDefault();
    153        pool.insertClassPath(new ClassClassPath(cls));
    154        CtMethod cm = pool.get(cls.getName()).getDeclaredMethod(methodName);
    155        LocalVariableAttribute attr = (LocalVariableAttribute) cm
    156                .getMethodInfo().getCodeAttribute()
    157                .getAttribute(LocalVariableAttribute.tag);
    158
    159        if (attr == null) {
    160            throw new Exception("attr is null");
    161        } else {
    162            Map<String, Object> paramNames = new HashMap<>();
    163            int paramNamesLen = cm.getParameterTypes().length;
    164            int pos = Modifier.isStatic(cm.getModifiers()) ? 0 : 1;
    165            if (StringUtils.isEmpty(include)) {
    166                for (int i = 0; i < paramNamesLen; i++) {
    167                    paramNames.put(attr.variableName(i + pos), i);
    168                }
    169            } else { // 若include不为空
    170                for (int i = 0; i < paramNamesLen; i++) {
    171                    String paramName = attr.variableName(i + pos);
    172                    if (include.indexOf(paramName) > -1) {
    173                        paramNames.put(paramName, i);
    174                    }
    175                }
    176            }
    177            return paramNames;
    178        }
    179    }
    180
    181    /**
    182     * 组装入参Map
    183     *
    184     * @param point       切点
    185     * @param methodParamNames 参数名称集合
    186     * @return
    187     */

    188    private Map getArgsMap(ProceedingJoinPoint point,
    189                           Map<String, Object> methodParamNames)
     
    {
    190        Object[] args = point.getArgs();
    191        if (null == methodParamNames) {
    192            return Collections.EMPTY_MAP;
    193        }
    194        for (Map.Entry<String, Object> entry : methodParamNames.entrySet()) {
    195            int index = Integer.valueOf(String.valueOf(entry.getValue()));
    196            if (args != null && args.length > 0) {
    197                Object arg = (null == args[index] ? "" : args[index]);
    198                methodParamNames.put(entry.getKey(), arg);
    199            }
    200        }
    201        return methodParamNames;
    202    }
    203}

    使用注解的方式处理接口日志

    接口改造如下:

    1    @Loggable(descp = "用户个人资料", include = "")
    2    @GetMapping(value = "/info")
    3    public BaseResult userInfo() {
    4        return mUserService.userInfo();
    5    }

    可以看到,只添加了注解@Loggable,所有的web层接口只需要添加@Loggable注解就能实现日志处理了,方便简洁!最终效果如下:

    访问入参,响应日志信息:

    用户行为日志入库部分信息:

    简单总结

    • 编写代码时,看到重复性代码应当立即重构,杜绝重复代码。
    • Spring aop 可以在方法执行前,执行时,执行后切入执行一段公共代码,非常适合用于公共逻辑处理。
    • 自定义注解,声明一种行为,使配置简化,代码层面更加简洁。

    最后

    更多原创文章会第一时间推送公众号【张少林同学】,欢迎关注!

  • 相关阅读:
    Gradle 修改 Maven 仓库地址
    返回到上一页的html代码的几种写法
    Android网络传输中必用的两个加密算法:MD5 和 RSA (附java完成测试代码)
    MyBatis传入多个参数的问题
    8 个最优秀的 Android Studio 插件
    Html中隐藏a标签
    js中获取jsp中的参数
    同一个页面里的JS怎样获取jsp从别的页面获取的参数
    《Android源码设计模式解析与实战》读书笔记(一)
    HDU ACM 1068 最大独立集
  • 原文地址:https://www.cnblogs.com/zhangshaolin/p/10232832.html
Copyright © 2011-2022 走看看