log4j2 日志配置实战

作者: 疯狂小兵 | 2017-04-26 | 阅读
「编辑」 「本文源码」

1.目的

替换log4j,log4j2是log4j的2.x版本但是在log4j上做了比较大的改变,log4j2的性能比log4j好。该文不会具体讲解常用的基础配置,主要介绍日志发邮件日志写mysql数据库日志写Mongo功能。

2.实战

预设环境

  • 默认项目为maven管理
  • 使用jdk1.8及以上

  • idea开发环境
  • mongo

2.1 配置

添加依赖

<properties>
    <druid.version>1.0.18</druid.version>
    <fastjson.version>1.2.9</fastjson.version>
    <log4j2.version>2.8</log4j2.version>
    <mail.version>1.4.7</mail.version>
    <mongo.version>3.4.2</mongo.version>
    <mysql.driver.version>5.1.36</mysql.driver.version>
    <slf4j.version>1.7.24</slf4j.version>
    <spring-mongo.version>1.10.3.RELEASE</spring-mongo.version>
</properties>

<dependencies>
    <!-- mysql驱动 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>${mysql.driver.version}</version>
    </dependency>

    <!--邮箱-->
    <dependency>
        <groupId>javax.mail</groupId>
        <artifactId>mail</artifactId>
        <version>${mail.version}</version>
    </dependency>
    <!-- mongoDB 驱动 -->
    <dependency>
        <groupId>org.mongodb</groupId>
        <artifactId>mongo-java-driver</artifactId>
        <version>${mongo.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.data</groupId>
        <artifactId>spring-data-mongodb</artifactId>
        <version>${spring-mongo.version}</version>
    </dependency>

    <!--json解析器-->
     <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>${fastjson.version}</version>
    </dependency>

    <!--slf4j 接口-->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>${slf4j.version}</version>
    </dependency>
    <!--java slf4j的桥接器-->
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-slf4j-impl</artifactId>
        <version>${log4j2.version}</version>
    </dependency>

    <!--log4j2 实现及接口-->
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-core</artifactId>
        <version>${log4j2.version}</version>
    </dependency>
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-api</artifactId>
        <version>${log4j2.version}</version>
    </dependency>
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-nosql</artifactId>
        <version>${log4j2.version}</version>
    </dependency>

    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-web</artifactId>
        <version>${log4j2.version}</version>
    </dependency>
</dependencies>

对以上依赖包的说明:

  • slf4jlog4j*包是日志门面接口和实际日志发送者

    其中log4j-web是为了在代码中使用Spring管理log4j2,通过Spring进行初始化,并应用Druid的数据源。

    log4j-nosql是为了使得log4j2支持非关系型数据库(如Mongo,CouchDb)。

    log4j-slf4j-impl是用于log4j2桥接slf4j,便于使用slf4j声明,log4j2输出日志。

  • fastjson:是将java类格式化为json格式,用于发送日志时将日志转为json。

  • mongo-java-driver:是mongo的java驱动,便于java对mongo的调用

  • spring-data-mongodb: 是Spring对Mongo的集成,方便以Spring的方式操作Mongo。

  • mail:发送邮件时需要。

  • mysql-connector-java:java操作mysql数据库的驱动。

这些只是当前讲解日志相关功能使用的包,而Spring的核心包以及其他如Mybatis等不在此文关心范围内。

2.2 log4j2.xml配置

log4j2.xml是作为log4j2使用时指定的文件名称之一,其他的还有(log4j.configurationFile,log4j2-test.properties,log4j2-test.yaml,log4j2-test.yml,log4j2-test.json,log4j2-test.jsn,log4j2-test.xml,log4j2.properties,log4j2.yaml,log4j2.yml,log4j2.json,log4j2.jsn,log4j2.xml,默认配置),log42会以名称出现的顺序查找上面的文件依次是否存在,而log4j2.xml是优先级最低的,log4j.configurationFile是指用户自定义指定的文件名,优先级最高。

贴出所有的log4j2.xml代码,但不会全部介绍

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

<!--monitorInterval:指定间隔监控配置文件是否更改,更改后自动重新配置,不需要重启服务。-->
<configuration name="esn-palmyy-plugin" status="info" monitorInterval="5" strict="false">

  <properties>
    <property name="receipients">to-mail@mail.com</property>
    <property name="from">from-mail@mail.com</property>
    <property name="smtpHost">mail.host.com</property>
    <property name="smtpPort">25</property>
    <property name="smtpProtocol">smtp</property>
    <property name="smtpUser">from-mail@mail.com</property>
    <property name="smtpPassword">password</property>
    <property name="smtpSubject">日志主题信息</property>
  </properties>

  <appenders>

    <!--打印日志到控制台-->
    <Console name="stdout" target="SYSTEM_OUT">
      <!--<ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY"/>-->
      <PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %class{36} %L %M - %msg%xEx%n"/>
    </Console>

    <!--将info级别的信息追加到info.log日志文件-->
    <RollingFile name="infoLog"
                 fileName="${catalina.home}/logs/esn-palmyy-plugin/info.log"
                 append="true"
                 filePattern="${catalina.home}/logs/esn-palmyy-plugin/info.log.%d{yyyy-MM-dd}">
      <PatternLayout
              pattern="%d{yyyy.MM.dd 'at' HH:mm:ss.SSS z} %-5level %class{36} %L %M - %msg%xEx%n"/>
      <ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/>
      <Policies>
        <TimeBasedTriggeringPolicy modulate="true" interval="24"/>
      </Policies>
    </RollingFile>

    <!--将error级别的信息追加到error.log日志文件-->
    <RollingFile name="errorLog"
                 fileName="${catalina.home}/logs/esn-palmyy-plugin/error.log"
                 append="true"
                 filePattern="${catalina.home}/logs/esn-palmyy-plugin/error.log.%d{yyyy-MM-dd}">
      <PatternLayout
              pattern="%d{yyyy.MM.dd 'at' HH:mm:ss.SSS z} %-5level %class{36} %L %M - %msg%xEx%n"/>
      <ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="DENY"/>
      <Policies>
        <TimeBasedTriggeringPolicy modulate="true" interval="24"/>
      </Policies>
    </RollingFile>

    <!--将特定的信息追加到error-esn.log日志文件-->
    <RollingFile name="errorEsnLog"
                 fileName="${catalina.home}/logs/esn-palmyy-plugin/error-esn.log"
                 append="true"
                 filePattern="${catalina.home}/logs/esn-palmyy-plugin/error-esn.log.%d{yyyy-MM-dd}">
      <PatternLayout pattern="%d %-5p %t - %c %l - %m%n"/>
      <ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="DENY"/>
      <Policies>
        <TimeBasedTriggeringPolicy modulate="true" interval="24"/>
      </Policies>
    </RollingFile>

    <!-- 配置日志发送邮件的标签,其中“${}”引用的配置都在最上面定义好了-->
    <SMTP name="Mailer"
          subject="${smtpSubject}" to="${receipients}" from="${from}"
          smtpHost="${smtpHost}" smtpPort="${smtpPort}"
          smtpProtocol="${smtpProtocol}" smtpUsername="${smtpUser}"
          smtpPassword="${smtpPassword}" smtpDebug="false" bufferSize="1024">
      <customHtmlLayout/>
    </SMTP>

    <!-- 异步发送邮件日志 -->
    <Async name="AsyncMailer">
      <appender-ref ref="Mailer"/>
    </Async>

    <!--配置日志写入mysql数据库 -->
    <JDBC name="databaseAppender" tableName="logger_info">
      <ConnectionFactory class="com.yonyou.esn.palmyy.common.LoggerConnectionFactory"
                         method="getDatabaseConnection"/>
      <Column name="thread_name" pattern="%t"/>
      <Column name="class_name" pattern="%C"/>
      <Column name="method_name" pattern="%method"/>
      <Column name="beginTime" pattern="%d{yyyy-MM-dd HH:mm:ss.SSS}"/>
      <Column name="logger_level" pattern="%level"/>
      <Column name="logger_name" pattern="%c"/>
      <Column name="logger_message" pattern="%m"/>
      <Column name="logger_throwable" pattern="%throwable{3}"/>
      <Column name="createTime" pattern="%d{yyyy-MM-dd HH:mm:ss.SSS}"/>
    </JDBC>

    <!--异步写入数据库-->
    <Async name="AsyncDatabaseAppender">
      <appender-ref ref="databaseAppender"/>
    </Async>

    <!--异步写入nosql中,如mongo-->
    <Async name="asyncNoSqlDbLogger">
      <appender-ref ref="noSqlDbAppender"/>
    </Async>

    <!--日志输出到Nosql数据库-->
    <NoSql name="noSqlDbAppender">
      <MongoDb databaseName="esn-palmyy" collectionName="log_info" server="172.20.9.41"
               port="27017"/>
    </NoSql>
  </appenders>


  <!--然后定义logger,只有定义了logger并引入的appender,appender才会生效 
    <logger>标签可以在<Root>标签中引用(全局使用)也可以在类调用时指定引用的名称,定向调用
    如:private static final Logger LOGGER_MAIL= LoggerFactory.getLogger("AsyncMailerLogger");
    LOGGER_MAIL的日志都只会发送邮件,不写到文件中
  
  -->
  <loggers>
    <logger name="AsyncMailerLogger" level="error" additivity="false">
      <appender-ref ref="Mailer"/>
    </logger>

    <logger name="noSqlDbLogger" level="info" additivity="false">
      <appender-ref ref="noSqlDbAppender"/>
    </logger>

    <logger name="AsyncDBLogger" level="error" additivity="false">
      <appender-ref ref="databaseAppender"/>
    </logger>

    <Logger name="errorEsnLogger" level="error" additivity="true">
      <AppenderRef ref="errorEsnLog"/>
    </Logger>

    <Root level="debug">
      <AppenderRef ref="stdout"/>
      <AppenderRef ref="errorLog"/>
      <AppenderRef ref="infoLog"/>
    </Root>
  </loggers>

</configuration>

具体配置的各个属性名称不做具体解释了,自解释了。

2.3 log4j2配置写mysql数据库功能

节选部分配置

<!--配置日志写入mysql数据库 -->
<JDBC name="databaseAppender" tableName="logger_info">
    <ConnectionFactory class="com.xxx.aaa.palmyy.common.LoggerConnectionFactory"
                        method="getDatabaseConnection"/>
    <Column name="thread_name" pattern="%t"/>
    <Column name="class_name" pattern="%C"/>
    <Column name="method_name" pattern="%method"/>
    <Column name="beginTime" pattern="%d{yyyy-MM-dd HH:mm:ss.SSS}"/>
    <Column name="logger_level" pattern="%level"/>
    <Column name="logger_name" pattern="%c"/>
    <Column name="logger_message" pattern="%m"/>
    <Column name="logger_throwable" pattern="%throwable{3}"/>
    <Column name="createTime" pattern="%d{yyyy-MM-dd HH:mm:ss.SSS}"/>
</JDBC>
  • tableName是数据库中的表名,
  • <Column>的name属性是指定数据库表中的字段名,pattern是匹配日志的内容格式,会将对应的内容写入到指定的字段下。

  • <ConnectionFactory> 需要自定义数据库连接工厂类,并提供获取DataSource或者Connection的静态方法。

2.4 编写LoggerConnectionFactory类实现逻辑

LoggerConnectionFactory类的代码如下:

package com.yonyou.esn.palmyy.common;

import com.alibaba.druid.pool.DruidDataSourceFactory;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;
import javax.annotation.PostConstruct;
import javax.sql.DataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * 日志配置所使用的数据库连接工厂类
 * <p>Created by followtry on 2017/4/20.
 */
@Component
public class LoggerConnectionFactory {

  private static final Logger LOGGER = LoggerFactory.getLogger(LoggerConnectionFactory.class);

  private static final LoggerConnectionFactory loggerConnectionFactory = new LoggerConnectionFactory();

  @Autowired
  private DataSource dataSource;

  //通过内部单例接口实例化当前工厂类并提供Connection连接
  private interface Singleton {
    LoggerConnectionFactory INSTANCE = new LoggerConnectionFactory();
  }

  @PostConstruct
  void init(){
    Properties prop = new Properties() {
      {
        put("username","abc");
        put("password","abc");
        put("url","jdbc:mysql://172.20.19.200:3306/xx_test?useUnicode=true&characterEncoding=UTF-8");
        put("driverClassName","com.mysql.jdbc.Driver");
      }
    };

    try {
      dataSource = DruidDataSourceFactory.createDataSource(prop);
    } catch (Exception e) {
      LOGGER.error("获取数据源出错",e);
    }
  }

  private LoggerConnectionFactory() {}

  public static Connection getDatabaseConnection() throws SQLException {
    return Singleton.INSTANCE.dataSource.getConnection();
  }
}

这样的方式是可以获取到Mysql数据库的Connection并且也可以将日志写入到数据库中,但是很明显这样有两个很大的缺点:

  • 自己控制对数据库的链接,没有应用业界比较好的数据源库也没有使用线程池。
  • 没有应用到Spring的IOC和DI。

2.5 Log4j2设置解惑

有人会问:

对于连接属性的配置直接调用Spring管理的数据源不行吗?答案是现在这样的配置不行,因为log4j2等日志是在Spring初始化前加载并初始化的,因此log4j2在当前配置下是拿不到Spring的bean信息的。

那这个问题就无解了吗?当然不是,要不然干嘛要引入log4j-web包呢。引入该包就是为了在web应用中使用Log4j2的,具体可以参考http://logging.apache.org/log4j/2.x/manual/webapp.html

要说明的是:Log4j2仅支持Servlet3.0及以上版本,Tomcat7.0及以上

Log4j2会在WEB容器启动和销毁时自动启动和关闭。是通过Log4jServletContainerInitializer继承自ServletContainerInitializer达到自动启动的目的。在Tomcat7.0.43之前,基于性能原因,web容器会忽略名为log4j*.jar包,阻止其获取随web容器启动的特性。但在之后的版本中已经修复。

如果禁止Log4j2的自动初始化,那么需要在web.xml中加入配置:

<context-param>
    <param-name>isLog4jAutoInitializationDisabled</param-name>
    <param-value>true</param-value>
</context-param>

但是在Servlet2.5需要在其他应用代码执行前初始化Log4j。

如果使用的是Servlet2.5,那么需要在web.xml文件中加入配置:

<listener>
    <listener-class>org.apache.logging.log4j.web.Log4jServletContextListener</listener-class>
  </listener>
  <filter>
    <filter-name>log4jServletFilter</filter-name>
    <filter-class>org.apache.logging.log4j.web.Log4jServletFilter</filter-class>
  </filter>
  <filter-mapping>
    <filter-name>log4jServletFilter</filter-name>
    <url-pattern>/*</url-pattern>
    <dispatcher>REQUEST</dispatcher>
    <dispatcher>FORWARD</dispatcher>
    <dispatcher>INCLUDE</dispatcher>
    <dispatcher>ERROR</dispatcher>
  </filter-mapping>

详情请看http://logging.apache.org/log4j/2.x/manual/webapp.html,接下来就说明下怎样通过在Log4j2中使用Spring管理的bean。

2.6 重新配置Log4j2支持JDBC

这次配置是基于Spring的,使用了Spring容器的功能,对配置文件中Log4j2的配置进行增加。

代码如下:

package com.yonyou.esn.palmyy.common;

import java.sql.Connection;
import java.sql.SQLException;
import javax.annotation.PostConstruct;
import javax.sql.DataSource;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.appender.AsyncAppender;
import org.apache.logging.log4j.core.appender.db.ColumnMapping;
import org.apache.logging.log4j.core.appender.db.jdbc.ColumnConfig;
import org.apache.logging.log4j.core.appender.db.jdbc.ConnectionSource;
import org.apache.logging.log4j.core.appender.db.jdbc.JdbcAppender;
import org.apache.logging.log4j.core.config.AppenderRef;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.LoggerConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * 配置log4j2的JDBC写数据库的配置类
 *
 * <p>Created by followtry on 2017/4/20.
 */
@Component
public class LoggerBean {

  @Autowired
  private DataSource dataSource;

  /**
   * 初始化日志写数据库配置,在构造方法后执行
   */
  @PostConstruct
  void init(){

    LoggerContext context = (LoggerContext)LogManager.getContext(false);

    //<configuration></configuration>
    Configuration cfg = context.getConfiguration();

    /**
     * 该配置对应columnConfig
     *
     * <JDBC name="databaseAppender" tableName="logger_info">
     <ConnectionFactory class="com.yonyou.esn.palmyy.common.LoggerConnectionFactory" method="getDatabaseConnection" />
     <Column name="thread_name" pattern="%t" />
     <Column name="class_name" pattern="%C"/>
     <Column name="method_name" pattern="%method" />
     <Column name="beginTime" pattern="%d{yyyy-MM-dd HH:mm:ss.SSS}" />
     <Column name="logger_level" pattern="%level" />
     <Column name="logger_name" pattern="%c" />
     <Column name="logger_message" pattern="%m" />
     <Column name="logger_throwable" pattern="%throwable{3}" />
     <Column name="createTime" pattern="%d{yyyy-MM-dd HH:mm:ss.SSS}" />
     </JDBC>
     */

    ColumnConfig[] columnConfig = {
      ColumnConfig.newBuilder().setConfiguration(cfg).setName("thread_name")
              .setPattern("%t").build(),
      ColumnConfig.newBuilder().setConfiguration(cfg).setName("class_name")
              .setPattern("%C").build(),
      ColumnConfig.newBuilder().setConfiguration(cfg).setName("method_name")
              .setPattern("%method").build(),
      ColumnConfig.newBuilder().setConfiguration(cfg).setName("beginTime")
              .setEventTimestamp(true).build(),
      ColumnConfig.newBuilder().setConfiguration(cfg).setName("logger_level")
              .setPattern("%level").build(),
      ColumnConfig.newBuilder().setConfiguration(cfg).setName("logger_name")
              .setPattern("%c").build(),
      ColumnConfig.newBuilder().setConfiguration(cfg).setName("logger_message")
              .setPattern("%m").build(),
      ColumnConfig.newBuilder().setConfiguration(cfg).setName("logger_throwable")
              .setPattern("%throwable{3}").build(),
      ColumnConfig.newBuilder().setConfiguration(cfg).setName("createTime")
              .setEventTimestamp(true).build(),
    };

    JdbcAppender jdbcAppender = JdbcAppender.newBuilder()
            .setConfiguration(cfg)
            .withName("databaseAppender")
            .withIgnoreExceptions(false)
            .setConnectionSource(new Connect(dataSource))
            .setTableName("logger_info")
            .setColumnConfigs(columnConfig)
            //ColumnMapping设置为空,是因为如果不设置或设置为null会报空指针异常
            .setColumnMappings(new ColumnMapping[]{}).build();
    jdbcAppender.start();

    //将<JDBC></JDBC>添加到<appenders></appenders>
    cfg.addAppender(jdbcAppender);



    /**
     * <Async name="AsyncDatabaseAppender">
            <appender-ref ref="databaseAppender"/>
        </Async>
     */
    AsyncAppender asyncDbAppender = AsyncAppender.newBuilder().setConfiguration(cfg).setName
            ("asyncDatabaseAppender").setAppenderRefs(new AppenderRef[] { AppenderRef
            .createAppenderRef(jdbcAppender.getName(),Level.ERROR,null) }).build();
    asyncDbAppender.start();
    //将<Async></Async>添加到<appenders></appenders>
    cfg.addAppender(asyncDbAppender);

    //将<logger></logger>添加到<loggers></loggers>
    LoggerConfig loggerConfig = new LoggerConfig();
    loggerConfig.setAdditive(false);
    //设置为jdbcAppender为同步写库,配置为asyncDbAppender为异步写库,但是异步写库会导致类名和方法名找不到
    loggerConfig.addAppender(jdbcAppender,Level.ERROR,null);
    cfg.addLogger("asyncDBLogger",loggerConfig);
    context.updateLoggers();
  }

  //内部类
  class Connect implements ConnectionSource {
    private DataSource dsource;
    public Connect(DataSource dsource) {
      this.dsource = dsource;
    }
    @Override
    public Connection getConnection() throws SQLException {
      return this.dsource.getConnection();
    }

  }

  private LoggerBean() {}

  public DataSource getDataSource() {
    return dataSource;
  }

  public void setDataSource(DataSource dataSource) {
    this.dataSource = dataSource;
  }
}

以上代码中的注释已经比较详细了,概括来说就是在当前LoggerBean被IOC实例化之后获取到已经从配置文件中读取的Log4j2的配置,并在init()方法中用代码将JDBC部分的逻辑补充上,更新下Log4j2的上下文即可。一个类内就把Log4j2的JDBC功能实现了,不需要在任何地方做log4j2对JBDC支持的配置。

2.7 Log4j2配置发邮件功能

log4j2.xml中</SMTP>内的配置就是控制日志发送邮件的功能了,默认使用的Lauout是<HtmlLayout>,对应类org.apache.logging.log4j.core.layout.HtmlLayout,比较蛋疼的是该类是final型的,不能被继承也就是说不能通过继承该类实现自定义Html布局。唉没办法,使用自定义的Layout吧!

自定义的layout需要继承org.apache.logging.log4j.core.layout.AbstractStringLayout,并且要在类上加上注解:

@Plugin(
  name = "HtmlLayout",
  category = "Core",
  elementType = "layout",
  printObject = true
)

其中name处value是配置文件log4j2.xml中使用的标签,如<HtmlLayout />,写者没有对其原理进行分析,直接强行复制其代码新建了个类,并修改了关键的位置@Plugin的name作为自己的布局模板。

自定义的布局模板关键代码如下:

@Plugin(
        //此处name值即在Log4j2.xml中配置的标签名,如<customHtmlLayout/>
        name = "customHtmlLayout",
        category = "Core",
        elementType = "layout",
        printObject = true)
/**
 * Created by followtry on 2017/4/26.
 */
public class CustomHtmlLayout extends AbstractStringLayout {
    public byte[] getHeader() {
        //实现布局Header部分的逻辑
    }
    public String toSerializable(LogEvent event){
        //内部实现布局body的逻辑
    }

    public byte[] getFooter() {
        //实现布局footer的逻辑
    }
}

通过将上面解释的逻辑调整为自己想要的就实现了自定义布局。

2.8 Log4j2支持Mongo写日志

此处不涉及Mongo的安装和使用

对于NoSql的配置就比较简单了,以Mongo为例:

<NoSql name="noSqlDbAppender">
    <MongoDb databaseName="esn-palmyy" collectionName="log_info" server="172.20.9.41"
            port="27017"/>
</NoSql>

相信Mongo配置的属性不用解释也能看明白了,就这点配置,只要能连上Mongo就可以将日志写入到Mongo中,当然前提是要在<Logger>或者<Root>标签中引用noSqlDbAppender

Mongo存储结构如下:

mongo的日志结构

3 注意事项

log4j2 中配置<Async>标签异步存储日志会使得写Mysql库和Mongo库时,调用Logger的ClassName和MethodName都为空,写者当前在项目中暂时没有使用异步写日志操作。


版权声明:本文由 在 2017年04月26日发表。本文采用CC BY-NC-SA 4.0许可协议,非商业转载请注明出处,不得用于商业目的。
文章题目及链接:《log4j2 日志配置实战》




  相关文章:

「游客及非Github用户留言」:

「Github登录用户留言」:

TOP