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);
    }
}

总结

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

相关推荐
java小白小2 天前
SpringBoot(01): 初识SpringBoot,从Spring的痛点说起
spring boot
用户3169353811832 天前
如何从零编写一个 Spring Boot Starter
spring boot
程序员晓琪3 天前
约定大于配置:基于 Java 包名自动生成 API 版本路由的最佳实践
java·spring boot·后端
Flittly3 天前
【AgentScope Java新手村系列】(11)中断与恢复
java·spring boot·spring
用户3521802454754 天前
🎆从 Prompt 到 Skill:让 Spring AI Agent 学会"装新技能"
人工智能·spring boot·ai编程
用户3521802454757 天前
当 Prompt 学会"热更新":Spring Boot × Nacos3 AI 实战
java·spring boot·ai编程
昵称为空C7 天前
手撸一个动态 SQL 执行引擎:不重启服务,在线增删改查任意数据库
spring boot·后端
霸道流氓气质8 天前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
于先生吖8 天前
SpringBoot对接大模型开发AI命理测算系统:八字排盘与AI解析接口源码全解
人工智能·spring boot·后端