• aop实现动态日志


    一.简介

    ​ aop面向切面编程,我们可以使用它来进行方法执行之前做一些事情,也可以在方法之后做一些事情等等。

    消息通知类包括:

    1.前置通知(before):目标方法运行之前调用;

    2.最终通知(after):在目标方法运行之后调用,无论是否正常执行完成,还是抛出异常,都会执行;

    3.后置通知(after-returning):在目标方法正常执行之后执行,如果出现异常,则不会执行;

    4.异常拦截通知(after-throwing):在目标方法运行之后,并且出现异常就执行;

    5.环绕通知(around):在目标方法执行之前和之后都会调用。

    基于aop的这种特性,我们可以用它来做一些全局性的控制,此处就来聊聊使用它做全局日志的控制,并且支持动态日志的实现。

    二.具体实现过程

    1.定义切点注解

    ​ 我们可以使用注解的方式定义切点,也可以使用通配符进行配置,此处我们使用注解的方式进行配置。

    @Target({ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface LogAnnotation {
    
        //模块名称
        ModuleNameEnum moduleName() default ModuleNameEnum.UNKNOWN;
    
        //操作对象
        String operaName() default "";
    
        //操作类型
        OperaTypeEnum operaType() default OperaTypeEnum.UNKNOWN;
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    @Target:定义了此注解的作用范围,ElementType.METHOD:作用在方法上;

    @Retention:定义此注解的生命周期,RetentionPolicy.RUNTIME:注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在;

    @Documented:定义文档。

    上面我们定义了一个注解LogAnnotation,此注解有三个参数,分别用于使用注解时传递对应的日志参数值。

    2.定义枚举值

    ​ 上面的注解中我们有使用枚举值作为参数,使用枚举值的好处就是,以后对枚举值的修改,只用修改枚举类就行,不用逐个去改引用此值的地方。

    @AllArgsConstructor
    @ToString
    public enum ModuleNameEnum implements Value<String> {
        ManageUser("人员管理"),
        SysUser("用户管理"),
        FeedBack("信息管理"),
        Publish("发布管理"),
        UNKNOWN("未定义");
    
        @JsonValue
        @Getter
        private final String value;
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    @AllArgsConstructor
    @ToString
    public enum OperaTypeEnum implements Value<String> {
    
        Insert("新增"),
        Update("更新"),
        Delete("删除"),
        UNKNOWN("未定义");
    
        @JsonValue
        @Getter
        private final String value;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    public interface Value<T> {
        T getValue();
    }
    
    • 1
    • 2
    • 3

    @AllArgsConstructor:使用注解生成类的构造函数;

    @ToString:使用注解生成类的tostring方法;

    @JsonValue:对于枚举值的格式定义;

    @Getter:对枚举值添加获取具体值的方法。

    3.定义日志切面类

    ​ 定义一个日志切面类,对方法进行拦截,对操作进行日志保存。

    //@Aspect:标识当前类为一个切面,在spring容器加载的时候,会扫描到它
    //@Component:把普通pojo实例化到spring容器中,类似于xml配置时的
    //@Slf4j:定义日志
    @Aspect
    @Component
    @Slf4j
    public class LogAspect {
    
        //注入日志service类
        @Autowired
        XXXLogService xxxLogService;
    
    
        //节点:使用LogAnnotation注解标识的方式都进行切入,也可以使用通配符配置具体要接入的方法名
        @Pointcut("@annotation(xxx.aop.LogAnnotation)")
        public void pointCut(){
    
        }
    
        //环绕通知,
        @Around("pointCut()")
        public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
            //方法执行之后的结果值
            Object jsonResult = joinPoint.proceed();
            try {
                //获取请求结果值
                MethodSignature signature = (MethodSignature)joinPoint.getSignature();
                //获取切入点所在的方法
                Method method = signature.getMethod();
                //获取到注解
                LogAnnotation annotation = method.getAnnotation(LogAnnotation.class);
                //获取注解里面字段的值
                String moduleName = annotation.moduleName().getValue();
                String operaName = annotation.operaName();
                String operaType = annotation.operaType().getValue();
                //获取到方法调用时传递的参数名
                String[] parameterNames = signature.getParameterNames();
                //获取到方法调用时各个参数的值
                Object[] args = joinPoint.getArgs();
                //使用替代的方式,获取动态日志
                operaName = getOperaName(parameterNames,args,operaName);
                //创建日志实体类
                XxxLog xxxLog = new XxxLog();
                xxxLog.setModuleName(moduleName);
                xxxLog.setOperaName(operaName);
                xxxLog.setOperaType(operaType);
                //获取登录用户信息,可以根据自己业务系统的方式进行获取
                xxxLog.setCreateUserId(StpUtil.getLoginIdAsString());
                //调用保存日志的方法
                xxxLogService.insertOne(xxxLog);
            } catch (Exception e){
                e.printStackTrace();
            } catch (Throwable e) {
                e.printStackTrace();
            }
            //返回方法执行之后的结果值
            return jsonResult;
        }
    
        /**
         * @Description: 使用替代的方式实现动态日志
         * @Author: zhanglizeng
         * @Param: [parameterNames, args, operaName]
         * @Return: java.lang.String
         */
        private String getOperaName(String[] argNames, Object[] args, String operaName) {
            //先把所有的参数进行遍历,作为替代的key,参数值作为替代的value
            Map<Object, Object> map = new HashMap<Object, Object>();
            for(int i = 0;i < argNames.length;i++){
                //参数和参数值是一一对应的,可以这样进行设置值
                map.put(argNames[i],args[i]);
            }
            try {
                //此处我们使用了operaName的值包含参数key的字符方式,例operaName="添加用户{{name}}"
                //而name作为方法调用时传递的一个参数名,在argNames[]数组中可以取到它,
                //name的参数值在args[]数组中可以取到它,然后我们使用字符替代的方式即可把operaName的值变为operaName="添加用户张三"
                for (Map.Entry<Object, Object> entry : map.entrySet()) {
                    Object k = entry.getKey();
                    Object v = entry.getValue();
                    operaName = operaName.replace("{{" + k + "}}", JSONObject.toJSONString(v));
                }
            }catch (Exception e){
                e.printStackTrace();
            }
            return operaName;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87

    @Aspect:标识当前类为一个切面,在spring容器加载的时候,会扫描到它;
    @Component:把普通pojo实例化到spring容器中,类似于xml配置时的;
    @Slf4j:定义日志;

    @Pointcut:定义切点;

    @annotation(xxx.aop.LogAnnotation):定义切点的入口是注解,并且指明注解的目录,此处也可以使用通配符的方式配置切点的入口,此处我们使用注解的方式;

    @Around(“pointCut()”):环绕通知,环绕的方法是上面切点的方法;

    4.方法中配置注解

    我们只需要在需要进行日志记录的方法上,添加我们的注解即可进行日志的添加。

        @LogAnnotation(moduleName= ModuleNameEnum.ManageUser,operaName="添加用户{{userName}}",operaType=OperaTypeEnum.Insert)
        public XxxUser addUser(String currentUserId,String userName) {
            XxxUser xxxUser = new XxxUser();
            xxxUser.setUserName(userName);
            xxxUserDao.addUser(xxxUser);
            return xxxUser;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    @LogAnnotation:使用注解标识调用此方法时需要执行aop的切面;可以在此处设置注解中字段的值,我们使用{{}}这样的特殊符号来作为替代字符的标识;注意{{}}里面的值需要与参数名保持一致,这样在后续替代的时候才能匹配的上。例如此处传递的userName值为张三,则替代后的operaName值为:添加用户张三。
    执行结果截图:
    在这里插入图片描述

    5.调用流程

    在这里插入图片描述

  • 相关阅读:
    springMVC之JSON和全局异常处理
    【js】 lodash命名转换和封装
    仿牛客网项目---用户注册登录功能的实现
    IOS 证书更新
    麒麟信安携手河南IT联盟召开 《麒麟信安信创应用解决方案》线上分享会
    java版Spring Cloud+Mybatis+Oauth2+分布式+微服务+实现工程管理系统
    2022_08_05__106期__栈和队列
    C++ 如何将一个vector内容赋值给另一个vector?(注意:用等号赋值,有坑!过了生命周期就不行了)
    【PyQt】在PyQt5的界面上集成matplotlib绘制的图像
    一文掌握CodiMD安装与使用
  • 原文地址:https://blog.csdn.net/ZHANGLIZENG/article/details/126414237