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程序媛7 小时前
final 别乱用!变量、方法、类的区别,90% 新手混淆
java·spring boot
念越7 小时前
初中语数英答题与竞赛平台
java·数据库·spring boot
imDwAaY9 小时前
Spring中过滤器和拦截器的区别是什么?
spring boot·后端·spring
萧瑟余晖11 小时前
Spring Boot Web开发与内嵌容器详解
spring boot
Q264336502311 小时前
【有源码】基于Spring Boot和Vue的在线考试与交流系统-教育信息化背景下在线考试与交流一体化平台
java·vue.js·spring boot·mysql·spring·毕业设计·课程设计
EatFan12 小时前
2026 后端 AI 集成三条落地路径:Spring AI 2.0、MCP 协议与旁路服务怎么选
java·人工智能·spring boot·spring·大模型·spring ai·mcp
imDwAaY13 小时前
Spring Boot的核心配置文件一览及其加载顺序
java·spring boot·后端
专业程序开发源13 小时前
springboot校园志愿者管理系统28562-计算机课程设计、毕业设计
java·spring boot·后端·python·django·php·课程设计
vx-程序开发1 天前
【计算机毕设】51300童装超市管理系统的设计与实现
java·vue.js·spring boot·源码·课程设计·毕设·大作业