eladmin——系统日志

221 阅读5分钟

一、 前言

本篇文章主要介绍eladmin中如何使用自定义注解实现日志记录,日志管理CRUD逻辑感兴趣的同学可以看源码学习——eladmin源码

二、自定义注解实现日志记录

2.1 定义日志注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
    String value() default "";
}

2.2 创建日志切面

// 第一步:创建切面类
@Component
@Aspect
@Slf4j
public class LogAspect {

    private final SysLogService sysLogService;

    ThreadLocal<Long> currentTime = new ThreadLocal<>();

    public LogAspect(SysLogService sysLogService) {
        this.sysLogService = sysLogService;
    }

    /**
     * 第二步:定义切入点
     */
    @Pointcut("@annotation(me.zhengjie.annotation.Log)")
    public void logPointcut() {
        // 该方法无方法体,主要为了让同类中其他方法使用此切入点
    }

    /**
     * 第三步:通知与切点整合
     *
     * @param joinPoint join point for advice
     */
    @Around("logPointcut()")
    public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
        Object result;
        currentTime.set(System.currentTimeMillis());
        result = joinPoint.proceed();
        SysLog sysLog = new SysLog("INFO",System.currentTimeMillis() - currentTime.get());
        currentTime.remove();
        HttpServletRequest request = RequestHolder.getHttpServletRequest();
        sysLogService.save(getUsername(), StringUtils.getBrowser(request), StringUtils.getIp(request),joinPoint, sysLog);
        return result;
    }

    /**
     * 配置异常通知
     *
     * @param joinPoint join point for advice
     * @param e exception
     */
    @AfterThrowing(pointcut = "logPointcut()", throwing = "e")
    public void logAfterThrowing(JoinPoint joinPoint, Throwable e) {
        SysLog sysLog = new SysLog("ERROR",System.currentTimeMillis() - currentTime.get());
        currentTime.remove();
        sysLog.setExceptionDetail(ThrowableUtil.getStackTrace(e).getBytes());
        HttpServletRequest request = RequestHolder.getHttpServletRequest();
        sysLogService.save(getUsername(), StringUtils.getBrowser(request), StringUtils.getIp(request), (ProceedingJoinPoint)joinPoint, sysLog);
    }

    public String getUsername() {
        try {
            return SecurityUtils.getCurrentUsername();
        }catch (Exception e){
            return "";
        }
    }
}

值得注意的是在这个LogAspect类中,使用ThreadLocal<Long>来存储时间确保线程安全。在并发环境下,多个请求可能会同时被处理,每个请求都会在自己的线程中执行。ThreadLocal提供了线程局部变量,也就是说,每个线程访问该变量时都有自己独立初始化的副本,各个线程之间的变量值是隔离的。这样,每个线程都可以安全地在没有同步的情况下访问其独立初始化的变量副本。在这个切面日志的场景中,每个请求(通常对应一个线程)开始时记录的当前时间是独立的,不会被其他请求(线程)所干扰。

简而言之,使用ThreadLocal是为了:

  1. 保证线程安全:确保每个线程都有自己的时间副本,防止在处理并发请求时出现数据竞争问题。
  2. 避免同步开销:如果不使用ThreadLocal而是选择通过同步机制来解决共享变量的线程安全问题,那么可能会引入不必要的性能开销。ThreadLocal可以无锁访问其独立的变量副本,从而提高效率。

然而,需要注意的是,虽然ThreadLocal很方便,但也要小心内存泄漏的问题。在上述代码中已经很好地管理了ThreadLocal的生命周期,通过在使用完毕后调用currentTime.remove()来清理资源,这是非常重要的一步,以确保不会有内存泄漏发生。

想要进一步了解ThreadLocal底层原理的同学可移步:浅谈ThreadLocal(一) - 掘金 (juejin.cn)

三、注解的作用及自定义注解实现方式

3.1 作用

  1. 提供元数据信息:注解可以为程序元素(如类、方法、字段)添加元数据信息,比如作者、版本号、创建日期等。这些信息可以被工具或框架读取和利用,用于生成文档、做版本控制、自动化处理等。
  2. 辅助编译检查:注解可以帮助编译器进行额外的静态检查和验证。例如,使用 @Override 注解可以确保某个方法重写了父类的方法,如果不满足条件则会产生编译错误。
  3. 配置和触发特定行为:注解可以用于配置和触发特定的行为。例如,Spring框架中的 @Autowired 注解可以自动注入依赖对象;JUnit中的 @Test 注解用于标记测试方法等。
  4. 代码生成和处理:注解可以用于代码生成和处理工具。例如,编写代码生成器时,可以使用注解来定义生成代码的规则和逻辑,从而简化代码生成过程。
  5. 运行时处理:注解可以在程序运行时通过反射机制获取,并根据注解的信息来做特定的处理。例如,使用注解实现权限控制、日志记录等功能。

3.2 实现

方式一:AOP

切面AOP编码3板斧:

  1. 创建切面类(Aspect):首先要创建一个切面类,其中包含了通知(Advice)以及定义切点(Pointcut)的逻辑。
  2. 定义切点(Pointcut):在切面类中定义切点,它决定了哪些连接点(Join Point)会被通知所影响。
  3. 编写通知并与切点整合:在切面类中编写不同类型的通知(Before、After、Around 等),然后将这些通知与切点进行整合,以实现对目标方法的增强操作。

示例:如第二节eladmin实现日志记录就是通过AOP的方式

方式二: 拦截器

  1. 创建拦截器:创建一个拦截器来检查方法是否被自定义注解标注,如果是,则执行相应的逻辑。
  2. 注册拦截器:在Spring配置中注册这个拦截器,以便它能够工作。

示例:

  1. 首先,需要定义一个自定义注解。假设我们要创建一个用于权限验证的注解@CheckPermission
@Target(ElementType.METHOD) // 注解适用于方法
@Retention(RetentionPolicy.RUNTIME) // 注解在运行时有效
public @interface CheckPermission {
    String value() default ""; // 该注解包含一个名为value的元素,可以在使用注解时指定
}
  1. 接下来,创建一个拦截器来检查方法是否被@CheckPermission注解标注,如果是,则执行相应的权限检查逻辑。
public class PermissionInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            Method method = handlerMethod.getMethod();
            
            // 检查方法是否有@CheckPermission注解
            CheckPermission checkPermission = method.getAnnotation(CheckPermission.class);
            if (checkPermission != null) {
                // 获取注解的value值,进行权限验证逻辑
                String permissionValue = checkPermission.value();
                // 这里添加权限验证的逻辑
                // 如果权限验证不通过,可以返回false或者抛出异常
            }
        }
        return true; // 如果没有注解或者权限验证通过,则继续执行
    }
}
  1. 最后,你需要在Spring配置中注册这个拦截器,以便它能够工作。如果你使用的是Spring MVC,则可以在配置类中添加如下配置:
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new PermissionInterceptor());
    }
}