JHHK

欢迎来到我的个人网站
行者常至 为者常成

bug菌_日志

目录

Spring Boot 整合 Log4j2

一、为什么要用日志

没有日志不影响程序正确运行,但没有日志的项目是不完整的。日志的作用集中在三件事:

作用 说明
调试 查看栈信息与运行结果,比大量使用 System.out / System.err 更专业
错误定位 运行一段时间后出现数据、网络、内存等异常时,快速定位错误位置
数据分析 日志包含点击行为、兴趣偏好等用户数据,可用于用户画像

二、日志框架与 Log4j2

常见日志框架:Log4j、Log4j2、Slf4j、JDKLog、Logback。

  • Log4j 已停止更新,Apache 推出 Log4j2 作为其替代与升级,同时改进了 Logback 的一些架构问题。
  • 目前的主流组合是 Log4j2 + Slf4j。

Log4j 由三部分构成:

组成部分 含义
日志信息优先级 指定这条日志信息的重要程度(即日志级别)
日志信息输出目的地 控制台、文件、GUI 组件、套接口服务器、NT 事件记录器、UNIX Syslog 守护进程等
日志信息输出格式 控制日志信息的显示内容

这三者都可以通过一个配置文件灵活调整,不需要修改应用代码。

三、日志级别

完整级别顺序:

ALL < TRACE < DEBUG < INFO < WARN < ERROR < FATAL < OFF

其中常用的六个级别:

级别 说明
trace 追踪,最低级别,一般不使用
debug 调试,开发阶段常设为最低级别
info 信息,输出重要信息,使用较多
warn 警告,程序不报错但需要提醒开发者
error 错误,开发中常用
fatal 严重错误,一旦发生程序基本要停止

设置某个级别后,会打印大于等于该级别的日志:例如设为 ERROR 只打印 ERROR、FATAL;设为 ALL 全部打印;设为 OFF 一条都不打印。所以严格来说只有六种日志级别。

四、集成步骤

环境:IDEA 2021.3、JDK 1.8、Spring Boot 2.3.1.RELEASE、Maven 3.8.2。

1. 引入依赖

<!-- log4j2的依赖引入 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>

2. 编写测试用例,输出各级别日志

package com.example.demo;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class Log4j2Test {

    private static Logger logger = LogManager.getLogger(Log4j2Test.class);

    @Test
    void log4j2Test() throws Exception {
        logAll();
    }

    // 打印各种级别的日志用于测试
    public static void logAll() throws Exception {
        // TRACE < DEBUG < INFO < WARN < ERROR < FATAL
        logger.trace("trace level log");
        logger.debug("debug level log");
        logger.info("info level log");
        logger.warn("warn level log");
        logger.error("error level log");
        logger.fatal("fatal level log");
    }
}

3. 业务代码中打印日志

@GetMapping("/demo1")
public void test1() {
    log.info("程序执行咯!记录日志啦");
}

控制台输出:

17:01:22.215 [http-nio-8080-exec-5] INFO com.example.demo.controller.TestController - 程序执行咯!记录日志啦

log4j2.xml 配置

src/main/resources/
├── application.yml
└── log4j2.xml      ← 自己新建
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
  <Appenders>
    <Console name="Console" target="SYSTEM_OUT">
      <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
    </Console>
  </Appenders>
  <Loggers>
    <Root level="ERROR">
      <AppenderRef ref="Console"/>
    </Root>
  </Loggers>
</Configuration>

配置分为两类:

节点 作用
Appenders 定义日志输出目的地、内容和格式
Loggers 定义日志级别以及使用的 Appenders

输出格式与验证

PatternLayout 的 pattern 决定日志格式:

占位符 含义
%date{HH:mm:ss.SSS} 输出日志打印日期,精确到毫秒
[%thread] 当前线程名称
%-5level 级别从左显示 5 个字符宽度
%logger{36} Logger 名称最长 36 个字符,一般是类的全限定路径名
%msg 日志信息
%n 日志换行

按上面的配置输出效果:

17:01:22.215 [http-nio-8080-exec-5] INFO com.example.demo.controller.TestController - 程序执行咯!记录日志啦

注意:示例中 Root level="ERROR",因此 trace / debug / info / warn 都不会输出,只有 error 及以上级别才会打印到控制台。

Spring Boot 整合 LogBack 实现日志本地保存

一、为什么可以直接用 Logback

  • Logback 是 Spring Boot 的默认日志框架:spring-boot-starter-web 已自带 spring-boot-starter-logging(内含 Logback),无需额外引入依赖。
  • 本节目标:日志在输出控制台的同时,按用途分门别类写入本地文件,日后排查问题只需查阅本地日志文件即可。
文件 用途
sys.log 系统日志(root 兜底)
error.log 只记录 ERROR 级别,异常一目了然
api.log 接口日志
demo_system.log 业务包的非 ERROR 操作日志
bak/ 目录 历史日志按天滚动备份

注意:
Logback 与第一章的 Log4j2 是二选一的关系。
若之前按第一章引入了 log4j2,改用 Logback 前需移除 log4j2 依赖并恢复 web starter 的默认依赖,否则两套日志实现会冲突。

二、创建 logback-spring.xml

在 resources 目录下新建 logback-spring.xml(文件名建议保持一致):

src/main/resources/
├── application.yml
└── logback-spring.xml   ← 自己新建

三、配置模板(核心)

<?xml version="1.0" encoding="utf-8"?>

<!--
🔥 核心逻辑:
谁(logger)产生日志 → 发到哪里(appender)

🔥 当前设计:
error.log       → 所有 ERROR
api.log         → Controller
operation.log   → 所有业务日志
sys.log         → 第三方日志
-->

<!--
scan="true":开启配置文件热加载;
scanPeriod="5 minutes":每 5 分钟检测一次本文件是否被修改,
修改后无需重启应用即可生效(生产环境临时调整日志级别非常实用)。
-->
<configuration scan="true" scanPeriod="5 minutes">

    <!-- ================== 日志目录 ================== -->
    <!--
    property:定义变量,后续以 ${LOG_HOME} 引用,集中管理路径。
    相对路径 logs/hellospring 相对于"应用启动时的工作目录"
    (IDEA 中一般是项目根目录;jar 部署时是执行命令所在目录)。
    -->
    <property name="LOG_HOME" value="logs/hellospring"/>


    <!-- ================== 控制台日志 ================== -->
    <!--
    appender:日志输出目的地,name 供 <appender-ref> 引用。
    ConsoleAppender:输出到控制台(System.out),开发期调试用。
    pattern 占位符:
      %d{HH:mm:ss.SSS} 时间(控制台省略日期,保持简洁)
      [%thread]        打日志的线程名
      %-5level         级别左对齐、最小宽 5(各列对齐)
      %logger{36}      logger 名,超 36 字符自动缩写包名,如 c.l.h.UserController
      %msg 日志内容 %n 换行
    -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <!-- 控制台简洁一点 -->
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>


    <!-- ================== 系统日志(第三方)sys.log ================== -->
    <!--
    RollingFileAppender:带滚动(拆分归档)能力的文件输出。
    rollingPolicy 决定"何时以及如何拆分":
    TimeBasedRollingPolicy 按时间滚动,跨天时自动把旧文件
    改名为 bak/sys.log.2026-11-11,新日志继续写入 sys.log。
    注意:这里未配 <maxHistory>,归档永不清理,磁盘会持续增长。
    -->
    <appender name="sysLog" class="ch.qos.logback.core.rolling.RollingFileAppender">

        <!-- 当前正在写入的活动文件(不含日期,日期只出现在归档名里) -->
        <file>${LOG_HOME}/sys.log</file>

        <!-- 每天滚动一次,历史文件归档到 bak/ 子目录 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOG_HOME}/bak/sys.log.%d{yyyy-MM-dd}</fileNamePattern>
        </rollingPolicy>

        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger:%L - %msg%n</pattern>
        </encoder>
    </appender>


    <!-- ================== 错误日志 error.log ================== -->
    <appender name="errorLog" class="ch.qos.logback.core.rolling.RollingFileAppender">

        <!--
        LevelFilter:对日志级别"精确匹配"。
        level=ERROR + onMismatch=DENY → 非 ERROR 一律拒绝;
        onMatch 未配置时默认 NEUTRAL(放行) → 只放行 ERROR。
        效果:error.log 成为全应用统一的错误日志。
        -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <onMismatch>DENY</onMismatch>
        </filter>

        <file>${LOG_HOME}/error.log</file>

        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOG_HOME}/bak/error.log.%d{yyyy-MM-dd}</fileNamePattern>
        </rollingPolicy>

        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger:%L - %msg%n</pattern>
        </encoder>
    </appender>


    <!-- ================== 业务日志 operation.log ================== -->
    <appender name="operationLog" class="ch.qos.logback.core.rolling.RollingFileAppender">

        <!--
        反向过滤:级别匹配到 ERROR 则 DENY,
        其余(INFO/WARN/DEBUG)全部 ACCEPT。
        → error 由 errorLog 专管,本文件只记业务日志,
        避免同一条 ERROR 在两个文件重复出现。
        -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <onMatch>DENY</onMatch>
            <onMismatch>ACCEPT</onMismatch>
        </filter>

        <file>${LOG_HOME}/operation.log</file>

        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOG_HOME}/bak/operation.log.%d{yyyy-MM-dd}</fileNamePattern>
        </rollingPolicy>

        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger:%L - %msg%n</pattern>
        </encoder>
    </appender>


    <!-- ================== 接口日志 api.log ================== -->
    <!--
    不加 filter:Logger 交给它的日志全收(含 ERROR),
    完整记录 Controller 的进/出,便于还原一次请求的完整链路。
    -->
    <appender name="apiLog" class="ch.qos.logback.core.rolling.RollingFileAppender">

        <file>${LOG_HOME}/api.log</file>

        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOG_HOME}/bak/api.log.%d{yyyy-MM-dd}</fileNamePattern>
        </rollingPolicy>

        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger:%L - %msg%n</pattern>
        </encoder>
    </appender>


    <!-- ================== Logger配置 ================== -->

    <!--
    🔥 Controller 层(接口日志)
    logger:按包名前缀匹配把日志路由到指定 appender;
    更具体的包(controller)优先于父包(com.lxy.hellospring)。
    additivity="false":禁止日志继续向父级(root)传播,
    防止 Controller 日志再被 root 写一遍到 sys.log(去重+隔离)。
    -->
    <logger name="com.lxy.hellospring.controller" additivity="false">
        <!-- 控制台 -->
        <appender-ref ref="STDOUT"/>

        <!-- 接口日志 -->
        <appender-ref ref="apiLog"/>

        <!-- 业务日志(保证链路完整) -->
        <appender-ref ref="operationLog"/>

        <!-- 错误日志 -->
        <appender-ref ref="errorLog"/>
    </logger>


    <!--
    🔥 业务层(Service / DAO 等)
    兜底本包其余类:不打到 apiLog,只进 operationLog;
    同样 additivity="false" 阻断向 root 传播。
    -->
    <logger name="com.lxy.hellospring" additivity="false">
        <!-- 控制台 -->
        <appender-ref ref="STDOUT"/>

        <!-- 业务日志 -->
        <appender-ref ref="operationLog"/>

        <!-- 错误日志 -->
        <appender-ref ref="errorLog"/>
    </logger>


    <!-- ================== 根日志(第三方日志) ================== -->

    <!--
    root:所有未显式配置的 logger 的最终兜底——
    Spring/MyBatis 等第三方库的包名不在 com.lxy.hellospring 下,
    全部落到这里。level="INFO" 过滤第三方框架的 DEBUG 噪音;
    它只写控制台和 sys.log,不污染业务/错误日志文件。
    -->
    <root level="INFO">
        <appender-ref ref="STDOUT"/>
        <appender-ref ref="sysLog"/>
    </root>

</configuration>

配置要点:

配置 作用
scan="true" scanPeriod="5 minutes" 配置文件变更后每 5 分钟自动重新加载
<property name="LOG_HOME"> 定义日志根目录变量,后续用 ${LOG_HOME} 引用
ConsoleAppender 控制台输出
RollingFileAppender 滚动文件输出:写当前文件 + 按规则备份历史
TimeBasedRollingPolicy 按 %d{yyyy-MM-dd} 按天滚动,历史文件备份到 bak/
LevelFilter 级别过滤:error.log 只收 ERROR(onMismatch=DENY);operation.log 只收非 ERROR(onMatch=DENY、onMismatch=ACCEPT)
additivity="false" 阻断向父级 logger 传播,避免同一条日志被重复写多份
<root level="INFO"> 全局兜底,未被具体 logger 命中的日志进 sys.log + 控制台

原文把同名 logger(com.example.connectmysqldemo)写成了两段(错误日志一段、操作日志一段),Logback 会把两段的 appender 累加到同一个 logger 上;
这里直接合并为一段,效果一致:ERROR 由 errorLog 的过滤器接走,其余由 opLog 接走。

四、重启验证

  1. 配置完成后刷新 Maven(或关闭项目重新打开),再重启项目,避免误以为日志配置未生效。
  2. 项目目录下生成 logs/springboot-demo 及各日志文件,控制台输出什么,实时写入对应的 log 文件。
  3. 定义一个接口请求验证:
/**
 * 查询db所有用户信息
 */
@GetMapping("/get-users")
@ApiOperation(value = "查询db所有用户信息", notes = "查询db所有用户信息")
public List<UserEntity> getUserList() {
    return userService.getUsers();
}

日志文件中可以清晰看到执行的 SQL 及返回条目数,方便日后 bug 定位。

五、控制台实时打印 SQL

在 application.yml 中把持久层日志级别调到 debug:

# 日志设置
logging:
  level:
    # 持久层日志级别
    com.example.connectmysqldemo.dao: debug

dao 层设为 debug 后,MyBatis 执行的 SQL 会实时打印到控制台(同时按 appender 配置写入对应日志文件)。





R
Valine - A simple comment system based on Leancloud.