王富贵

Stay hungry,Stay foolish

若你自认理性且正确,那更应该心平气和,言之有据地与人交流,即使不能立刻说服对方,但也至少埋下了一颗种子,冷嘲热讽或居高临下的态度,总会加大人与人之间的隔阂,最终只是在自己的小圈子中制造回声。愿我们最终都能成为乐观且包容的人,愿意耐心听取他人的苦衷和心声。
  menu
69 文章
0 浏览
0 当前访客
ღゝ◡╹)ノ❤️

java日志规范使用

日志

御风大世界:JAVA程序员如何正确打日志?日志框架选择,日志级别,日志实操,MDC源码解析

springboot集成日志门面dmeo:wfg-log-demo

1、java日志简介

1.1、基本概念

什么是日志:

日志是一个很重要的东西,是在java程序运行过程中用来记录系统操作事件的文件集合,可分为事件日志和消息日志。具有处理历史数据、诊断问题的追踪以及理解系统的活动等重要作用

日志的作用:

  • 调试

    在Java项目调试时,查看栈信息可以方便地知道当前程序的运行状态,输出的日志便于记录程序在之前的运行结果。如果你大量使用System.out或者System.err,这是一种最方便最有效的方法,但显得不够专业

  • 错误定位

    不要以为项目能正确跑起来就可以高枕无忧,项目在运行一段时候后,可能由于数据问题,网络问题,内存问题等出现异常。这时日志可以帮助开发或者运维人员快速定位错误位置,提出解决方案

  • 数据分析

    大数据的兴起,使得大量的日志分析成为可能,ELK也让日志分析门槛降低了很多。日志中蕴含了大量的用户数据,包括点击行为,兴趣偏好等,用户画像对于公司下一步的战略方向有一定指引作用

当我们的系统变的复杂的之后,难免会集成其他的系统,不同的系统之间可能会使用不同的日志系统。那么在一个系统中,我们的日志框架可能会出现多个,会出现混乱,而且随着时间的发展,可能会出现新的效率更高的日志系统,如果我们想切换代价会非常的大。如果我们的日志系统能和jdbc一样,有一套自己的规范,其他实现均按照规范去实现,就能很灵活的使用日志框架了

日志门面就是为了解决这个问题而出现的一种技术,日志门面是规范,其他的实现按照规范实现各自的日志框架即可,我们程序员基于日志门面编程即可。举个例子:日志门面就好比菜单,日志实现就好比厨师,我们去餐馆吃饭按照菜单点菜即可,厨师是谁其实不重要,但是有一个符合我口味的厨师当然会更好

java日志体系如下:

java中日志将打印日志接口剥离出来了,分为日志的门户和日志的实现

  1. 门户用来提供通用的接口,将日志的使用方法统一起来,相当于接口类
  2. 实现就相当于接口的实现,他会将门户的内容,真正的输出出来,其中,日志的实现会有性能差异

常见日志:

  1. 门户: JCL(java官方)、slf4j
  2. 实现:JUL、log4j、logback、log4j2

1

无论谁编写推出的东西都希望拥有一套标准而不是依赖人家的代码进行运行,事实上,这些日志实现并不老实,log4j2 以及 logback 等等日志实现均有自己实现的一套标准(标准也就是门面),我们在使用的时候主要不要使用日志实现的API,而是使用日志门面的API。那么当实现出现问题的时候,我们能够无感知的替换日志实现。

2、日志门面

2.1、JCL日志门面

全称为Jakarta Commons Logging,是Apache提供的一个通用日志API,改日志门面的使用并不是很广泛

它是为所有的Java日志实现提供一个统一的接口,它自身也提供一个日志的实现,但是功能非常常弱(SimpleLog),所以一般不会单独使用它。他允许开发人员使用不同的具体日志实现工具: Log4j, Jdk 自带的日志(JUL)

JCL 有两个基本的抽象类:Log(基本记录器)和LogFactory(负责创建Log实例)

2.1.1、简单使用

pom依赖

<dependency>
    <groupId>commons-logging</groupId>
    <artifactId>commons-logging</artifactId>
    <version>1.2</version>
</dependency>

基础代码

public class JULTest {
    @Test
    public void testQuick() throws Exception {
        // 创建日志对象
        Log log = LogFactory.getLog(JULTest.class);
        // 日志记录输出
        log.fatal("fatal");
        log.error("error");
        log.warn("warn");
        log.info("info");
        log.debug("debug");
    }
}

2.2、SLF4J日志门面

SLF4J是目前市面上最流行的日志门面。现在的项目中,基本上都是使用SLF4J作为我们的日志系统

SLF4J日志门面主要提供两大功能:

  1. 日志框架的绑定
  2. 日志框架的桥接

2.2.1、SLF4J实战

我们采用log4j

maven依赖

<!--slf4j门面api-->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
</dependency>
<!--log4j实现-->
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j2-core</artifactId>
</dependency>

简单实现

public class TestSlf4j {

    // 声明日志对象
    public final static Logger LOGGER =
            LoggerFactory.getLogger(TestSlf4j.class);
    @Test
    public void testSlfSimple()  {
        //打印日志信息
        LOGGER.error("error");
        LOGGER.warn("warn");
        LOGGER.info("info");
        LOGGER.debug("debug");
        LOGGER.trace("trace");
        // 使用占位符输出日志信息
        String name = "lucy";
        Integer age = 18;
        LOGGER.info("{}今年{}岁了!", name, age);
        // 将系统异常信息写入日志
        try {
            int i = 1 / 0;
        } catch (Exception e) {
            // e.printStackTrace();
            LOGGER.error("出现异常:", e);
        }
    }
}

2.2.3、绑定其他日志的实现

我们上面只是简单的使用log4j2官方的实现来实现,同时我们知道使用其他实现方式也是可以的

绑定jul的实现

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.27</version>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-jdk14</artifactId>
    <version>1.7.25</version>
</dependency>

绑定log4j的实现

<!--slf4j core 使用slf4j必須添加-->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.27</version>
</dependency>
<!-- log4j-->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>1.7.27</version>
</dependency>
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>

要切换日志框架,只需替换类路径上的slf4j绑定。例如,要从java.util.logging切换到log4j,只需将slf4j-jdk14-1.7.27.jar替换为slf4j-log4j12-1.7.27.jar即可

2.2.4、桥接门面

我们知道,原来的项目可能使用别的门面,现在我们想切换到slf4j门面来实现

桥接解决的是项目中日志的遗留问题,当系统中存在之前的日志API,可以通过桥接转换到slf4j的实现

  1. 先去除之前老的日志框架门面的依赖,必须去掉
  2. 添加SLF4J提供的桥接组件,这个组件就是模仿之前老的日志写了一套相同的api,只不过这个api是在调用slf4j的api
  3. 为项目添加SLF4J的具体实现

其实底层原理就是接替原来门面的接口

迁移的方式:

<!-- 桥接的组件 -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>log4j-over-slf4j</artifactId>
    <version>1.7.27</version>
</dependency>

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.27</version>
</dependency>

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-simple</artifactId>
    <version>1.7.27</version>
</dependency>

SLF4J提供的桥接器:

<!-- log4j-->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>log4j-over-slf4j</artifactId>
    <version>1.7.27</version>
</dependency>
<!-- jul -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>jul-to-slf4j</artifactId>
    <version>1.7.27</version>
</dependency>
<!--jcl -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>jcl-over-slf4j</artifactId>
    <version>1.7.27</version>
</dependency>

3、日志实现

3.1、Logback

Logback是由log4j创始人设计的另一个开源日志组件,性能比log4j要好

Logback主要分为三个模块:

  • logback-core:其它两个模块的基础模块
  • logback-classic:它是log4j的一个改良版本,同时它完整实现了slf4j API
  • logback-access:访问模块与Servlet容器集成提供通过Http来访问日志的功能 后续的日志代码都是通过SLF4J日志门面搭建日志系统,所以在代码是没有区别,主要是通过修改配置文件和pom.xml依赖

3.1.1.使用Logback

添加依赖

<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.3</version>
</dependency>

java代码

public class TestLogback {

    private final static Logger logger = LoggerFactory.getLogger(TestLog4j.class);
  
    @Test
    public void testLogback(){
        //打印日志信息
        logger.error("error");
        logger.warn("warn");
        logger.info("info");
        logger.debug("debug");
        logger.trace("trace");
    }
}

其实我们发现即使项目中没有引入slf4j我们这里也是用的slf4j门面进行编程

3.2、log4j2

Apache Log4j2是对Log4j的升级版,参考了logback的一些优秀的设计,并且修复了一些问题,因此带来了一些重大的提升,主要有:

  • 异常处理,在logback中,Appender中的异常不会被应用感知到,但是在log4j2中,提供了一些异常处理机制
  • 性能提升, log4j2相较于log4j 和logback都具有很明显的性能提升,后面会有官方测试的数据
  • 自动重载配置,参考了logback的设计,当然会提供自动刷新参数配置,最实用的就是我们在生产 上可以动态的修改日志的级别而不需要重启应用

4、SpringBoot使用log4j2

SpringBoot默认是使用slf4j2门面,logback作为日志实现的,那么我们需要使用log4j2作为门面该怎么办呢?

pom依赖

  1. 排除logback
  2. 添加log4j2
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
    <exclusions>
        <!--排除默认的logback-->
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </exclusion>
    </exclusions>
</dependency>
 <!--log4j2-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>

那么springboot默认实际上引入了 日志门面SLF4J , 使用 日志实现logback ,我们的目的是将实现替换,因此只要把 logback 排除,引入 log4j2 就可以了。

简单使用

使用@Slf4j默认注入门面的api

使用门面api输出即可

@SpringBootTest
@Slf4j
public class logTest {
    @Test
    public void test(){
        log.info("输出");
        log.warn("输出");
        log.error("输出");
        log.debug("输出");
        log.trace("输出");
    }
}

5、日志打印标准使用

5.1、阿里日志规约

  1. 应用中不可直接使用日志系统(Log4j、Logback)中的API,而应依赖使用日志框架SLF4J中的API。使用门面模式的日志框架,有利于维护和各个类的日志处理方法统一
  2. 日志文件推荐至少保存15天,因为有些异常具备以“周”为频次发生的特点
  3. 应用中的扩展日志(如打点、临时监控、访问日志等)命名方式:appName_logType_logName.log。logType为日志类型,推荐分类有stats/monitor/visit 等
  4. logName为日志描述。这种命名的好处:通过文件名就可以知道日志文件属于哪个应用,哪种类型,有什么目的,这也有利于归类查找
  5. 对trace/debug/info级别的日志输出,必须使用条件输出形式或者占位符的方式
  6. 避免重复打印日志,否则会浪费磁盘空间。务必在日志配置文件中设置 additivity=false
  7. 异常信息应该包括两类:案发现场信息和异常堆栈信息。如果不处理,那么通过关键字向上抛出
  8. 谨慎地记录日志。生产环境禁止输出debug日志;有选择地输出info日志;如果使用warn记录刚上线时的业务行为信息,一定要注意日志输出量的问题,避免吧服务器磁盘撑爆,并及时删除这些观察日志
  9. 可以使用warn日志级别记录用户输入参数错误的情况,避免当用户投诉时无所适从

5.2、基本的日志使用

如今,我们常用的日志门面是 SLF4J ,而日志的实现选择的余地非常大的,甚至可能出现用到一半替换的情况。所以我们使用 日志门面 SLF4J ,springboot默认的日志实现 logback 来进行演示,此外,我们一定要使用日志门面 SLF4J 的API (阿里日志规约第一条)

要输出日志,我们就需要将日志注入进来,如下所示。

@Service
public class LogServiceImpl {
    // 通过日志工厂获取日志实现
    private static final Logger log = LoggerFactory.getLogger(LogServiceImpl.class);
}

当然,如果你使用了 lombok 插件,使用注解 @slf4j 就可以编译时候自动注入进来。其底层就是编译的时候使用即时编译技术替代我们将log类注入。

@Service
@Slf4j // 注入slf4j日志门面实现
public class LogServiceImpl {
}

5.2.1、日志接口的api

日志接口的api通常来说有5种,分别对应五种不同的日志

  1. info:输出普通日志
  2. warn(警告):本不应该出错,但好在程序还在运行
  3. error(错误):非常严重的错误日志,程序已经超过想象的范围非正常或者停止运行了,一定要记录并进行监控并发布警告
  4. debug:用于debug(排除bug)的日志,使用最好判断一下配置文件等等的dbug日志是否开启 (阿里日志规约第八条)
  5. trace:用于问题追踪

样例如下:

@Service
@Slf4j
public class LogServiceImpl {
    public void baseLog(){
        log.info("没事儿,就是想打个日志");
        log.error("这个错误很严重,程序阻断了,要记录监控预警");
        log.warn("这个错误不应该,但程序还会往下走,有点丢面子");
        log.debug("debug日志,在开发阶段输出,不过你要是线上有这个日志就等死吧");
        log.trace("为了追踪?但是info和debug不行吗?");
    }
}

5.2.2、规范的打印日志

我们在工作中,需要标准的打印日志,这有利于我们排除问题,解决快速线上bug。下面这个示例能够日常开发中打印日志的场景,我们需要注意一下几点:

  1. 接口开头打印 方法名接口入参
  2. 对重要的分支条件打印当前进入了哪个分支(dbug的时候用)
  3. 重要的API调用也可以打印入参和结果响应(当然也可以在方法内部进行)
  4. 在异常捕获中,使用error日志去替代堆栈信息打印收集
  5. 请求结束相应使用日志输出结果响应内容
@Slf4j
@RestController
public class LogInfController {

    @PostMapping("/log")
    public void log(@RequestBody JSONObject request) {
        // 1.日志开头输出入参
        log.info("log() called with params => [request = {}]",request);

        // 2.对重要的分支条件进行日志输出进入了哪个分支
        Boolean isVip = Boolean.TRUE;
        if (isVip) {
            log.info("vip biz");
        } else {
            log.info("not vip biz");
        }

        // 3.重要的远程调用日志打印入参和结果
        log.info("callRemote() called with params => [request = {}]",request);
        JSONObject result = callRemote(request); // 执行远程调用
        log.info("log() returned => [{}]", request);

        // 4.异常捕获中使用错误日志替换堆栈信息打印收集
        try {
            getOrder(); // 模拟尝试获取订单
        } catch (Exception e) {
            // e.printStackTrace(); 前往不要打印错误堆栈了,不然leader捶你
            log.error("get order error");
            // throw new RuntimeException(e); 我们可以抛个异常去捕获,给用户一个提示信息
        }

        // 5.日志结尾输出响应内容
        log.info("log() returned => [{}]", request);
    }

    /**
     * 模拟远程调用方法
     */
    private JSONObject callRemote(JSONObject request) {
        System.out.println("我是远程调用");
        return request;
    }

    /**
     * 模拟一个可能出现异常的调用
     */
    private void getOrder(){
        if (new Random().nextBoolean()){
            throw new RuntimeException("随处给你整个错");
        }
    }
}

Tips:实际上日志的入参和相应结果我们可以使用spring的aop来完成,这样我们就不需要重复的写了.当然,在使用aop的过程会遇到字符串拼接参数的问题,这样性能会低一点,后续我们会讲到

5.2.3、满足规约的小细节

我们还有一些在日志打印过程中的小细节需要了解一下

  1. 使用占位符拼接字符串而非使用普通加号拼接
  2. dbug日志一定需要判断是否需要输出
@Slf4j
@RestController
public class OtherLogInfController {
    @PostMapping("/")
    public void otherLog(){
        String name = "王富贵";
        // 1.使用占位符拼接字符串
        // 错误:使用加号拼接字符串
        log.info("我的名字:"+name);
        // 正确:使用 {} 占位符拼接字符串
        log.info("我的名字:{}",name);

        // 2.dbug级别日志需要判断是否开启,线上环境禁止输出dbug日志
        // 错误:直接输出了dbug日志
        log.debug("我是dbug日志");

        // 正确:判断dbug日志是否开启。通常在配置配置文件中或者配置类中控制开启按钮
        if (log.isDebugEnabled()) {
            log.debug("我是dbug日志");
        }


        // 正确示范
        if (log.isDebugEnabled()) {
            log.debug("dbug员的名字为:{}",name);
        }
    }
}

之所以不使用加号拼接是因为使用加号实际上是调用 new String() 而非 String.append() 这个方法进行拼接,我们知道 String 是一个被 final 修饰的变量,因此,在使用追加的时候jvm会重新创建一个新的字符串对象,这种形式非常的消耗性能,因此我们不推荐使用字符串追加的形式进行拼接。

5.3、springboot使用日志

在依托于springboot的自动配置,我们在框架中使用日志是非常便捷的,步骤如下:

  1. 引入对应的依赖(springboot默认使用SLF4J日志门面,Logback日志实现,如果需要更改,请做对应的移除)
  2. 配置配置文件
    1. 通常情况日志配置更倾向于采用xml文件的方式
    2. yaml或者properties配置文件部分实现支持

5.3.1、springboot使用Log4j2

log4j2-配置详情

我们好好了解一下想要线上使用日志框架,应该怎么做

1.引入对应的依赖

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
        <exclusions>
            <!--排除logback依赖-->
            <exclusion>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-logging</artifactId>
            </exclusion>
        </exclusions>
    </dependency>

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

2.配置配置文件

我们采用xml的方式来配置,配置文件配置的大致内容如下

  • 日志输出样式
  • 日志输出的路径,以及日志文件大小等
  • 日志的级别(info,debug等)

首先我们需要编写一个xml,文件名为 log4j2-spring.xml 或者 log4j2.xml 放在 resource 目录下。如果你想要使用其他目录,就需要在配置文件中指定日志配置的地址:logging.config= classpath:log4j2.xml

那么我们的xml示例如下:

<?xml version="1.0" encoding="UTF-8"?>
<configuration status="WARN" monitorInterval="30">
    <!--先定义所有的appender-->
    <appenders>
        <!--控制台日志-->
        <console name="Console" target="SYSTEM_OUT">
            <!--输出日志的格式-->
            <PatternLayout pattern="%date{HH:mm:ss.SSS}  %-5level [%thread] %logger{36} - %msg%n"/>
        </console>

        <!--文件日志-->
        <!--同步日志-->
        <RollingFile name="RollingFileInfo" fileName="./logs/log4j2-sync-rolling.log"
                     filePattern="${sys:user.home}/logs/$${date:yyyy-MM}/info-%d{yyyy-MM-dd}-%i.log">
            <!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)-->
            <ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/>
            <PatternLayout pattern="%date{HH:mm:ss.SSS}  %-5level [%thread] %logger{36} - %msg%n"/>
            <Policies>
                <TimeBasedTriggeringPolicy/>
                <SizeBasedTriggeringPolicy size="100 MB"/>
            </Policies>
        </RollingFile>

        <!--同步日志-->
        <File name="SyncFile" fileName="./logs/log4j2-sync.log" append="false">
            <PatternLayout pattern="%date{HH:mm:ss.SSS}  %-5level [%thread] %logger{36} - %msg%n"/>
        </File>

    </appenders>
    <!--然后定义logger,只有定义了logger并引入的appender,appender才会生效-->
    <loggers>
        <!--日志的输出级别,INFO,DEBUG等等-->
        <root level="INFO">
            <appender-ref ref="Console"/>
            <appender-ref ref="SyncFile"/>
            <appender-ref ref="RollingFileInfo"/>
        </root>
    </loggers>
</configuration>

配置完成之后,我们就可以正常的输出日志了。

5.4、分布式日志

随着微服务盛行,很多公司都把系统按照业务边界拆成了很多微服务,在排错查日志的时候。因为业务链路贯穿着很多微服务节点,导致定位某个请求的日志以及上下游业务的日志会变得有些困难。

那么这个问题的最优解,就是考虑上SkyWalking,Pinpoint等分布式追踪系统来解决,基于OpenTracing规范,而且通常都是无侵入性的,并且有相对友好的管理界面来进行链路Span的查询。

但是搭建分布式追踪系统,熟悉以及推广到全公司的系统需要一定的时间周期,而且当中涉及到链路span节点的存储成本问题,全量采集还是部分采集?如果全量采集,就以SkyWalking的存储来举例,ES集群搭建至少需要5个节点。这就需要增加服务器成本。况且如果微服务节点多的话,一天下来产生几十G上百G的数据其实非常正常。如果想保存时间长点的话,也需要增加服务器磁盘的成本。

那么如果你想要低成本或者快速构建分布式日志解决方案,或者来不及构建链路追踪服务的话,就可以考虑使用MDC模式或者日志标签框架来解决这个问题。

MDC模式实际上也是给日志打一个流水标签(给单个请求链路服务标识id),让程序员在排错的时候能够快速定位请求

总结一下,解决分布式日志的方案有三种

  • 日志框架实现的简单的MDC模式
  • 日志标签框架,如TLog(拓展了MDC)
  • 分布式链路日志监控中间件(最终方案,最好的方案,最贵的方案)

5.4.1、MDC

MDC日志是日志门面SLF4J实现的功能,因此我们也是建议使用SFL4J门面,而JCL门面就没有MDC实现。

日志框架的MDC实际上就是给日志记录一个流水号,当然,这个流水号需要我们自己生成,在分布式中就需要请求顶端节点生成唯一的日志ID。那么MDC的作用线来了解一下怎么使用吧。

/**
 * @author: 王富贵
 * @description: 日志拦截器,把唯一id号放入ThreadLocal
 * @createTime: 2022年12月05日 15:41:52
 */
@Component
public class LogInterceptor implements AsyncHandlerInterceptor {
    /**
     * 执行处理程序之前,注入唯一ID
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        // 注入一个Entry,value就是请求流水号
        MDC.put("trance",request.getRequestURI());
        return true;
    }

    /**
     * 处理了之后
     */
    @Override
    public void afterConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response, Object handler) {
        MDC.clear();
    }
}

如图所示,我们注册了一个拦截器,实现了两个方法,请求进入之前和请求完成之后。

  • 请求进入之前:我们使用MDC工具(直接调用静态方法),能够直接put一个流水号
  • 请求完成之后:我们必须将MDC清理
  • 千万不要忘记将拦截器注入容器,比如使用 @Compoent 注解

那么这样做实际上我们就可以在业务当中获取到放入MDC的流水号,并在日志中携带输出了

@PostMapping("/MDC")
public void testMDC(){
    // 从MDC能够获取当前请求的之前放入MDC的流水号
    String trance = MDC.get("trance");
    log.info("我从MDC来获取当前请求的流水号:{}",trance);
}
5.4.1.1、MDC原理

不难发现,MDC的put和get方法一定是某一个map的实现,而处理结尾我们必须清理MDC的值,这与ThreadLocal非常的相近,因此,MDC实际上就是一个请求前注入的ThreadLocal,以便我们能够在业务场景中随时获取当前请求唯一流水ID并输出日志。

我们可以了解一下再MDC工具内部的方法,我们看看put方法的实现其他方法就可以大致推测出来了:

public static void put(String key, String val) throws IllegalArgumentException {
    if (key == null) {
        throw new IllegalArgumentException("key parameter cannot be null");
    }
    if (mdcAdapter == null) {
        throw new IllegalStateException("MDCAdapter cannot be null. See also " + NULL_MDCA_URL);
    }
    // 使用mdcAdapter进行put
    mdcAdapter.put(key, val);
}

mdcAdapter 实际上是一个接口,因为SLF4J只是一个日志门面,只存在接口而不存在实现,而 mdcAdapter 需要将日志实现的MDC实现注入进来使用。 SPI机制的应用 我们在项目中引入的是log4j2的实现,因此我们最后使用的就是log4j2的MDC实现,通过 Adapter适配器进行适配注入。 适配器模式

我们就来看看log4j2对MDC的实现:

public class BasicMDCAdapter implements MDCAdapter {
		// 定义了ThreadLocal
    private InheritableThreadLocal<Map<String, String>> inheritableThreadLocal = new InheritableThreadLocal<Map<String, String>>() {
        @Override
        protected Map<String, String> childValue(Map<String, String> parentValue) {
            if (parentValue == null) {
                return null;
            }
            return new HashMap<String, String>(parentValue);
        }
    };

    // put方法就是往ThreadLocalMap中添加
    public void put(String key, String val) {
        if (key == null) {
            throw new IllegalArgumentException("key cannot be null");
        }
        Map<String, String> map = inheritableThreadLocal.get();
        if (map == null) {
            map = new HashMap<String, String>();
            inheritableThreadLocal.set(map);
        }
        map.put(key, val);
    }

    // clear方法是应用ThreadLocal必须的动作,否则可能会出现内存泄漏问题
    public void clear() {
        Map<String, String> map = inheritableThreadLocal.get();
        if (map != null) {
            map.clear();
            inheritableThreadLocal.remove();
        }
    }

}

5.4.2、TLog

Tlog 是一个 轻量级的分布式日志标记追踪神器 ,他在当前日志门面和实现下拓展了一些功能,实际上就是为日志打标签,并标准化了标签的输入输出。而最常见的打标签就是请求流水id号。

想要使用Tlog也非常简单,他也说了是轻量级的,详细的文档请查阅 TLog官方文档

  • 除了日志依赖之外,还需要引入TLog依赖
  • 日志框架适配(使用TLog的配置替代传统实现配置文件)
  • 对日志打标签

1.引入TLog依赖

我们就使用最简单的 spring-boot-web 依赖来使用,当然他还拓展了很多中间件依赖以供你适配使用,可以阅读官方文档。

<dependency>
    <groupId>com.yomahub</groupId>
    <artifactId>tlog-web-spring-boot-starter</artifactId>
    <version>1.5.0</version>
</dependency>

2.日志框架适配

日志框架的适配实际上就是在日志实现配置文件中使用TLog的配置文件来替换,下面这个是log4j2使用TLogMDC的配置文件

简单了解一下区别

  • 日志输出信息占位符从 %msg 变成了 %tmsg
  • TLog输出信息占位符为 %TX{tl}
<?xml version="1.0" encoding="UTF-8"?>
<configuration status="WARN" monitorInterval="30">
    <!--先定义所有的appender-->
    <appenders>
        <!--控制台日志-->
        <console name="Console" target="SYSTEM_OUT">
            <!--输出日志的格式-->
            <PatternLayout pattern="%date{HH:mm:ss.SSS} %TX{tl} %-5level [%thread] %logger{36} - %tmsg%n"/>
        </console>

        <!--文件日志-->
        <!--同步日志-->
        <RollingFile name="RollingFileInfo" fileName="./logs/log4j2-sync-rolling-mdc.log"
                     filePattern="${sys:user.home}/logs/$${date:yyyy-MM}/info-%d{yyyy-MM-dd}-%i.log">
            <!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)-->
            <ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/>
            <PatternLayout pattern="%date{HH:mm:ss.SSS} %TX{tl} %-5level [%thread] %logger{36} - %tmsg%n"/>
            <Policies>
                <TimeBasedTriggeringPolicy/>
                <SizeBasedTriggeringPolicy size="100 MB"/>
            </Policies>
        </RollingFile>

        <!--同步日志-->
        <File name="SyncFile" fileName="./logs/log4j2-sync-mdc.log" append="false" >
            <PatternLayout pattern="%date{HH:mm:ss.SSS} %TX{tl} %-5level [%thread] %logger{36} - %tmsg%n"/>
        </File>

    </appenders>
    <!--然后定义logger,只有定义了logger并引入的appender,appender才会生效-->
    <loggers>
        <root level="INFO">
            <appender-ref ref="Console"/>
            <appender-ref ref="SyncFile"/>
            <appender-ref ref="RollingFileInfo"/>
        </root>
    </loggers>
</configuration>

3.使用注解 @TLogAspect 即可

@GetMapping("/tlog/{id}")
@TLogAspect("id")
public String testTLog(@PathVariable int id) {
    log.info("接收到请求参数了");
    return "成功";
}

那么我们在注解中标识到di之后,日志就会自动记录我们的id流水号了。

当然,他也支持多标签和对象值获取,比如:

@TLogAspect({"person.id","person.age","person.company.department.dptId"})
public void demo(Person person){
  log.info("多参数加多层级示例");
}

TLog还支持更多的标签形式,可以阅读:TLog业务标签官方文档


标题:java日志规范使用
作者:1938857445
地址:https://www.lmlx66.top/articles/2022/12/05/1670237878595.html