若依作为最近非常火的脚手架,分析它的源码,不仅可以更好的使用它,在出错时及时定位,也可以在需要个性化功能时轻车熟路的修改它以满足我们自己的需求,同时也可以学习人家解决问题的思路,提升自己的技术水平
若依提供了若干的自定义注解,本文记录了其中一个:@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) { try { // 获取当前的用户 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(); } }
- 若依提供的用于存储操作日志的表名为sys_oper_log,对应的实体类就是SysOperLog
- 若依通过自己写的一些工具类来获取用户的信息,如用户名、IP等。点开这些工具类我们发现其实使用的就是Spring或者SpringSecurity的一些常用的类,如RequestServletContext、SecurityContextHolder等,感兴趣的同学可以自行查看
- 通过连接点
JoinPoint
获取目标的所属类和方法名 - 处理注解参数和请求参数
- 异步将日志存储到数据库
前三步就不说了,这里说一下第四步和第五步
处理请求参数
跟着源码一路点下来,看见获取请求参数的方法为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)); } else { // 提取地址栏中的参数 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工具,实现日志的可视化等等。