51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

SpringBoot 3.0 日志相关配置

Preface {#preface}

Hi,大家好!

鲁迅曾经说过:日志打得好,排查没烦恼。

启动日志 {#启动日志}

将 Spring Boot 启动类修改成如下形式:

@SpringBootApplication
public class MwApiApplication {
private static final Logger LOG = LoggerFactory.getLogger(MwApiApplication.class);

public static void main(String[] args) {
    // 创建一个 SpringApplication 对象,参数 MwApiApplication.class 是 Spring Boot 应用的启动类。
    SpringApplication app = new SpringApplication(MwApiApplication.class);
    // 使用 SpringApplication 对象运行 Spring Boot 应用,并获取应用运行后的环境对象。
    ConfigurableEnvironment env = app.run(args).getEnvironment();
    LOG.info("启动成功!!!");
    // env.getProperty("server.port") 是从环境对象中获取 "server.port" 这个属性的值,也就是应用的运行端口。
    LOG.info("地址:http://127.0.0.1:{}", env.getProperty("server.port"));
}

}

Logback 配置 {#logback-配置}

Logback 是一个用于 Java 应用程序的日志框架。它旨在成为Log4j的替代品,并由 Ceki Gülcü 创建。Logback 提供了一个灵活且高效的日志解决方案,适用于各种不同的应用场景。

Logback 包含三个主要组件:

  1. logback-core: 提供了通用的日志功能,包括Logger、Appender 和 Layout 等基本构件。
  2. logback-classic: 构建在 logback-core 之上,是 Logback 的经典实现,同时也是 Log4j 的直接替代品。它提供了一个类似于 Log4j 的 API,并在内部使用 logback-core 的功能。
  3. logback-access: 用于 Web 应用程序的模块,提供对 HTTP 访问日志的支持。

Logback 的特点包括:

  • 性能: Logback 被设计为高性能的日志框架,能够处理大量的日志事件而不会对应用程序的性能产生显著影响。
  • 灵活性: Logback 提供了丰富的配置选项,允许开发人员根据应用程序的需求进行灵活配置。它支持 XML 和 Groovy 等多种配置方式。
  • 可插拔性: Logback 支持多种 Appender 和 Layout,使开发人员能够根据具体需求选择合适的组件。
  • 遗产兼容性: logback-classic API 的设计与 Log4j 相似,因此可以较为轻松地将现有的 Log4j 日志代码迁移到 Logback。

总体而言,Logback 是一个强大而灵活的日志框架,广泛用于 Java 应用程序的日志记录需求。

resources目录下新建logback-spring.xml文件,用来配置日志打印的格式,保存路径及保存方式等。

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!-- 定义一个名为 PATH 的变量,值为日志文件保存路径,为当前项目的 log 目录下。在下方使用 ${PATH} 引用此变量的值 -->
    <property name="PATH" value="./log" />
&lt;!-- 在日志系统中,appender 是一种组件,用于定义日志消息的输出目的地。它决定了日志消息将被发送到何处,例如控制台、文件、数据库等。appender 负责将日志事件记录到指定的目标。--&gt;
&lt;!-- 定义名为 STDOUT 的控制台输出 appender --&gt;
&lt;appender name=&quot;STDOUT&quot; class=&quot;ch.qos.logback.core.ConsoleAppender&quot;&gt;
    &lt;encoder&gt;
        &lt;!-- 原始格式:&lt;Pattern&gt;%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight(%-5level) %blue(%-50logger{50}:%-4line) %thread %green(%-18X{LOG_ID}) %msg%n&lt;/Pattern&gt; --&gt;

        &lt;!-- 定义控制台输出的日志格式 --&gt;
        &lt;!-- %d{mm:ss.SSS}:表示输出日期和时间,使用分钟、秒和毫秒的格式。例如,12:34.567 表示时间为12分钟34秒567毫秒。 --&gt;
        &lt;!-- %highlight(%-5level):表示输出日志级别,并使用颜色进行高亮显示。%-5level 表示左对齐并固定占用5个字符的空间,不足的部分在右侧填充空格。 --&gt;
        &lt;!-- %blue(%-40logger{40}:%-4line):表示输出记录器名称和代码所在的文件、行号,并使用蓝色进行着色。%-40logger{40} 表示左对齐并固定占用40个字符的空间,不足的部分在右侧填充空格;%-4line 表示左对齐并固定占用4个字符的空间,不足的部分在右侧填充空格。 --&gt;
        &lt;!-- %thread:表示输出线程名称。 --&gt;
        &lt;!-- %green(%-18X{LOG_ID}):表示输出名为 LOG_ID 的线程上下文变量,并使用绿色进行着色。%-18X{LOG_ID} 表示左对齐并固定占用18个字符的空间,不足的部分在右侧填充空格。 --&gt;
        &lt;!-- %msg%n:表示输出日志消息和换行符。%msg 表示输出日志事件的消息部分,%n 表示换行符。 --&gt;
        &lt;!-- 综合起来,该格式的日志输出包含了日期、时间、日志级别、记录器名称、代码所在的文件和行号、线程名称、线程上下文变量 LOG_ID 以及日志消息。各个部分通过特定的格式和颜色进行了美化和区分。 --&gt;
        &lt;Pattern&gt;%d{mm:ss.SSS} %highlight(%-5level) %blue(%-40logger{40}:%-4line) %thread %green(%-18X{LOG_ID}) %msg%n&lt;/Pattern&gt;
    &lt;/encoder&gt;
&lt;/appender&gt;

&lt;!-- 定义名为 TRACE_FILE 的滚动文件 appender,包含整个项目的日志 --&gt;
&lt;!-- 在日志系统中,滚动文件(RollingFile)是一种日志输出策略,用于处理日志文件的大小或时间滚动。这种策略旨在避免单个日志文件过大,或者按时间生成新的日志文件,以便更好地管理和维护日志记录。 --&gt;
&lt;appender name=&quot;TRACE_FILE&quot; class=&quot;ch.qos.logback.core.rolling.RollingFileAppender&quot;&gt;
    &lt;!-- 定义日志文件路径 --&gt;
    &lt;file&gt;${PATH}/trace.log&lt;/file&gt;
    &lt;!-- 定义滚动策略为基于时间的滚动 --&gt;
    &lt;rollingPolicy class=&quot;ch.qos.logback.core.rolling.TimeBasedRollingPolicy&quot;&gt;
        &lt;!-- 定义日志文件名的规则 --&gt;
        &lt;!-- trace:这是日志文件的基本名称,文件名将以此开始。 --&gt;
        &lt;!-- ${PATH}:这是一个占位符,表示引用之前定义的属性 PATH。在这个配置中,PATH 被设置为 ./log,所以 ${PATH} 将被解析为 ./log。 --&gt;
        &lt;!-- %d{yyyy-MM-dd}:这是一个日期格式化的占位符,表示在文件名中插入当前日期,格式为 yyyy-MM-dd。 --&gt;
        &lt;!-- %i:这是用于在文件名中插入滚动索引的占位符。当日志文件大小达到指定的限制时,会创建一个新的日志文件,索引会递增。 --&gt;
        &lt;!-- .log:这是日志文件的扩展名。 --&gt;
        &lt;FileNamePattern&gt;${PATH}/trace.%d{yyyy-MM-dd}.%i.log&lt;/FileNamePattern&gt;
        &lt;!-- 定义时间和大小的滚动触发策略 --&gt;
        &lt;timeBasedFileNamingAndTriggeringPolicy class=&quot;ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP&quot;&gt;
            &lt;!-- 定义最大文件大小,每 10MB 生成一个新的文件 --&gt;
            &lt;maxFileSize&gt;10MB&lt;/maxFileSize&gt;
        &lt;/timeBasedFileNamingAndTriggeringPolicy&gt;
    &lt;/rollingPolicy&gt;
    &lt;!-- 定义日志输出格式 --&gt;
    &lt;layout&gt;
        &lt;pattern&gt;%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %-50logger{50}:%-4line %green(%-18X{LOG_ID}) %msg%n&lt;/pattern&gt;
    &lt;/layout&gt;
&lt;/appender&gt;

&lt;!-- 定义名为ERROR_FILE的滚动文件 appender,包含整个项目错误相关的日志 --&gt;
&lt;appender name=&quot;ERROR_FILE&quot; class=&quot;ch.qos.logback.core.rolling.RollingFileAppender&quot;&gt;
    &lt;!-- 定义日志文件路径 --&gt;
    &lt;file&gt;${PATH}/error.log&lt;/file&gt;
    &lt;!-- 定义滚动策略为基于时间的滚动 --&gt;
    &lt;rollingPolicy class=&quot;ch.qos.logback.core.rolling.TimeBasedRollingPolicy&quot;&gt;
        &lt;!-- 定义日志文件名的规则 --&gt;
        &lt;FileNamePattern&gt;${PATH}/error.%d{yyyy-MM-dd}.%i.log&lt;/FileNamePattern&gt;
        &lt;!-- 定义时间和大小的滚动触发策略 --&gt;
        &lt;timeBasedFileNamingAndTriggeringPolicy class=&quot;ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP&quot;&gt;
            &lt;!-- 定义最大文件大小 --&gt;
            &lt;maxFileSize&gt;10MB&lt;/maxFileSize&gt;
        &lt;/timeBasedFileNamingAndTriggeringPolicy&gt;
    &lt;/rollingPolicy&gt;
    &lt;!-- 定义日志输出格式 --&gt;
    &lt;layout&gt;
        &lt;pattern&gt;%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %-50logger{50}:%-4line %green(%-18X{LOG_ID}) %msg%n&lt;/pattern&gt;
    &lt;/layout&gt;
    &lt;!-- 定义过滤器,只接受 ERROR 级别的日志 --&gt;
    &lt;filter class=&quot;ch.qos.logback.classic.filter.LevelFilter&quot;&gt;
        &lt;level&gt;ERROR&lt;/level&gt;
        &lt;onMatch&gt;ACCEPT&lt;/onMatch&gt;
        &lt;onMismatch&gt;DENY&lt;/onMismatch&gt;
    &lt;/filter&gt;
&lt;/appender&gt;

&lt;!-- 根日志设置,输出 ERROR 级别的日志到 ERROR_FILE appender --&gt;
&lt;root level=&quot;ERROR&quot;&gt;
    &lt;appender-ref ref=&quot;ERROR_FILE&quot; /&gt;
&lt;/root&gt;

&lt;!-- 根日志设置,输出 TRACE 级别的日志到 TRACE_FILE appender --&gt;
&lt;root level=&quot;TRACE&quot;&gt;
    &lt;appender-ref ref=&quot;TRACE_FILE&quot; /&gt;
&lt;/root&gt;

&lt;!-- 根日志设置,输出 INFO 级别的日志到 STDOUT appender --&gt;
&lt;root level=&quot;INFO&quot;&gt;
    &lt;appender-ref ref=&quot;STDOUT&quot; /&gt;
&lt;/root&gt;

</configuration>

配置完成之后,控制台打印的日志样式如下:

image-20231120094357384

请求参数和响应打印 {#请求参数和响应打印}

打印每次客户端发起请求所携带的参数和处理完毕后的响应,并统计处理本次请求所用的时间。

pox.xml中引入以下依赖:

<!-- hutool 工具包-->
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.16</version>
</dependency>

<!-- fastjson 工具--> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>2.0.42</version> </dependency>

<!-- AOP 依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>

新建aspect包,并在包下新建LogAspect类:

import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.support.spring.PropertyPreFilters;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;

/**

  • 打印日志切面类

  • @author ldwcool */ @Aspect @Component public class LogAspect {

    private final static Logger LOG = LoggerFactory.getLogger(LogAspect.class);

    /**

    • 定义一个切点
    • 注解@Pointcut: 这是一个 Spring AOP 注解,用于定义切点。
    • "execution(public * cool.ldw..Controller.(..))": 这是切点表达式,指定了切点的匹配规则。让我们逐步解释:
    • execution: 表示切点的类型,即在方法执行时触发切面。
      
    • public *: 表示方法的修饰符,这里指定了只匹配 public 修饰的方法。
      
    • cool.ldw..: cool.ldw 是包名,..表示任意子包。这样的写法匹配了以 cool.ldw 开头的任何包。
      
    • *Controller: 匹配以 Controller 结尾的类名。
      
    • *(..): 匹配任意方法名,且方法参数任意。
      
    • 综合起来,这个切点表达式的意思是匹配任何以 cool.ldw 开头的包中,类名以 Controller 结尾且方法是 public 修饰的任意方法。
    • 这通常用于定义一个切面,以便在这些 Controller 的方法执行前后执行额外的逻辑,比如日志记录、性能监控等。 */ @Pointcut("execution(public * cool.ldw..Controller.(..))") public void controllerPointcut() { }

    /**

    • 使用@Before注解标注,会在目标方法执行前进行处理

    • @param joinPoint 切点 */ @Before("controllerPointcut()") public void doBefore(JoinPoint joinPoint) { // 开始打印请求日志

      // 获取请求参数 ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); assert attributes != null; HttpServletRequest request = attributes.getRequest(); // 获取方法名 Signature signature = joinPoint.getSignature(); String name = signature.getName();

      // 打印请求信息 LOG.info("------------- 请求开始处理 -------------"); LOG.info("请求地址: {} {}", request.getRequestURL().toString(), request.getMethod()); LOG.info("类名方法: {}.{}", signature.getDeclaringTypeName(), name); LOG.info("远程地址: {}", request.getRemoteAddr());

      // 打印请求参数 Object[] args = joinPoint.getArgs(); // LOG.info("请求参数: {}", JSONObject.toJSONString(args));

      // 排除特殊类型的参数,如文件类型 Object[] arguments = new Object[args.length]; for (int i = 0; i < args.length; i++) { if (args[i] instanceof ServletRequest || args[i] instanceof ServletResponse || args[i] instanceof MultipartFile) { continue; } arguments[i] = args[i]; }

      // 排除字段,敏感字段或太长的字段不显示:身份证、手机号、邮箱、密码等 String[] excludeProperties = {}; // 创建 PropertyPreFilters 对象,用于过滤属性 PropertyPreFilters filters = new PropertyPreFilters(); // 创建 MySimplePropertyPreFilter 实例,并添加到过滤器中 PropertyPreFilters.MySimplePropertyPreFilter excludeFilter = filters.addFilter(); // 将要排除的属性添加到过滤器中 excludeFilter.addExcludes(excludeProperties); // 使用 JSONObject.toJSONString 方法将结果对象转换为 JSON 字符串,并在日志中打印 LOG.info("请求参数: {}", JSONObject.toJSONString(arguments, excludeFilter)); }

    /**

    • 使用@Around注解标注,表示这是一个环绕通知,会在目标方法执行前后进行处理

    • @param proceedingJoinPoint 提供了对目标方法执行过程的控制和访问的接口

    • @return 目标方法执行的结果

    • @throws Throwable 抛出异常,允许在环绕通知中处理异常 */ @Around("controllerPointcut()") public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { // 记录方法执行开始时间 long startTime = System.currentTimeMillis();

      // 执行目标方法,proceed方法会继续执行目标方法,如果不调用,目标方法将不会执行 Object result = proceedingJoinPoint.proceed();

      // 排除字段,敏感字段或太长的字段不显示:身份证、手机号、邮箱、密码等 String[] excludeProperties = {}; PropertyPreFilters filters = new PropertyPreFilters(); PropertyPreFilters.MySimplePropertyPreFilter excludeFilter = filters.addFilter(); excludeFilter.addExcludes(excludeProperties); LOG.info("返回结果: {}", JSONObject.toJSONString(result, excludeFilter)); LOG.info("------------- 请求处理结束 耗时:{} ms -------------", System.currentTimeMillis() - startTime);

      // 返回目标方法执行的结果 return result; }

}

接在再新建一个包filter,并创建LogIdFilter类。

由于过滤器的优先级高于 AOP 切面类,因此在请求刚被处理时,就由日志过滤器 LogIdFilter设置好了日志流水号。这确保了在处理请求过程中,日志流水号能够被及时打印,从而方便追踪和调试。

使用@Order(Ordered.HIGHEST_PRECEDENCE)确保此过滤器优先级最高,最先被执行。

import cn.hutool.core.util.RandomUtil;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.MDC;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

/**

  • 日志过滤器,设置日志流水号

  • 注解@Order(Ordered.HIGHEST_PRECEDENCE) 确保此过滤器最先被执行

  • @author ldwcool */ @Order(Ordered.HIGHEST_PRECEDENCE) @Component public class LogIdIFilter extends OncePerRequestFilter {

    /**

    • 在请求处理过程中的过滤器方法,用于增加日志流水号和设置日志自定义参数 LOG_ID。

    • 为什么要在过滤器中设置 LogId 呢?因为过滤器的优先级高。

    • 在过滤器中设置 LogId 并将当前过滤器优先级设置为最高 可以在处理一次请求中尽可能早得设置好日志流水号,方便查询一次请求的所有日志。

    • @param request HTTP 请求对象

    • @param response HTTP 响应对象

    • @param filterChain 过滤器链,用于继续处理请求

    • @throws ServletException 如果发生 Servlet 异常

    • @throws IOException 如果发生 I/O 异常 */ @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 增加日志流水号,设置日志自定义参数 LOG_ID。 // 这里的 LOG_ID 就是 resource 目录下 logback-spring.xml 日志文件配置中 <Pattern> 标签下配置的 LOG_ID 线程上下文变量。 MDC.put("LOG_ID", System.currentTimeMillis() + RandomUtil.randomString(5));

      filterChain.doFilter(request, response); } }

配置完毕之后重启应用,当客户端发起请求时将会打印如下日志:

image-20231121235017037

可以搜索日志流水号,如上图中的1700581649227m24qd找出某一次请求的日志打印记录。

参考资源 {#参考资源}

赞(3)
未经允许不得转载:工具盒子 » SpringBoot 3.0 日志相关配置