若依作为最近非常火的脚手架,分析它的源码,不仅可以更好的使用它,在出错时及时定位,也可以在需要个性化功能时轻车熟路的修改它以满足我们自己的需求,同时也可以学习人家解决问题的思路,提升自己的技术水平

若依提供了若干的自定义注解,本文记录了其中一个: @Log -- 自定义操作日志记录注解 的实现步骤

主要思想

在controller中标记了 @Log 注解的方法,会在方法执行完或者抛出异常后异步的将用户的操作记录存储到数据库中

具体步骤

1. 注解

我们先来看一下 @Log 注解,在 com/ruoyi/common/annotation 包下

@Target({ ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log
    public String title() default "";
    public BusinessType businessType() default BusinessType.OTHER;
     * 操作人类别
    public OperatorType operatorType() default OperatorType.MANAGE;
     * 是否保存请求的参数
    public boolean isSaveRequestData() default true;
     * 是否保存响应的参数
    public boolean isSaveResponseData() default true;
}

一个可以标记在方法或者参数上的注解,有5个属性。每个属性若依都写了注解:

  • title: 表示的是业务模块
  • businessType: 表示功能,增删改查导入导出等等
  • operatorType:表示操作人类别,是后台用户或者手机端用户。
  • isSaveRequestData:是否保存request和参数和值
  • isSaveResponseData:是否保存response和参数和值

2. 切面

下面看一下 @Log 注解的切面 LogAspect ,它在 com/ruoyi/framework/aspectj 包下

@Aspect
@Component
public class LogAspect
    private static final Logger log = LoggerFactory.getLogger(LogAspect.class);
     * 处理完请求后执行
     * @param joinPoint 切点
    @AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult")
    public void doAfterReturning(JoinPoint joinPoint, Log controllerLog, Object jsonResult)
        handleLog(joinPoint, controllerLog, null, jsonResult);
     * 拦截异常操作
     * @param joinPoint 切点
     * @param e 异常
    @AfterThrowing(value = "@annotation(controllerLog)", throwing = "e")
    public void doAfterThrowing(JoinPoint joinPoint, Log controllerLog, Exception e)
        handleLog(joinPoint, controllerLog, e, null);
    protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult)
        ......
    ......
}

首先忽略掉一些具体的处理细节,这里使用了 Spring 五种通知里的两种: 返回通知 异常通知 。也就是说,加了 @Log 注解方法在方法执行完或者抛出异常后,都会进行日志的记录操作。

具体记录日志的方法在 handleLog() 方法中:

protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult)
            // 获取当前的用户
            LoginUser loginUser = SecurityUtils.getLoginUser();
            // *========数据库日志=========*//
            SysOperLog operLog = new SysOperLog();
            operLog.setStatus(BusinessStatus.SUCCESS.ordinal());
            // 请求的地址
            String ip = IpUtils.getIpAddr(ServletUtils.getRequest());
            operLog.setOperIp(ip);
            operLog.setOperUrl(ServletUtils.getRequest().getRequestURI());
            if (loginUser != null)
                operLog.setOperName(loginUser.getUsername());
            // 如果异常不为空,说明报错。设置状态为失败,设置错误信息为异常信息
            if (e != null)
                operLog.setStatus(BusinessStatus.FAIL.ordinal());
                operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000));
            // 设置方法名称
            String className = joinPoint.getTarget().getClass().getName();
            String methodName = joinPoint.getSignature().getName();
            operLog.setMethod(className + "." + methodName + "()");
            // 设置请求方式
            operLog.setRequestMethod(ServletUtils.getRequest().getMethod());
            // 处理设置注解上的参数
            getControllerMethodDescription(joinPoint, controllerLog, operLog, jsonResult);
            // 保存数据库
            AsyncManager.me().execute(AsyncFactory.recordOper(operLog));
        catch (Exception exp)
            // 记录本地异常日志
            log.error("==前置通知异常==");
            log.error("异常信息:{}", exp.getMessage());
            exp.printStackTrace();
    }
  1. 若依提供的用于存储操作日志的表名为 sys_oper_log ,对应的实体类就是 SysOperLog
  2. 若依通过自己写的一些工具类来获取用户的信息,如用户名、IP等。点开这些工具类我们发现其实使用的就是 Spring 或者 SpringSecurity 的一些常用的类,如 RequestServletContext SecurityContextHolder 等,感兴趣的同学可以自行查看
  3. 通过连接点 JoinPoint 获取目标的所属类和方法名
  4. 处理注解参数和请求参数
  5. 异步将日志存储到数据库

前三步就不说了,这里说一下第四步和第五步

处理请求参数

跟着源码一路点下来,看见获取请求参数的方法为 setRequestValue

private void setRequestValue(JoinPoint joinPoint, SysOperLog operLog) throws Exception
    String requestMethod = operLog.getRequestMethod();
    if (HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod))
        // 去被拦截的方法中提取参数。遍历参数,去掉文件对象、HttpServletRequest等参数
        String params = argsArrayToString(joinPoint.getArgs());
        operLog.setOperParam(StringUtils.substring(params, 0, 2000));
        // 提取地址栏中的参数
        Map<?, ?> paramsMap = (Map<?, ?>) ServletUtils.getRequest().getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
        operLog.setOperParam(StringUtils.substring(paramsMap.toString(), 0, 2000));
}

若依对请求方式进行了判断,如果是 put 或者 post 方法,就去被拦截的方法中提取参数,并过滤掉文件对象、 HttpServletRequest 等参数。如果是其他请求方式就直接提取地址栏中的参数,我也写了相应注释。

异步存储日志

为了不影响业务的处理速度,若依写了一个异步的任务 AsyncManager 来存储日志,位于 com/ruoyi/framework/manager 包下。

public class AsyncManager
    // 操作延迟10毫秒
    private final int OPERATE_DELAY_TIME = 10;
    // 异步操作任务调度线程池
    private ScheduledExecutorService executor = SpringUtils.getBean("scheduledExecutorService");
    // 单例模式
    private AsyncManager(){}
    private static AsyncManager me = new AsyncManager();
    public static AsyncManager me()
        return me;
    // 执行任务
    public void execute(TimerTask task)
        executor.schedule(task, OPERATE_DELAY_TIME, TimeUnit.MILLISECONDS);
    // 停止任务线程池
    public void shutdown()
        Threads.shutdownAndAwaitTermination(executor);
}

我们发现,若依使用Java提供的 ScheduledExecutorService 来执行定时任务。由于 AsyncManager 并没有注册到Spring容器中,所有它没办法注入 scheduledExecutorService ,所以若依使用了一个工具类 SpringUtils.getBean() 从Spring容器中获取它。 scheduledExecutorService 这个容器在 com/ruoyi/framework/config/ThreadPoolConfig.java 中:

/**
 * 执行周期性或定时任务
@Bean(name = "scheduledExecutorService")
protected ScheduledExecutorService scheduledExecutorService()
    return new ScheduledThreadPoolExecutor(corePoolSize,
            new BasicThreadFactory.Builder().namingPattern("schedule-pool-%d").daemon(true).build(),
            new ThreadPoolExecutor.CallerRunsPolicy())
        @Override
        protected void afterExecute(Runnable r, Throwable t)
            super.afterExecute(r, t);
            Threads.printException(r, t);
}

两个小细节

1. 根据IP查找地址

若依通过一个接口去查找IP所在的地址,具体的方法在 com.ruoyi.common.utils.ip.AddressUtils 中,感兴趣的同学可以自行查看

2. getBean

若依使用 ConfigurableListableBeanFactory getBean() 方法去获取Bean,其实这个接口继承自 BeanFactory ,使用的也是 BeanFactory getBean() 方法

总结

  • 标记了 @Log 注解的方法,在执行后或者抛出异常后会异步的将操作记录(IP、模块、请求方法、请求参数等)存到数据库中。
  • 了解了他的写法之后,我们可以随意的改造它。比如我们还想存一些我们自己个性化需求的内容,再比如我们可以把日志存储到ElasticSearch中,再借助一些ETL工具,实现日志的可视化等等。
java尺寸车 长度java

java 中的 length属性是针对数组说的,比如说你声明了一个数组,想知道这个数组的长度则用到了 length 这个属性. java 中的 length() 方法是针对字符串说的,如果想看这个字符串的长度则用到 length() 这个方法. java 中的 size() 方法是针对泛型集合说的,如果想看这个泛型有多少个元素,就调用此方法来查看!

mysql 存储过程 乱码 mysql存储过程保存在哪

1. 存储过程存储过程优势存储过程把经常使用的SQL语句或业务逻辑封装起来,预编译保存在数据库中,当需要时从数据库中直接调用,省去了编译的过程。提高了运行速度。同时降低网络数据传输量。存储过程保存在mysql.proc表中。2. 语法2.1. 创建存储过程CREATE PROCEDURE sp_name ([ proc_parameter [,proc_parameter ...]])