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

总结

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

相关推荐
小刘爱搬砖29 分钟前
SpringBoot3 + GraalVM安装和初次打包
spring boot·graalvm
_UMR_1 小时前
springboot集成Jasypt实现配置文件启动时自动解密-ENC
java·spring boot·后端
蓝色王者2 小时前
springboot 2.6.13 整合flowable6.8.1
java·spring boot·后端
hashiqimiya4 小时前
springboot事务触发滚动与不滚蛋
java·spring boot·后端
因我你好久不见4 小时前
Windows部署springboot jar支持开机自启动
windows·spring boot·jar
无关86885 小时前
SpringBootApplication注解大解密
spring boot
追梦者1237 小时前
springboot整合minio
java·spring boot·后端
帅气的你7 小时前
Spring Boot 集成 AOP 实现日志记录与接口权限校验
java·spring boot
计算机毕设VX:Fegn08958 小时前
计算机毕业设计|基于springboot + vue在线音乐播放系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
计算机毕设VX:Fegn08958 小时前
计算机毕业设计|基于springboot + vue博物馆展览与服务一体化系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计