Springboot Log4j2 记录业务日志到数据库

​ 之前写过博客介绍过如何在传统的SpringMVC or Struts2、Spring、Hibernate or Mybatis中使用log4j1.x来输出业务日志到数据库中完成低耦合的日志输出;最近把基础框架升级到Springboot2.x,顺带将该功能做了迁移。考虑到Log4j2在异步处理上性能高于log4j1.x,所以本次最终的选择是Springboot+log4j2。

为什么选择Log4j2

Springboot框架默认的日志实现是slf4j + logback。经过一番试用后发现logback在日志输出到数据库这个能力上相比较log4j2要稍微复杂点而且欠灵活;以前的版本是基于log4j1.x的经过一番评估和考虑最终决定使用slf4j+log4j2这套方案。

配置实战

Maven 依赖及排除

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

        <dependency>
            <groupId>com.lmax</groupId>
            <artifactId>disruptor</artifactId>
            <version>3.4.2</version>
        </dependency>

​ 这里有个坑我有义务给大家指出来;Springboot对各类日志框架都做了适配,可以根据用户引入的jar做自动选择;选择的时候也有优先级;具体原理我给大家推荐一篇文章:Springboot的Log日志系统详解 同类型的文章百度上非常多大家可以多找两篇看看。根据这一原理:我们首先要排除:spring-boot-starter-logging,再排除:logback-classic ,这时Springboot才去适配我们想要的log4j2。

​ 填坑:对于新手来说按照网上绝大多少的文章给的信息是无法完全排除这两个框架的;网上的是一个demo;我们实际项目中会集成很多的第三方框架;所以在排除的时候要找到你项目POM的依赖图或者依赖关系(不同的IDE查看方式不一样);去看看有哪些框架依赖导致了前面两个框架被间接依赖都需要进行排除。举例如下:

				<!-- jdbc -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
            <exclusions>
                <exclusion>
                    <artifactId>logback-classic</artifactId>
                    <groupId>ch.qos.logback</groupId>
                </exclusion>
                <exclusion>
                    <artifactId>spring-boot-starter-logging</artifactId>
                    <groupId>org.springframework.boot</groupId>
                </exclusion>
            </exclusions>
        </dependency>

 				<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

​ 上面的代码只是针对我的项目;你自己的项目引用可能更多;需要自己确认一下是不是完完整整的排除了两个优先级较高的日志框架。

Log4j2 配置文件

​ 配置文件是核心,我直接贴出全部配置文件,对其中的一个小坑进行预警:

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="OFF">
		<!-- status 是log4j2框架自身的日志级别控制;这里我就直接关闭了。 -->
    <Appenders>
				<!-- 控制台输出 -->
        <Console name="STD_OUT" target="SYSTEM_OUT">
            <PatternLayout pattern=".%d{yyyy-MM-dd HH??ss.SSS} [%thread] [%file:%line] %-5level %logger{36} - %msg  %X{username} %X{operateResult} %n "/>
        </Console>

      	<!-- 配置JDBC日志输出,这也是log4j2比logback 方便的地方(个人认为) 
				 bufferSize 是缓冲池;直白点说就是屯多少条日志一把入库;对应jdbc的batch(批量)操作。
				-->
        <JDBC name="DatabaseAppender" tableName="sys_log" bufferSize="1">
            <Filters>
              	<!-- 启用过滤器对日志级别进行过滤;只处理INFO级别的日志。
										因为我在做业务日志记录的时候;是占用了INFO这个日志级别;所以我也只想INFO这个级别的日志入库。
                    保持日志的规范和干净,避免不相关的一些调试信息也被入库。
 								-->
                <ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY"/>
                <ThresholdFilter level="WARN" onMatch="DENY" onMismatch="NEUTRAL"/>
            </Filters>
           <!-- 
						下面配置数据源以及字段映射;这块也是LOG4J2 比 LOG4J1 退步的地方稍显麻烦;扩展性降低了。
						因为Springboot在启动时便会初始化log4j2,所以这个类里面是不能使用@Autowired 等注解的;
						用了被注入的值也是null。
						小坑预警(耗费了我一整天时间去排查):
						1、我使用的是hibernate 也有一个日志对应的Log类;所以hibernate会给我建表;字段的是按照驼峰原则拆分加_来间隔的。
						2、Colnmn 里面对应的是数据库的字段名称;这里的字段我看了源码,他会被直接拼凑到INSERT SQL中;
 						3、%X{username} 对应的是 MDC(slf4j & log4j1.x)or ThreadContext (log4j2) 因为我使用了slf4j;所以我原有代码基本不需要改造。他会自动的去适配MDC和ThreadContext的关系。
						-->
            <ConnectionFactory 
                class="com.sitech.bds.portal.log4j.extend.ConnectionFactoryConfig" 
             method="getDatabaseConnection"/>
            <Column name="username" pattern="%X{username}"/>
            <Column name="client_ip" pattern="%X{clientIp}"/>
            <Column name="operate_module" pattern="%C"/>
            <Column name="operate_module_name" pattern="%X{operateModuleName}"/>
            <Column name="operate_type" pattern="%M"/>
            <Column name="operate_time" pattern="%d{yyyy-MM-dd HH??ss}"/>
            <Column name="operate_content" pattern="%m"/>
            <Column name="operate_result" pattern="%X{operateResult}"/>
        </JDBC>
    </Appenders>

    <Loggers>
      	<!-- 
						自定义你的类所在的包 的日志级别以及往哪里输出。
						这里有个注意点,经过我实际测试的也是很疑惑的地方;也是和log4j1.x不同之处
						name的值 com.sitech.bds.portal.controller.UserController 一定要取到类之前的包,
						否则过滤不掉有些类。很恶心的一个地方。这样会迫使我定义数个logger,如下所示:
 				-->
        <logger name="com.sitech.bds.portal.controller" level="INFO" additivity="false">
            <AppenderRef ref="DatabaseAppender"/>
        </logger>
        <logger name="com.sitech.bds.portal.serivce.impl" level="INFO" additivity="false">
            <AppenderRef ref="DatabaseAppender"/>
        </logger>
        <logger name="com.sitech.bds.portal.quartz.job" level="INFO" additivity="false">
            <AppenderRef ref="DatabaseAppender"/>
        </logger>
        <logger name="com/sitech/bds/portal/interceptor" level="INFO" additivity="false">
            <AppenderRef ref="DatabaseAppender"/>
        </logger>

        <Root level="ERROR">
            <AppenderRef ref="STD_OUT"/>
        </Root>
    </Loggers>

</Configuration>

Controller 日志记录

获取Logger:

// 导入的包是:org.slf4j.*

public static Logger LOG = LoggerFactory.getLogger(LogController.class);

记录日志:

LOG.info("日志列表导出");

设置模块中文名称

下面的代码放在每一个你需要记录日志的Controller中,例如:UserController、IndexController

@ModelAttribute
	public void doString(){
		MDC.put("operateModuleName","日志管理");
		result.clear();
}

共性属性统一设置

对 IP、操作人这些每次都填充的属性;我创建了BaseController 给其他Controller集成;对共性值进行统一设置。

public class BaseController {
  @Autowired
	HttpServletRequest request;
	Map<String,Object> result = new HashMap<String, Object>();
	protected  boolean flag;
	protected  String message;

	@ModelAttribute
	public void logInit(HttpServletRequest request_){
		User user = (User)request_.getSession().getAttribute("user");
        MDC.put("username",null!=user?user.getUsername():"anonymous");
        MDC.put("clientIp", NetUtil.getIpAddr(request_));
        MDC.put("operateResult", "success");
	}
  
  //其他代码都省略了
  
}  

最终效果:

image-20200714155149940