SpringBoot 自定义切面+自定义注解 实现全局操作日志记录 ThreadLocal统计请求耗时

前言

最近在工作中碰到一个需求,要求对于用户进行的增删改查操作,都做一个操作日志。最简单的方式就是每个增删改查操作完成之后,都调用一个封装好的保存日志的方法。本文介绍一下基于自定义切面+自定义注解的实现方式,这种方案的优点就是对原来代码的侵入性低,并且是一种横向的拓展,和业务无关。

下面创建了一个demo项目,来演示一下具体的实现方式。

表设计

这里设计一个用户表(t_user),一个日志表(t_oper_log)。我们对用户表的数据进行增删改查,将操作日志保存到日志表。

用户表:

日志表:

代码实现

UserController

如下,现在有一个UserController,里面有增删改查四个方法,现在需要对这些方法,进行操作记录

java 复制代码
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping("/list")
    public List<UserEntity> list() {
        return userService.list();
    }

    @PostMapping("/save")
    public boolean save(@RequestBody UserEntity userEntity) {
        return userService.save(userEntity);
    }

    @PostMapping("/delete")
    public boolean delete(@RequestParam("id") long id) {
        return userService.removeById(id);
    }

    @PostMapping("/update")
    public boolean update(@RequestBody UserEntity userEntity) {
        return userService.updateById(userEntity);
    }
}

自定义注解

这一步需要定义一个注解,将来会将他标注在需要进行操作记录的controller层方法上,可以指定模块名称、业务名称、业务类型。这里也可自行拓展其他的字段。

java 复制代码
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Log {
    /**
     * 模块名称
     *
     * @return
     */
    String module() default "";

    /**
     * 业务名称
     *
     * @return
     */
    String business() default "";

    /**
     * 业务类型
     *
     * @return
     */
    BusinessType businessType() default BusinessType.OTHER;
}
java 复制代码
public enum BusinessType {
    SELECT,
    SAVE,
    DELETE,
    UPDATE,
    OTHER;
}

自定义切面

代码如下,使用ThreadLocal变量计算了请求耗时。

java 复制代码
@Aspect
@Component
public class LogAspect {

    @Autowired
    private OperLogService operLogService;

    //请求耗时
    private static final ThreadLocal<Long> COST_TIME = new NamedThreadLocal<>("cost_time");

    /**
     * 前置通知
     *
     * @param joinPoint
     * @param log
     */
    @Before("@annotation(log)")
    public void before(JoinPoint joinPoint, Log log) {
        //设置初始值,用于计算请求耗时
        COST_TIME.set(System.currentTimeMillis());
    }

    /**
     * 返回通知
     *
     * @param joinPoint
     * @param log
     * @param result
     */
    @AfterReturning(pointcut = "@annotation(log)", returning = "result")
    public void afterReturning(JoinPoint joinPoint, Log log, Object result) {
        insertLog(joinPoint, log, result, null);
    }

    /**
     * 异常通知
     *
     * @param joinPoint
     * @param log
     * @param e
     */
    @AfterThrowing(pointcut = "@annotation(log)", throwing = "e")
    public void afterThrowing(JoinPoint joinPoint, Log log, Exception e) {
        insertLog(joinPoint, log, null, e);
    }

    /**
     * 日志记录
     *
     * @param joinPoint
     * @param log
     * @param result
     * @param e
     */
    private void insertLog(JoinPoint joinPoint, Log log, Object result, Exception e) {
        try {
            OperLogEntity entity = new OperLogEntity();
            //基本信息
            entity.setAddTime(new Date());
            entity.setAddUser(((UserEntity) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getId());
            //业务信息
            entity.setModule(log.module());
            entity.setBusiness(log.business());
            entity.setBusinessType(log.businessType().toString());
            //请求信息
            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            HttpServletRequest request = attributes.getRequest();
            entity.setRequestIp(request.getLocalAddr().contains(":") ? "127.0.0.1" : request.getLocalAddr());
            entity.setRequestUrl(request.getRequestURI());
            entity.setRequestMethod(request.getMethod());
            //方法信息
            String className = joinPoint.getTarget().getClass().getName();
            String methodName = joinPoint.getSignature().getName();
            entity.setMethod(className + "." + methodName + "()");
            //请求耗时
            entity.setCostTime(System.currentTimeMillis() - COST_TIME.get());
            //错误日志
            entity.setStatus(BusinessStatus.SUCCESS.ordinal());
            if (e != null) {
                entity.setStatus(BusinessStatus.FAIL.ordinal());
                entity.setErrMsg(e.getMessage().length() < 255 ? e.getMessage() : e.getMessage().substring(0, 255));
            }
            //保存日志
            operLogService.save(entity);
        } catch (Exception exception) {
            e.printStackTrace();
        } finally {
            COST_TIME.remove();
        }
    }
}

使用注解

在需要进行记录的controller层方法上标注注解

java 复制代码
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping("/list")
    @Log(module = "用户管理", business = "查询列表", businessType = BusinessType.SELECT)
    public List<UserEntity> list() {
        return userService.list();
    }

    @PostMapping("/save")
    @Log(module = "用户管理", business = "保存用户", businessType = BusinessType.SAVE)
    public boolean save(@RequestBody UserEntity userEntity) {
        return userService.save(userEntity);
    }

    @PostMapping("/delete")
    @Log(module = "用户管理", business = "删除用户", businessType = BusinessType.DELETE)
    public boolean delete(@RequestParam("id") long id) {
        return userService.removeById(id);
    }

    @PostMapping("/update")
    @Log(module = "用户管理", business = "修改用户", businessType = BusinessType.UPDATE)
    public boolean update(@RequestBody UserEntity userEntity) {
        return userService.updateById(userEntity);
    }
}

总结

以上,就通过自定义切面+自定义注解 完成了全局的操作日志记录。

相关推荐
MetaLite15 分钟前
Spring Boot 加了 @Valid,嵌套参数为什么还没校验?
java·spring boot·后端
霸道流氓气质22 分钟前
Lingua 语言检测库:n-gram 统计模型、置信度阈值调优与 Spring Boot 分层检测管道的生产级实战
java·spring boot·后端
她的男孩1 小时前
低代码加字段后表结构怎么同步:预览安全 DDL,高风险变更再强制执行
java·spring boot·低代码
huaweichenai1 小时前
spring boot对接kafka实现消息队列处理
spring boot·后端·kafka
Thomas.Sir1 小时前
第11课:文件上传下载、跨域CORS配置,SpringBoot4.x跨域新方案
java·spring boot
寰梦2 小时前
Spring Boot + MinIO 大文件上传完整实战:MD5 秒传、分片上传、断点续传与引用一致性
java·spring boot·文件上传·minio
行者-全栈开发2 小时前
【码动四季·秋】把 Spring Boot 的 Tomcat 换成 Undertow:一次容器替换的三处暗坑
spring boot·tomcat·容器化·依赖管理·undertow
长弓三石2 小时前
Spring Boot 4.1 + AgentScope:企业级 Agent 平台的 MCP 双向集成实战
人工智能·spring boot·后端·harness·开源智能体
小蒜学长14 小时前
基于Spring Boot+Vue3的在线医疗咨询系统的设计与实现(代码+数据库+LW)
java·spring boot·后端·在线问诊·在线医疗咨询系统
m0_6399069418 小时前
SpringBoot自动配置原理
java·spring boot·后端