切面编程-记录操作日志(aop + 正则表达式)

前段时间看了极海老师的切面编程视频: https://www.bilibili.com/video/BV1oD4y1W7Lh,刚好公司的系统也需要做一个日志记录注解,所以就在此写这篇文章记录一下。

1 什么是操作日志?与系统日志有什么区别

操作日志:主要是对某个对象进行新增操作或者修改操作后记录下这个新增或者修改,操作日志要求可读性比较强,因为它主要是给用户看的,比如订单的物流信息,用户需要知道在什么时间发生了什么事情。再比如,客服对工单的处理记录信息。

操作日志的记录格式大概分为下面几种:

  • 单纯的文字记录。比如:2021-09-16 10:00 订单创建。
  • 简单的动态的文本记录。比如:2021-09-16 10:00 订单创建,订单号:NO.11089999,其中涉及变量订单号“NO.11089999”。
  • 修改类型的文本,包含修改前和修改后的值。比如:2021-09-16 10:00 用户小明修改了订单的配送地址:从“金灿灿小区”修改到“银盏盏小区” ,其中涉及变量配送的原地址“金灿灿小区”和新地址“银盏盏小区”。

系统日志:系统日志主要是为开发排查问题提供依据,一般打印在日志文件中;系统日志的可读性要求没那么高,日志中会包含代码的信息,比如在某个类的某一行打印了一个日志。

2 实现方式

我们只需要开发一个注解,并使用在Controller层,每次请求时,这个注解能拿到请求参数,并记录本次请求修改了哪些业务即可。这样不仅对原有的业务代码侵入性少,还满足开闭原则。

3 如何获取参数?

3.1 使用转换类

具体实现方式参见极海老师的视频:https://www.bilibili.com/video/BV1oD4y1W7Lh
优点:灵活,因为是手写转化类,所以参数实例内的每个成员变量都可以获取到,比如:类套类、类中套类数组等复杂的参数类。
缺点:每个参数实例都需要手写一个转化类。

3.2 使用SpEL表达式

具体实现方式参见这篇博文:https://www.cnblogs.com/linyb-geek/p/14628180.html
优点:Spring原生自带SpEL表达式
缺点:解析耗费cpu的计算资源;一些复杂的参数类就没办获取里面的成员变量

3.3 使用正则表达式

本次要介绍的方式
优点:简单,并可以截取到参数类型为 JSONObject 的成员变量。
缺点:有一定特征的参数才行,比如本系统的订单号是:SH20230227123456,特征是:SH开头,8位数字表示当前日期,最后6位随机数。
正则表达式性能优化方式:https://www.jianshu.com/p/f313746119ad

4 实例代码

Spring.xml

	
	<aop:aspectj-autoproxy proxy-target-class="true">
        <aop:include name="TMSLogInDbAspect"/>
    aop:aspectj-autoproxy>
    
    <aop:config expose-proxy="true">
        <aop:aspect ref="TMSLogInDbAspect">
        	
            <aop:pointcut id="process" expression="@annotation(com.hand.htms.base.product.order.anno.TMSLogInDb)"/>
            
            <aop:around method="doLogRecord" pointcut-ref="process"/>
        aop:aspect>
    aop:config>
    
    <bean id="TMSLogInDbAspect" class="com.hand.htms.base.product.order.aspect.TMSLogInDbAspect">bean>

注解:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface TMSLogInDb {

    /**
     * 前端传入参数的索引,默认是0,第一个
     */
    int argIndex() default 0;

    /**
     * 日志备注,必填项
     */
    String remark();
}

横切逻辑类:

public class TMSLogInDbAspect {

    /**
     * 在remark中获取订单号的标志
     */
    private static final String SHIPMENTXID_FLAG = "${shipmentXid}";

    private static final Pattern SHIPMENTXID_PATTERN = Pattern.compile("[S][H](\\d{4})(\\d{2})(\\d{2})(\\d{6})+");

    @Autowired
    private ThreadPoolTaskExecutor poolTaskExecutor;

    /**
     * 横切逻辑
     *
     * @param proceedingJoinPoint
     * @throws Throwable
     */
    public ResponseData doLogRecord(ProceedingJoinPoint proceedingJoinPoint) {
        //获取目标方法的参数
        Object[] args = proceedingJoinPoint.getArgs();

        //通过反射获取目标方法摘要
        MethodSignature methodSignature = (MethodSignature) proceedingJoinPoint.getSignature();

        //获取目标方法
        Method method = methodSignature.getMethod();
        //获取目标方法的注解
        TMSLogInDb annotation = method.getAnnotation(TMSLogInDb.class);
        int index = annotation.argIndex();
        String remark = annotation.remark();

        //获取userId以知道操作人。principal无法强转为User类,只能这样拿
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        Object principal = authentication.getPrincipal();
        JSONObject principalObject = JSONUtil.parseObj(principal);
        Long userId = Long.parseLong(principalObject.getStr("userId"));

        ResponseData result = new ResponseData();

        try {
            //执行目标方法
            result = (ResponseData) proceedingJoinPoint.proceed();

            //异步存入数据库,只有成功才添加,若目标方法出错,则不添加
            if (result.isSuccess()) {
                poolTaskExecutor.execute(() -> {
           			//Convert使用的是hutool包下的类
                    List argList = Convert.convert(List.class, args[index]);
                    for (Object arg : argList) {
                        String argStr = arg.toString();
                        String shipmentXid = this.getValueByReg(SHIPMENTXID_PATTERN, argStr);
                        String finalRemark = this.decorateRemark(remark, shipmentXid)
						//todo  
						//操作日志表:存入操作人:userId,订单号:shipmentXid,日志记录:finalRemark 
                    }
                });
            }
        } catch (Throwable e) {
            e.printStackTrace();
        }

        return result;
    }

    /**
     * 通过正则获取值
     *
     * @param pattern 正则pattern
     * @param content 内容
     * @return
     */
    private String getValueByReg(Pattern pattern,String content) {
        Matcher m = pattern.matcher(content);
        if (m.find()) {
            return m.group(0);
        } else {
            return "";
        }
    }
    /**
     * remark修饰,将备注中带有${shipmentXid}的标志做替换
     *
     * @param remark
     * @param shipmentXid
     * @return
     */
    private String decorateRemark(String remark, String shipmentXid) {
        return remark.replace(SHIPMENTXID_FLAG, shipmentXid);
    }
}

使用实例:

	@RequestMapping(value = "/addMovementsToShipment")
    @ResponseBody
    @TMSLogInDb(remark = "创建了产品,并添加到${shipmentXid}中")
    public ResponseData addMovementsToShipment(@RequestBody JSONObject json,
                                               HttpServletRequest request) {
    //.....
    }

5 参考

极海-切面编程教学:https://www.bilibili.com/video/BV1oD4y1W7Lh
Spring Aop 面向切面编程:https://blog.csdn.net/pingzhuyan/article/details/126332270
Aop+Spel编程实战:https://www.cnblogs.com/linyb-geek/p/14628180.html
Spel官方文档:https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#expressions-collection-projection
美团技术团队-如何优雅地记录操作日志:https://mp.weixin.qq.com/s/JC51S_bI02npm4CE5NEEow

你可能感兴趣的:(正则表达式,spring,java)