一个注解,优雅搞定操作日志记录(含详细代码及注释)

今天做了一个需求,对系统中重要操作进行操作日志的收集和持久化。

      当然,这个需求最简单就是在控制层植入一段业务操作日志保存的逻辑,这样只适合体量非常小的项目。如果接口数量多,将重复写很多逻辑一样的代码,已开发上线的接口,还要重新再修改植入业务操作日志保存的逻辑并测试,就非常重复和麻烦。

    一、Spring AOP

    AOP(Aspect-Oriented Programming),⾯向切⾯编程,说起AOP,几乎学过Spring框架的人都知道,它是Spring的三大核心思想之一(IOC:控制反转,DI:依赖注入,AOP:面向切面编程)。能够将那些与业务⽆关,却为业务模块所共同调⽤的逻辑或责任(例如事务处理、⽇志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。

    SpringAOP,则是AOP的一种具体实现,Spring内部对SpringAOP的应用最经典的场景就是Spring的事务,通过事务注解的配置,Spring会自动在业务方法中开启、提交业务,并且在业务处理失败时,执行相应的回滚策略;与过滤器、拦截器相比,更加重要的是其适用范围不再局限于SpringMVC项目,可以在任意一层定义一个切点,织入相应的操作,并且还可以改变返回值。

    二、AOP核心要素

    1、什么时候切入:业务代码执行前还是后;

    2、在哪里切入:即切入点在哪里;

    3、干什么事情:即切入后的处理逻辑,比如权限校验,日志记录等。

    三、具体实现

      1、Maven依赖


<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

      2、自定义注解


/**
 * 自定义操作日志注解
 */
@Target(ElementType.METHOD) // 注解放置的目标位置,METHOD表示可应用在方法上
@Retention(RetentionPolicy.RUNTIME) //指定注解的保留策略,RUNTIME表示在运行时任然存在
@Documented // 表示该注解可以出现在生成的Api文档中
public @interface OperateLog {
    /**
     * 业务名称
     */
    String operModul() default "";

    /**
     * 操作说明
     */
    String operDesc() default "";

    /**
     * 操作类型
     */
    EnumOperateType operType() default EnumOperateType.GET;
}

      3、操作类型枚举


/**
 * 日志操作类型
 */
public enum EnumOperateType {
    POST("新增"),
    PUT("修改"),
    DELETE("删除"),
    GET("查询"),
    UP("启用"),
    DOWN("停用"),
    ADDORUPDATE("新增或修改");

    private final String typeName;

    EnumOperateType(String typeName) {
        this.typeName = typeName;
    }

    public String getTypeName() {
        return typeName;
    }
}

      4、数据模型对象


/**
 * 操作日志表;
 */
@Data
@TableName("t_operate_log")
public class TOperateLog extends BaseEntity {

    private static final long serialVersionUID = 1L;

    /**
     * 创建人
     */
    @TableField("create_name")
    private String createName;

    /**
     * 更新人
     */
    @TableField("update_name")
    private String updateName;

    /**
     * 业务名称
     */
    @TableField("model_name")
    private String modelName;

    /**
     * 操作记录
     */
    @TableField("operate_content")
    private String operateContent;

    /**
     * 操作账号
     */
    @TableField("operate_account")
    private String operateAccount;

    /**
     * 操作用户
     */
    @TableField("operate_user")
    private String operateUser;

    /**
     * 操作时间
     */
    @TableField("operate_time")
    private Date operateTime;

    /**
     * 操作单位
     */
    @TableField("unit_id")
    private String unitId;

    /**
     * 记录相关ID
     */
    @TableField("row_id")
    private String rowId;

    /**
     * 操作类型( select  update...)
     */
    @TableField("row_type")
    private String rowType;
}

      5、切面处理类


/**
 * 自定义操作日志注解切面处理类
 */
@Aspect // 标记该类为切面,结合其他注解(@Before、@After、@Around 等)来具体定义切面的行为
@Component
public class OperLogAspect {
    @Resource
    private PlatformFeignClient platformFeignClient;

    /**
     * 操作日志切入点:表示在注解位置切入代码
     * 括号内写自定义注解完全限定名
     */
    @Pointcut("@annotation(com.csin.common.annotation.OperateLog)")
    public void operLogPoinCut() {
    }

    /**
     * 目标方法执行成功并返回后执行
     *
     * @param joinPoint  目标方法信息对象,可获取目标方法方法名、参数等
     * @param keys      目标方法返回值,可以将目标方法返回值传递给切面方法这个参数
     */
    @AfterReturning(value = "operLogPoinCut()", returning = "keys")// 切入点表达式;表示将目标方法返回值绑定到切面方法参数keys
    public void saveOperLog(JoinPoint joinPoint, Object keys) {
        // 获取 RequestAttributes
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        // 从 RequestAttributes 中获取 HttpServletRequest 的信息(可获取请求方法,URL,IP等)
        HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);

        // 创建一个 TOperateLog 对象用于保存操作日志
        TOperateLog operateLog = new TOperateLog();
        try {
            // 通过反射机制获取切面织入点处的方法信息
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            // 获取切入点所在的方法对象
            Method method = signature.getMethod();
            // 获取方法上的 OperateLog 注解
            OperateLog opLog = method.getAnnotation(OperateLog.class);
            if (opLog != null) {
                // 获取注解中的操作模块、操作类型和操作描述信息,并设置到操作日志对象中
                String operModul = opLog.operModul(); // 获取操作模块
                EnumOperateType operType = opLog.operType(); // 获取操作类型
                String operDesc = opLog.operDesc(); // 获取操作描述

                operateLog.setModelName(operModul); // 设置业务名称
                operateLog.setRowType(operType.getTypeName()); // 设置操作类型
                operateLog.setOperateContent(operDesc); // 设置操作记录
            }

            operateLog.setOperateTime(new Date());
            operateLog.setUnitId(UserUtil.getUnitId());
            operateLog.setOperateUser(UserUtil.getUsername());
            operateLog.setOperateAccount(UserUtil.getAccount());

            // 调用 platformFeignClient 的 add 方法,向日志服务中添加操作日志(根据场景实际进行持久化)
            ApiResponse<Boolean> add = platformFeignClient.add(operateLog);

            // 判断日志服务添加操作是否成功
            if (!add.isSuccess()) {
                throw new BusinessException("feign调用日志服务失败");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

      6、测试

/**
 * 测试控制器
 **/
@RefreshScope
@RestController
@RequestMapping("/test")
public class TestController {

    @RequestMapping("/logTest")
    @OperateLog(operModul = "测试接口模块", operDesc = "测试注解效果", operType = EnumOperateType.POST)
    public ApiResponse<Boolean> logAnnotationTest () {
        System.out.println("测试自定义操作日志注解.........");
        return ApiResponse.success(true);
    }
}

      7、数据库表


CREATE TABLE `t_operate_log` (
  `id` varchar(32) NOT NULL COMMENT '主键ID',
  `create_name` varchar(32) DEFAULT NULL COMMENT '创建人',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_name` varchar(32) DEFAULT NULL COMMENT '更新人',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  `model_name` varchar(32) NOT NULL COMMENT '业务名称',
  `operate_content` varchar(255) NOT NULL COMMENT '操作记录',
  `operate_account` varchar(255) DEFAULT NULL COMMENT '操作账号',
  `operate_user` varchar(255) DEFAULT NULL COMMENT '操作用户',
  `operate_time` datetime DEFAULT NULL COMMENT '操作时间',
  `unit_id` varchar(32) DEFAULT NULL COMMENT '操作单位',
  `row_id` varchar(32) DEFAULT NULL COMMENT '记录相关ID',
  `row_type` varchar(16) DEFAULT NULL COMMENT '操作类型( select  update...)',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='操作日志表;';

8、注意事项

      如果你自定义的操作日志注解的切面逻辑未生效,可以按照以下步骤进行排查:

      1、确保注解和切面类的定义正确:检查自定义的操作日志注解和切面类的定义是否正确,包括注解的声明和切面类的命名、注解的元注解等。

      2、确保切面类被正确扫描:切面类需要被 Spring AOP 所扫描到才能生效。确保切面类所在的包是被扫描的,可以使用 @ComponentScan 或 <context:component-scan> 等注解或配置来指定扫描的包。

      3、检查切面类的顺序:如果有多个切面类作用于同一个目标方法,切面的执行顺序可能会影响到最终的结果。可以为切面类添加 @Order 注解来指定切面的执行顺序。

      4、确认目标类和方法的代理情况:如果目标类和方法是在同一个类中,并且是通过直接调用而非通过代理调用的方式,切面逻辑可能不会生效。确保目标类和方法是通过代理调用,例如使用基于接口的代理或者使用 Spring AOP 提供的代理机制。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值