--- layout: post title: log4j2 日志配置实战 category: 技术 tags: Logger keywords: description: 主要介绍Log4J2的日志发邮件、日志写mysql数据库、日志写Mongo等功能。为日志在系统中的高级应用打基础。希望通过通读该文,可以很方便简单的配置日志发邮件、日志写mysql数据库、日志写Mongo的功能。 --- {:toc} ## 1.目的 替换log4j,log4j2是log4j的2.x版本但是在log4j上做了比较大的改变,log4j2的性能比log4j好。该文不会具体讲解常用的基础配置,主要介绍`日志发邮件`、`日志写mysql数据库`、`日志写Mongo`功能。 ## 2.实战 **预设环境** - 默认项目为maven管理 - 使用jdk1.8及以上 - idea开发环境 - mongo ### 2.1 配置 添加依赖 ```xml 1.0.18 1.2.9 2.8 1.4.7 3.4.2 5.1.36 1.7.24 1.10.3.RELEASE mysql mysql-connector-java ${mysql.driver.version} javax.mail mail ${mail.version} org.mongodb mongo-java-driver ${mongo.version} org.springframework.data spring-data-mongodb ${spring-mongo.version} com.alibaba fastjson ${fastjson.version} org.slf4j slf4j-api ${slf4j.version} org.apache.logging.log4j log4j-slf4j-impl ${log4j2.version} org.apache.logging.log4j log4j-core ${log4j2.version} org.apache.logging.log4j log4j-api ${log4j2.version} org.apache.logging.log4j log4j-nosql ${log4j2.version} org.apache.logging.log4j log4j-web ${log4j2.version} ``` 对以上依赖包的说明: - `slf4j`和`log4j*`包是日志门面接口和实际日志发送者 其中`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 to-mail@mail.com from-mail@mail.com mail.host.com 25 smtp from-mail@mail.com password 日志主题信息 ``` 具体配置的各个属性名称不做具体解释了,自解释了。 ### 2.3 log4j2配置写mysql数据库功能 节选部分配置 ```xml ``` - tableName是数据库中的表名, - ``的name属性是指定数据库表中的字段名,pattern是匹配日志的内容格式,会将对应的内容写入到指定的字段下。 - `` 需要自定义数据库连接工厂类,并提供获取DataSource或者Connection的静态方法。 ### 2.4 编写LoggerConnectionFactory类实现逻辑 `LoggerConnectionFactory`类的代码如下: ```java 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; /** * 日志配置所使用的数据库连接工厂类 *

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的,具体可以参考 **要说明的是:Log4j2仅支持Servlet3.0及以上版本,Tomcat7.0及以上** Log4j2会在WEB容器启动和销毁时自动启动和关闭。是通过`Log4jServletContainerInitializer`继承自`ServletContainerInitializer`达到自动启动的目的。在Tomcat7.0.43之前,基于性能原因,web容器会忽略名为log4j*.jar包,阻止其获取随web容器启动的特性。但在之后的版本中已经修复。 如果禁止Log4j2的自动初始化,那么需要在web.xml中加入配置: ```xml isLog4jAutoInitializationDisabled true ``` 但是在Servlet2.5需要在其他应用代码执行前初始化Log4j。 如果使用的是**Servlet2.5**,那么需要在web.xml文件中加入配置: ```xml org.apache.logging.log4j.web.Log4jServletContextListener log4jServletFilter org.apache.logging.log4j.web.Log4jServletFilter log4jServletFilter /* REQUEST FORWARD INCLUDE ERROR ``` 详情请看,接下来就说明下怎样通过在Log4j2中使用Spring管理的bean。 ### 2.6 重新配置Log4j2支持JDBC 这次配置是基于Spring的,使用了Spring容器的功能,对配置文件中Log4j2的配置进行增加。 代码如下: ```java 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写数据库的配置类 * *

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 cfg = context.getConfiguration(); /** * 该配置对应columnConfig * * */ 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(); //将添加到 cfg.addAppender(jdbcAppender); /** * */ AsyncAppender asyncDbAppender = AsyncAppender.newBuilder().setConfiguration(cfg).setName ("asyncDatabaseAppender").setAppenderRefs(new AppenderRef[] { AppenderRef .createAppenderRef(jdbcAppender.getName(),Level.ERROR,null) }).build(); asyncDbAppender.start(); //将添加到 cfg.addAppender(asyncDbAppender); //将添加到 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中``内的配置就是控制日志发送邮件的功能了,默认使用的Lauout是``,对应类`org.apache.logging.log4j.core.layout.HtmlLayout`,比较蛋疼的是该类是final型的,不能被继承也就是说不能通过继承该类实现自定义Html布局。唉没办法,使用自定义的Layout吧! 自定义的layout需要继承`org.apache.logging.log4j.core.layout.AbstractStringLayout`,并且要在类上加上注解: ```java @Plugin( name = "HtmlLayout", category = "Core", elementType = "layout", printObject = true ) ``` 其中name处value是配置文件`log4j2.xml`中使用的标签,如``,写者没有对其原理进行分析,直接强行复制其代码新建了个类,并修改了关键的位置`@Plugin`的name作为自己的布局模板。 自定义的布局模板关键代码如下: ```java @Plugin( //此处name值即在Log4j2.xml中配置的标签名,如 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为例: ```xml ``` 相信Mongo配置的属性不用解释也能看明白了,就这点配置,只要能连上Mongo就可以将日志写入到Mongo中,当然前提是要在``或者``标签中引用`noSqlDbAppender`。 Mongo存储结构如下: ![mongo的日志结构](//raw.githubusercontent.com/George5814/blog-pic/master/image/log4j2/log4j2-mongo-1.png) ## 3 注意事项 log4j2 中配置``标签异步存储日志会使得写Mysql库和Mongo库时,调用Logger的ClassName和MethodName都为空,写者当前在项目中暂时没有使用异步写日志操作。