1 业务日志的标准

业务日志,也叫操作日志。主要功能: 记录用户行为,方便业务数据回溯与统计。

1.1 操作日志记录内容

操作日志记录主要内容:用户是谁,在什么时间,对什么数据,做了什么样的更改。
逻辑中必须增加业务日志的位置:
(1)业务数据的变更处(新增、修改、删除)
(2)特别分支条件、边界条件处。

1.2 业务常见的日志记录形式

● 动态的文本记录,比如:2022-03-12 10:00 订单创建,创建用户“小新”,订单号:NO.123456 ”。
● 修改类型的文本,包含修改前和修改后的值,比如:2021-03-12 11:00 用户“小新”修改了订单收货人:“小新”修改成“小花” 。

public Result applyOrder(orderRequest request){
    // 业务逻辑blabla...
    OperateLogModel operateLogModel = new  OperateLogModel();
    operateLogModel.setOperateIp();          //1、业务操作IP地址
    operateLogModel.serO[erateMis();         //2、操作人mis
    operateLogModel.setOperateDegist();      //3、业务操作具体描述           
    operateLogModel.setUserOperateType();    //4、权限操作类型; 0:增加 1:删除 2:修改
    operateLogModel.setCreateTime();         //5、操作日志生成时间 
}

2 背景知识

AOP(Aspect-Oriented Programming)中文意思是面向切面编程。通过运行期的动态代理,实现在不修改源代码的情况下给程序动态添加功能的编程范式。可以对业务逻辑的各个模块进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

2.1 AOP实现的四个关键元素

切面(Aspect):用于封装通用部分的组件(或者模块),比如日志组件。让日志模块可以被切入到其他目标业务方法上。
连接点(JointPoint):程序执行的某个特定位置,是抽象概念,不涉及代码实现。(Spring仅支持方法的连接点,即仅能在方法调用前、方法调用后、方法抛出异常时以及方法调用前后这些程序执行点织入增强)
切入点(PointCut):用于指定哪些组件方法调用方面(共通)处理, 切点相当于查询条件,连接点相当于记录,一个切点可以匹配多个连接点
通知(Advice):常被称为“增强”,满足切入点的一段执行代码。Spring的AOP,会将 advice 模拟为一个拦截器(interceptor),并且在 join point 上维护多个 advice,进行层层拦截。
前置通知(Before):在目标方法调用前调用通知功能;
后置通知(After):在目标方法调用之后调用通知功能,不关心方法的返回结果;
返回通知(AfterReturning):在目标方法成功执行之后调用通知功能;
异常通知(AfterThrowing):在目标方法抛出异常后调用通知功能;
环绕通知(Around):通知方法会将目标方法封装起来,在目标方法调用之前和之后执行自定义的行为。

2.2 切面业务日志的核心实现流程

  1. 建立日志拦截器,自定义模板
  2. 创建日志处理切面
  3. 在业务接口(controller)方法上增加日志注解。

3 切面日志的实现

3.1 创建日志拦截器

修饰符 @interface 注解名 {
    属性类型 属性名() default 默认值;
}

在实际工程中,首先建立以下日志注解接口,作为业务日志拦截器。

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

    /**
     * 用户操作类型
     * @return
     */
    String operateType();
}

3.2 统一日志处理切面

     从切面的建设来说,通常可能覆盖: 1. 业务服务层 2. 数据持久层 3. 中间件访问层 4. 远程rpc/http服务层 5. 工具层

其中,1是建议建设的一层,业务服务是所有出入口都经过的一层,通常即@Service 2,3,4其实都可以认为是第三方依赖层,建设这一层日志有助于更细级别的追踪,因为通常业务服务可能会组合多个操作 5层暂定是一些工具类、转换类等,需要有统一的特征(可以约定)来通过切面拦截

@Slf4j
@Aspect
@Component
public class LogAspect {

    /**
     * 定义切面
     */
    @Pointcut("@annotation(com.gitee.theskyone.bird.LogAspect.LogApi)")
    public void logPointcut() {
        throw new UnsupportedOperationException();
    }

    @Around("logPointcut()")
    public Object handleAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        //获取当前请求对象,记录请求信息
        Object[] args = proceedingJoinPoint.getArgs();
        Object result = proceedingJoinPoint.proceed();
        //获取方法连接点的相关信息
        MethodSignature methodSignature = (MethodSignature)proceedingJoinPoint.getSignature();
        String methodName = methodSignature.getName();
        Method method = methodSignature.getMethod();
        LogApi logApi =  method.getAnnotation(LogApi.class);
        log.info("用户操作类型:  {}", logApi.operateType());
        log.info("请求方法      : 【{}】", methodName );
        log.info("请求参数      :  {}", args);
        log.info("返回结果      :  {}", Objects.isNull(result) ? "" : result);
        log.info("方法执行总耗时 :  {} ms", System.currentTimeMillis() - start);
        return result;
    }

    /**
     * 日志记录(实际工程中可改成异步日志持久化)
     * @param joinPoint
     * @param userName
     * @return
     */
    ServiceLog saveLog(ProceedingJoinPoint joinPoint, String userName) {
        return ServiceLog.builder()
            .traceId(UUID.randomUUID().toString())
            .createTime(LocalDateTime.now())
            //  暂只支持类名.方法名方式
            .operation(joinPoint.getSignature().toShortString())
            .operator(userName)
            .build();
    }
}

3.3 日志注解使用于业务

在项目中做自定义日志切面,业务中以注解方式记录。
(1)在进入 Controller 方法前,打印出请求参数、调用的方法名。
(2) 在方法逻辑执行后,打印出结果以及耗时。

@RestController
@RequestMapping("/log")
public class LogController  extends HandlerInterceptorAdapter {
    //LoggerFactory是slf4j的日志对象工程
    private Logger logger = LoggerFactory.getLogger(this.getClass());

    @LogApi(operateType = "create")
    @RequestMapping(value = "/userOperation", method = RequestMethod.GET)
    public List<String> getUserInfo(@RequestParam(value = "userName") String userName,
        @RequestParam(value = "orderNumber") String orderNumber) {
        //建立虚拟返回参数
        List<String> resultList = new ArrayList<>();
        resultList.add("原订单收货人:newBird");
        resultList.add("新订单收货人:flyBird");
        return resultList;
    }

}

补充代码逻辑避坑

(1)Json记录业务日志耗能
json编码对CPU损耗非常大,如果只是日志记录,别用这么重的编码形式,精简日志或者简单字符串拼接会更经济。
(2)代码中的shopInfoVO若为NULL,会引入了异常导致影响业务主流程

catch(Exception e){ **加粗样式**
    logger.error("[getShopInfoVo] error shopInfoVO={}",shopInfoVO,e); 
}
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐