若依框架----源码分析(@Log)

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 若依框架----源码分析(@Log)

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


若依提供了若干的自定义注解,本文记录了其中一个:@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();
        }
    }


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


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


总结


  • 标记了@Log注解的方法,在执行后或者抛出异常后会异步的将操作记录(IP、模块、请求方法、请求参数等)存到数据库中。


  • 了解了他的写法之后,我们可以随意的改造它。比如我们还想存一些我们自己个性化需求的内容,再比如我们可以把日志存储到ElasticSearch中,再借助一些ETL工具,实现日志的可视化等等。
相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
19天前
|
XML JSON Java
Logback 与 log4j2 性能对比:谁才是日志框架的性能王者?
【10月更文挑战第5天】在Java开发中,日志框架是不可或缺的工具,它们帮助我们记录系统运行时的信息、警告和错误,对于开发人员来说至关重要。在众多日志框架中,Logback和log4j2以其卓越的性能和丰富的功能脱颖而出,成为开发者们的首选。本文将深入探讨Logback与log4j2在性能方面的对比,通过详细的分析和实例,帮助大家理解两者之间的性能差异,以便在实际项目中做出更明智的选择。
125 3
|
3天前
|
Java 程序员 API
Android|集成 slf4j + logback 作为日志框架
做个简单改造,统一 Android APP 和 Java 后端项目打印日志的体验。
17 1
|
2月前
|
设计模式 SQL 安全
PHP中的设计模式:单例模式的深入探索与实践在PHP的编程实践中,设计模式是解决常见软件设计问题的最佳实践。单例模式作为设计模式中的一种,确保一个类只有一个实例,并提供全局访问点,广泛应用于配置管理、日志记录和测试框架等场景。本文将深入探讨单例模式的原理、实现方式及其在PHP中的应用,帮助开发者更好地理解和运用这一设计模式。
在PHP开发中,单例模式通过确保类仅有一个实例并提供一个全局访问点,有效管理和访问共享资源。本文详细介绍了单例模式的概念、PHP实现方式及应用场景,并通过具体代码示例展示如何在PHP中实现单例模式以及如何在实际项目中正确使用它来优化代码结构和性能。
37 2
|
19天前
|
SQL XML 监控
SpringBoot框架日志详解
本文详细介绍了日志系统的重要性及其在不同环境下的配置方法。日志用于记录系统运行时的问题,确保服务的可靠性。文章解释了各种日志级别(如 info、warn、error 等)的作用,并介绍了常用的日志框架如 SLF4J 和 Logback。此外,还说明了如何在 SpringBoot 中配置日志输出路径及日志级别,包括控制台输出与文件输出的具体设置方法。通过这些配置,开发者能够更好地管理和调试应用程序。
|
2月前
|
Java
日志框架log4j打印异常堆栈信息携带traceId,方便接口异常排查
日常项目运行日志,异常栈打印是不带traceId,导致排查问题查找异常栈很麻烦。
|
2月前
|
运维 NoSQL Java
SpringBoot接入轻量级分布式日志框架GrayLog技术分享
在当今的软件开发环境中,日志管理扮演着至关重要的角色,尤其是在微服务架构下,分布式日志的统一收集、分析和展示成为了开发者和运维人员必须面对的问题。GrayLog作为一个轻量级的分布式日志框架,以其简洁、高效和易部署的特性,逐渐受到广大开发者的青睐。本文将详细介绍如何在SpringBoot项目中接入GrayLog,以实现日志的集中管理和分析。
171 1
|
3月前
|
XML Java Maven
Spring5入门到实战------16、Spring5新功能 --整合日志框架(Log4j2)
这篇文章是Spring5框架的入门到实战教程,介绍了Spring5的新功能——整合日志框架Log4j2,包括Spring5对日志框架的通用封装、如何在项目中引入Log4j2、编写Log4j2的XML配置文件,并通过测试类展示了如何使用Log4j2进行日志记录。
Spring5入门到实战------16、Spring5新功能 --整合日志框架(Log4j2)
|
3月前
|
Java 数据库连接 数据库
后端框架的学习----mybatis框架(6、日志)
这篇文章介绍了如何在MyBatis框架中使用日志功能,包括配置MyBatis的日志实现、使用log4j作为日志工具,以及如何通过配置文件控制日志级别和输出格式。
|
3月前
|
存储 缓存 监控
【嵌入式SD NAND】基于FATFS/Littlefs文件系统的日志框架实现
综上所述,构建一个基于FATFS/Littlefs文件系统的日志框架需要对文件系统的操作有深入理解,并以此为基础设计一套完整的日志处理机制。这样的框架不仅能够确保日志数据的完整性和系统的鲁棒性,同时还能够满足嵌入式系统对于性能和资源使用的严格要求。
113 4
|
3月前
|
人工智能 Java Spring
Spring框架下,如何让你的日志管理像‘AI’一样智能,提升开发效率的秘密武器!
【8月更文挑战第31天】日志管理在软件开发中至关重要,不仅能帮助开发者追踪问题和调试程序,还是系统监控和运维的重要工具。在Spring框架下,通过合理配置Logback等日志框架,可大幅提升日志管理效率。本文将介绍如何引入日志框架、配置日志级别、在代码中使用Logger,以及利用ELK等工具进行日志聚合和分析,帮助你构建高效、可靠的日志管理系统,为开发和运维提供支持。
55 0