Logback 日志框架详解

一、Logback 简介

Logback 是一个日志框架,旨在成为 log4j 的替代品。它由 Ceki Gülcü 创建并维护,是一款开源的日志框架,是 slf4j(Simple Logging Facade for Java)的实现。相比于 log4j,Logback 具有更高的性能和更好的可扩展性,并提供了众多的特性,如异步日志、动态日志级别、决策器等。

在项目中使用 Logback 可以很方便地记录系统运行时的信息、警告和错误等,对于开发人员来说是非常有帮助的。

Logback 的基本概念。

  • Logger:Logger 是 Logback 的核心概念,它用于记录日志。Logger 是按照类的层次结构进行命名的,每个 Logger 对象都有一个名字,如果没有显式指定名字,则使用当前类的全限定名作为它的名字。
  • Appender:Appender 用于定义日志的输出方式,例如输出到控制台、写入文件、发送邮件等。Logback 有多种类型的 Appender,可以同时输出到多个目标。
  • Layout:Layout 用于定义日志输出格式,例如日期格式、日志级别、线程名、类名等。Logback 有多种类型的 Layout,可以根据需求选择使用。
  • Filter:Filter 用于对日志进行过滤,可以根据日志级别、关键字、线程名等条件进行过滤。

二、Logback 的主要功能和特点

1. Logback 的主要功能

Logback 的主要功能包括:

  • 支持多种日志级别:Logback 支持多种日志级别,包括 TRACE、DEBUG、INFO、WARN 和 ERROR 等。
  • 多种日志输出方式:Logback 支持将日志输出到控制台、文件、Syslog、JMS、邮件等多种输出方式,用户可以根据自己的需求选择不同的 Appender。
  • 灵活的配置方式:Logback 的配置文件可以使用 XML 或者 Groovy 编写,非常灵活方便。
  • 高性能:Logback 的性能非常好,可以满足高并发场景下的需求。
  • 精细的过滤功能:Logback 支持使用 Filter 对日志进行精细的过滤操作,可以根据日志级别、线程名、关键字等条件进行过滤。

2. Logback 的特点

Logback 与其他日志系统相比,具有以下几个特点:

  • 高性能:Logback 是目前 Java 日志框架中性能最好的一个,它支持异步输出和无锁数据结构等方式来提升性能。
  • 灵活的配置:Logback 配置文件可以使用 XML 或者 Groovy 编写,非常灵活方便。同时,Logback 还提供了 Web 界面工具 JaninoConfigurer,可以通过 Web 页面来配置 Logback。
  • 多种输出方式:Logback 支持多种输出方式,并且可以自定义 Appender 和 Layout,对于比较复杂的应用场景也可以很好地满足需求。
  • 易于集成:对于 Spring、Hibernate、Lucene 等流行框架,Logback 都提供了官方的集成插件,使用起来非常方便。

三、Logback 常用配置

1. name 属性

Logger 中的 name 属性用于记录器的命名,它是一个字符串,可以用任何字符串来命名 Logger。Logback 中同一个 Logger 可以有多个子 Logger,它们之间构成了一个层次结构,其命名规则按照“类所在包名+类名称”的方式进行。例如:

<logger name="com.demo.UserService" level="DEBUG" additivity="false">
    <appender-ref ref="FILE"/>
    <appender-ref ref="CONSOLE"/>
</logger>

上述配置中,Logger 的 name 属性为 com.demo.UserService,表示一个 UserService 类的 Logger。如果不指定 name,则默认使用 RootLogger,即根 Logger。

2. level 属性

Logger 的 level 属性用于指定输出的日志级别,Logback 日志的级别由低到高分别为 TRACE、DEBUG、INFO、WARN、ERROR、OFF。若设置了 Logger 的 level 属性,则将只输出指定级别及更高级别的日志信息,例如:

<logger name="com.demo.UserService" level="DEBUG" additivity="false">
    <appender-ref ref="FILE"/>
    <appender-ref ref="CONSOLE"/>
</logger>

上述配置中,Logger 的 level 属性为 DEBUG,表示只输出 DEBUG 级别及更高级别的日志信息。

3. additivity 属性

additivity 属性表示是否继承父 Logger 的 Appender,即是否在父 Logger 的 Appender 中同时输出相同的日志信息。如果为 false,则该 Logger 只会将日志信息输出到自己指定的 Appender 中,并不会将日志信息传递给父 Logger。默认情况下,additivity 属性为 true。

<logger name="com.demo.UserService" level="DEBUG" additivity="false">
    <appender-ref ref="FILE"/>
    <appender-ref ref="CONSOLE"/>
</logger>

上述配置中,additivity 属性为 false,表示 UserService Logger 只会将日志信息输出到自己指定的 Appender,而不会将日志信息传递给 RootLogger(父 Logger)。

4. ConsoleAppender 配置

ConsoleAppender 可以将日志信息输出到控制台,配置示例如下:

<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <target>System.out</target>
    <encoder>
        <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n</pattern>
    </encoder>
</appender>

上述配置中,我们定义了一个名为 CONSOLE 的 Appender,其 class 属性指定为 ch.qos.logback.core.ConsoleAppender,表示输出到控制台。具体细节如下:

(1)target:指定输出目标,可以是 System.out 或 System.err,默认为 System.out。

(2)encoder:指定输出格式,通常使用 PatternLayoutEncoder 来指定输出格式,具体内容详见 Layout 配置。

5. FileAppender 配置

FileAppender 可以将日志信息输出到文件中,配置示例如下:

<appender name="FILE" class="ch.qos.logback.core.FileAppender">
    <file>log/test.log</file>
    <append>false</append>
    <encoder>
        <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} : %msg%n</pattern>
    </encoder>
</appender>

上述配置中,我们定义了一个名为 FILE 的 Appender,其 class 属性指定为 ch.qos.logback.core.FileAppender,表示输出到文件。具体细节如下:

(1)file:指定输出文件路径及名称。

(2)append:指定是否追加数据到输出文件中,若为 true 则追加,否则覆盖,默认值为 true。

(3)encoder:指定输出格式,通常使用 PatternLayoutEncoder 来指定输出格式,具体内容详见 Layout 配置。

6. RollingFileAppender 配置

RollingFileAppender 可以将日志信息输出到文件中,同时支持按时间、大小等条件进行切割,以避免单个日志文件过大的问题,配置示例如下:

<appender name="ROLLING_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>log/test.log</file>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
        <fileNamePattern>log/test.%d{yyyy-MM-dd}.log</fileNamePattern>
        <maxHistory>7</maxHistory>
    </rollingPolicy>
    <encoder>
        <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} : %msg%n</pattern>
    </encoder>
</appender>

上述配置中,我们定义了一个名为 ROLLING_FILE 的 Appender,其 class 属性指定为 ch.qos.logback.core.rolling.RollingFileAppender,表示输出到文件。具体细节如下:

(1)file:指定输出文件路径及名称。

(2)rollingPolicy:指定切割策略,此处使用的是 ch.qos.logback.core.rolling.TimeBasedRollingPolicy,表示按时间切割。fileNamePattern 即指定切割后文件名称的格式,maxHistory 指定最大的历史文件个数。

(3)encoder:指定输出格式,通常使用 PatternLayoutEncoder 来指定输出格式,具体内容详见 Layout 配置。

7. SMTPAppender 配置

SMTPAppender 可以将日志信息以邮件形式发送到指定邮箱,配置示例如下:

<appender name="EMAIL" class="ch.qos.logback.classic.net.SMTPAppender">
    <smtpHost>smtp.test.com</smtpHost>
    <smtpPort>465</smtpPort>
    <ssl>true</ssl>
    <username>[email protected]</username>
    <password>123456</password>
    <to>[email protected],[email protected]</to>
    <from>[email protected]</from>
    <subject>错误日志</subject>
    <layout class="ch.qos.logback.classic.html.HTMLLayout"/>
    <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
        <level>ERROR</level>
    </filter>
</appender>

上述配置中,我们定义了一个名为 EMAIL 的 Appender,其 class 属性指定为 ch.qos.logback.classic.net.SMTPAppender,表示通过邮件发送日志信息。具体细节如下:

(1)smtpHost、smtpPort:指定邮件服务器的地址和端口。

(2)ssl:是否使用 SSL 连接邮件服务器。

(3)username、password:邮件服务器的登录账号和密码。

(4)to、from:指定接收邮件的邮箱地址和发送者邮箱地址。

(5)subject:邮件主题。

(6)layout:指定输出格式,可以使用各种 Layout 类型,此处使用的是 HTMLLayout。

(7)filter:指定过滤条件,此处使用的是 ThresholdFilter,表示只发送 ERROR 级别及以上的日志信息。

8. PatternLayout 配置

PatternLayout 可以按照指定的格式输出日志信息,下面是一些常用的格式占位符及其含义:

(1)%d{HH:mm:ss.SSS}:输出日志的时间,精确到毫秒。

(2)[%thread]:输出日志的线程名。

(3)%-5level:输出日志级别,左对齐并占位 5 个字符,若不足则用空格补齐。

(4)%logger{36}:输出日志所在类的名称,最长 36 个字符。

(5): %msg%n:输出日志消息及换行符。

配置示例如下:

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

上述配置中,我们使用 PatternLayoutEncoder 来指定输出格式,其 pattern 属性指定了输出格式。具体细节可见上面的格式占位符及其含义。

9. HTMLLayout 配置

HTMLLayout 可以将日志信息以 HTML 格式进行输出,用于可视化展示和查看。配置示例如下:

<layout class="ch.qos.logback.classic.html.HTMLLayout"/>

上述配置中,我们使用 HTMLLayout 类型来指定输出格式,其 class 属性指定为 ch.qos.logback.classic.html.HTMLLayout。如果需要添加样式和自定义输出内容,可以通过 CSS 和 Header、Footer 配置进行实现。

10. LevelFilter 配置

LevelFilter 用于根据日志级别过滤日志信息,可用于只记录某些级别的日志信息或忽略某些级别的日志信息,配置示例如下:

<filter class="ch.qos.logback.classic.filter.LevelFilter">
    <level>WARN</level>
    <onMatch>DENY</onMatch>
    <onMismatch>ACCEPT</onMismatch>
</filter>

上述配置中,我们使用 LevelFilter 类型来指定过滤器,其 class 属性指定为 ch.qos.logback.classic.filter.LevelFilter。具体细节如下:

(1)level:指定要过滤的日志级别。

(2)onMatch:指定当过滤条件匹配时要执行的操作,此处使用 DENY 表示拒绝输出。

(3)onMismatch:指定当过滤条件未匹配时要执行的操作,此处使用 ACCEPT 表示接受输出。

  1. DuplicateMessageFilter 配置

DuplicateMessageFilter 用于过滤掉重复的日志信息,即当多个日志消息内容相同时,只记录其中一个日志信息,可用于减少日志量或避免误导信息,配置示例如下:

<filter class="ch.qos.logback.classic.filter.DuplicateMessageFilter">
    <allowedRepetitions>1</allowedRepetitions>
</filter>

上述配置中,我们使用 DuplicateMessageFilter 类型来指定过滤器,其 class 属性指定为 ch.qos.logback.classic.filter.DuplicateMessageFilter。allowedRepetitions 属性指定了允许的重复次数,此处设置为 1 表示当某个日志消息出现重复内容时,只记录一次该日志信息。

四、配置示例

1. 一个简单常用的配置

    <?xml version="1.0" encoding="UTF-8"?>
    <configuration>

    <!-- 定义日志文件保存的路径和文件名 -->
    <property name="LOG_HOME" value="/var/log/myapp"/>
    <property name="APP_NAME" value="myapp"/>

    <!-- 控制台输出 -->
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- 按日期拆分的文件输出 -->
    <appender name="rollingFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_HOME}/${APP_NAME}.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOG_HOME}/${APP_NAME}-%d{yyyy-MM-dd}.log.gz</fileNamePattern>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- 过滤一些无用的日志 -->
    <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
        <level>INFO</level>
    </filter>

    <!-- 设置日志记录器 -->
    <logger name="com.mypackage" level="debug" additivity="false">
        <appender-ref ref="rollingFile"/>
        <appender-ref ref="console"/>
    </logger>

    <!-- 根的日志记录器 -->
    <root level="error">
        <appender-ref ref="rollingFile"/>
        <appender-ref ref="console"/>
    </root>

    </configuration>

上述配置代码中,控制台输出和按日期拆分的文件输出是两个常见的 appender。这里也可以添加其他的 appender 来实现将日志发往不同渠道(邮件、数据库等),根据需要进行修改即可。在日志文件备份方面,这里使用了 TimeBasedRollingPolicy 滚动策略,可以在每天或每个小时结束时生成一个新的日志文件。当然,也可以使用 SizeAndTimeBasedRollingPolicy 等其他的日志文件备份策略。

注意,上述代码中的路径、文件名、包名等都是示例,实际应用时需要根据具体情况进行修改。

2. ELK相关配置

<?xml version="1.0" encoding="UTF-8"?>
<configuration>

    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{35} : %msg%n</pattern>
        </encoder>
    </appender>

    <appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>logs/app.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>logs/app.%d{yyyy-MM-dd}.log</fileNamePattern>
            <maxHistory>7</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{35} : %msg%n</pattern>
        </encoder>
    </appender>

    <appender name="logstash" class="ch.qos.logback.core.net.SyslogAppender">
        <syslogHost>${ELK_HOST}</syslogHost>
        <syslogPort>${ELK_PORT}</syslogPort>
        <suffixPattern>%msg%n</suffixPattern>
        <facility>USER</facility>
        <includeMDC>true</includeMDC>
        <layout class="net.logstash.logback.layout.LogstashLayout">
            <jsonFormatter class="net.logstash.logback.jackson.LogstashJacksonJsonProvider"/>
            <fieldNames>
                <message>log</message>
            </fieldNames>
            <customFields>{"app_name":"${appName}"}</customFields>
        </layout>
    </appender>

    <logger name="com.example" level="INFO">
        <appender-ref ref="logstash"/>
        <appender-ref ref="console"/>
        <appender-ref ref="file"/>
    </logger>

    <root level="INFO">
        <appender-ref ref="logstash"/>
        <appender-ref ref="console"/>
        <appender-ref ref="file"/>
    </root>

</configuration>

3. 比较详细的配置


<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false">

  <!-- 定义日志文件保存的路径和文件名 -->
  <property name="LOG_DIR" value="/var/log/myapp"/>
  <property name="APP_NAME" value="myapp"/>

  <!-- 控制台输出 -->
  <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
  </appender>

  <!-- 按日期拆分的文件输出 -->
  <appender name="rollingFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>${LOG_DIR}/${APP_NAME}.log</file>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
      <fileNamePattern>${LOG_DIR}/${APP_NAME}-%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
      <maxHistory>30</maxHistory>
    </rollingPolicy>
    <encoder>
      <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
    <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
      <level>DEBUG</level>
    </filter>
  </appender>

  <!-- 异步输出日志,避免阻塞主线程 -->
  <appender name="asyncAppender" class="ch.qos.logback.classic.AsyncAppender">
    <queueSize>1024</queueSize>
    <appender-ref ref="rollingFile"/>
    <!-- 还可以添加其他 appender -->
  </appender>

  <!-- 按大小和日期拆分的文件输出,比较灵活 -->
  <appender name="rollingFile2" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>${LOG_DIR}/${APP_NAME}.log</file>
    <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
      <fileNamePattern>${LOG_DIR}/${APP_NAME}-%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
      <maxFileSize>100MB</maxFileSize>
      <maxHistory>30</maxHistory>
      <totalSizeCap>10GB</totalSizeCap>
    </rollingPolicy>
    <encoder>
      <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
    <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
      <level>DEBUG</level>
    </filter>
  </appender>

  <!-- 根据系统环境变量动态配置日志级别 -->
  <appender name="dynamicThresholdLogging" class="ch.qos.logback.classic.sift.SiftingAppender">
    <discriminator>
      <key>env</key>
      <defaultValue>dev</defaultValue>
    </discriminator>
    <sift>
      <appender name="FILE-${env}" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_DIR}/${APP_NAME}-${env}.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
          <fileNamePattern>${LOG_DIR}/${APP_NAME}-${env}-%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
          <maxHistory>7</maxHistory>
        </rollingPolicy>
        <encoder>
          <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
          <level>${env == 'prod' ? 'INFO' : 'DEBUG'}</level>
        </filter>
      </appender>
    </sift>
  </appender>

  <!-- 设置日志记录器 -->
  <logger name="com.mypackage" level="debug" additivity="false">
    <appender-ref ref="asyncAppender"/>
  </logger>

  <!-- 根的日志记录器 -->
  <root level="error">
    <appender-ref ref="asyncAppender"/>
    <appender-ref ref="console"/>
  </root>

  <!-- MDC(Mapped Diagnostic Context) -->
  <conversionRule conversionWord="reqId" converterClass="com.acme.logback.ReqIdConverter"/>

  <appender name="mdcDemo" class="ch.qos.logback.classic.net.SyslogAppender">
    <syslogHost>localhost</syslogHost>
    <facility>LOCAL0</facility>
    <suffixPattern>%mdc{reqId} - %message</suffixPattern>
  </appender>

  <logger name="com.acme.service" level="TRACE">
    <appender-ref ref="mdcDemo"/>
  </logger>

  <!-- MDC 的另一种使用方式:通过配置文件 -->
  <appender name="mdcDemo2" class="ch.qos.logback.classic.net.SyslogAppender">
    <syslogHost>localhost</syslogHost>
    <facility>LOCAL0</facility>
    <suffixPattern>%X{reqId} - %message</suffixPattern>
  </appender>

  <logger name="com.acme.service2" level="TRACE">
    <appender-ref ref="mdcDemo2"/>
  </logger>

  <!-- Logback Groovy 编写复杂的过滤条件 -->
  <appender name="groovyDemo" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>${LOG_DIR}/${APP_NAME}.log</file>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
      <fileNamePattern>${LOG_DIR}/${APP_NAME}-%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
      <maxHistory>30</maxHistory>
    </rollingPolicy>
    <encoder>
      <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
    <filter class="ch.qos.logback.core.filter.Filter">
      <groovy>
        def mdc = event.getMDC()
        if (mdc == null) {
          return FilterReply.NEUTRAL
        }
        def user = mdc.get("user")
        if (user == null || !user.equals("admin")) {
          return FilterReply.DENY
        }
        return FilterReply.ACCEPT
      </groovy>
    </filter>
  </appender>

  <logger name="com.mypackage2" level="debug">
    <appender-ref ref="groovyDemo"/>
  </logger>

  <!-- JMX 监控,可以在 JConsole 或者 JVisualVM 中查看 -->
  <jmxConfigurator/>
  <statusListener class="ch.qos.logback.core.status.OnConsoleStatusListener"/>

  <!-- 日志分离存储 -->
  <appender name="dbAppender" class="com.example.logback.appender.DBAppender">
    <connectionSource class="com.example.logback.datasource.DataSourceConnectionSource">
      <url>jdbc:mysql://localhost:3306/dbname</url>
      <user>username</user>
      <password>password</password>
      <driverClass>com.mysql.jdbc.Driver</driverClass>
      <minConnectionsPerPartition>5</minConnectionsPerPartition>
      <maxConnectionsPerPartition>20</maxConnectionsPerPartition>
      <partitionCount>2</partitionCount>
    </connectionSource>
    <bufferSize>1000</bufferSize>
    <tableName>logs</tableName>
    <columns>
      <column name="timestamp" pattern="%d{yyyy-MM-dd HH:mm:ss.SSS}" />
      <column name="level" pattern="%level" />
      <column name="logger" pattern="%logger" />
      <column name="message" pattern="%message" />
    </columns>
  </appender>

  <logger name="com.example.myapp" level="INFO">
    <appender-ref ref="asyncAppender"/>
    <appender-ref ref="dbAppender"/>
  </logger>

</configuration>

此份 Logback 配置有以下功能:

  • 按大小和日期拆分的文件输出:可以通过配置文件来实现,比较灵活;
  • 根据系统环境变量动态配置日志级别:可以根据环境变量的值来决定输出的日志级别;
  • MDC(Mapped Diagnostic Context):通过为每个线程关联一个 Map 来实现在日志输出中添加自定义的上下文信息,例如请求 ID、用户信息等;
  • 可以通过配置文件或者编写 Java 类来使用 MDC;
  • 可以通过 Groovy 或者 JavaScript 等脚本语言来编写复杂的过滤条件,可以根据日志事件中的任意字段来决定是否输出;
  • JMX 监控:可以在 JConsole 或者 JVisualVM 中查看 Logback 使用情况和日志输出;
  • 日志分离存储:将日志输出到数据库中。

需要注意的是,上述配置代码的变量值和路径都是示例,实际使用时需要根据实际情况进行修改。

猜你喜欢

转载自blog.csdn.net/u012581020/article/details/130988541