文章目录
- 概述
- 一、springmvc工程
- 1.创建数据库日志表
- 2.log4j2.xml引入JDBCAppender
- 3.定义日志管理类
- 4.编写日志输出代码
- 5.运行结果
- 6.完整代码
- 二、springboot工程
- 1. 创建数据库日志表
- 2.log4j2.xml引入JDBCAppender
- 3.定义日志管理类
- 4. 遗留问题
- 5. 解决办法
- 6. 完整代码
概述
Apache Log4j 2是对Log4j的升级,它比其前身Log4j 1.x提供了重大改进,并提供了Logback中可用的许多改进,同时修复了Logback架构中的一些问题。是目前最优秀的Java日志框架,没有之一。
官方Appenders提供了日志的多种输出方式实现。
下面我们以 JDBCAppender 为例来说明如何在项目中实现系统日志保存到数据库。
一、springmvc工程
1.创建数据库日志表
CREATE TABLE IF NOT EXISTS boot_log ( `id` bigint NOT NULL AUTO_INCREMENT,`event_id` varchar(50) ,`event_date` datetime ,`thread` varchar(255) ,`class` varchar(255) ,`function` varchar(255) ,`message` varchar(255) ,`exception` text,`level` varchar(255) ,`time` datetime,PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
2.log4j2.xml引入JDBCAppender
<?xml version="1.0" encoding="UTF-8"?>
<configuration status="off" monitorInterval="0"><properties><property name="LOG_HOME">../logs</property><property name="PROJECT">spring</property><property name="FORMAT">%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n</property></properties><appenders><console name="Console" target="system_out"><patternLayout pattern="${FORMAT}" /></console><JDBC name="databaseAppender" bufferSize="20" tableName="boot_log"><ConnectionFactory class="com.fly.core.log.LogPoolManager" method="getConnection" /><Column name="event_id" pattern="%X{id}" /><Column name="event_date" isEventTimestamp="true" /><Column name="thread" pattern="%t %x" /><Column name="class" pattern="%C" /><Column name="`function`" pattern="%M" /><Column name="message" pattern="%m" /><Column name="exception" pattern="%ex{full}" /><Column name="level" pattern="%level" /><Column name="time" pattern="%d{yyyy-MM-dd HH:mm:ss.SSS}" /></JDBC></appenders><loggers><logger name="org.springframework" level="INFO" /><root level="INFO"><appender-ref ref="Console" /><appender-ref ref="databaseAppender" /></root></loggers>
</configuration>
3.定义日志管理类
LogPoolManager.java
/*** * 日志数据库数据源* * @author 00fly* @version [版本号, 2023年3月27日]* @see [相关类/方法]* @since [产品/模块版本]*/
public final class LogPoolManager
{private LogPoolManager(){super();}/*** getConnection* * @return* @throws SQLException* @see [类、类#方法、类#成员]*/public static Connection getConnection()throws SQLException{// TODO: mvc工程下使用此写法,可行,boot工程不行DataSource dataSource = SpringContextUtils.getBean(DataSource.class);Assert.notNull(dataSource, "dataSource is null");return dataSource.getConnection();}
}
4.编写日志输出代码
@Slf4j
@Component
@Configuration
public class ScheduleJob
{@Value("${welcome.message:hello, 00fly in java!}")private String welcome;@Autowiredprivate JdbcTemplate jdbcTemplate;@Scheduled(cron = "0/10 * 7-20 * * ?")public void run(){log.info("---- {}", welcome);long count = jdbcTemplate.queryForObject("select count(*) from boot_log", Long.class);log.info("------------ boot_log count: {} ----------", count);if (count > 100){log.info("###### truncate table boot_log ######");jdbcTemplate.execute("truncate table boot_log");}}@Beanpublic ScheduledExecutorService scheduledExecutorService(){// return Executors.newScheduledThreadPool(5);return new ScheduledThreadPoolExecutor(5, new CustomizableThreadFactory("schedule-pool-"));}
}
5.运行结果
mysql 数据库日志数据如下在log4j2.xml中设置了 bufferSize=“20”,这边日志容量达到20才执行一次批量保存。
6.完整代码
https://gitee.com/00fly/java-code-frame/tree/master/springmvc-dbutils
二、springboot工程
1. 创建数据库日志表
CREATE TABLE IF NOT EXISTS boot_log ( `id` bigint NOT NULL AUTO_INCREMENT ,`event_id` varchar(50) ,`event_date` datetime ,`thread` varchar(255) ,`class` varchar(255) ,`function` varchar(255) ,`message` varchar(255) ,`exception` text,`level` varchar(255) ,`time` datetime,
PRIMARY KEY (id)
);
2.log4j2.xml引入JDBCAppender
<!-- bufferSize 没起作用,待排查 --><JDBC name="databaseAppender" bufferSize="20" tableName="boot_log"><ConnectionFactory class="com.fly.core.log.LogPoolManager" method="getConnection" /><Column name="event_id" pattern="%X{id}" /><Column name="event_date" isEventTimestamp="true" /><Column name="thread" pattern="%t %x" /><Column name="class" pattern="%C" /><Column name="`function`" pattern="%M" /><Column name="message" pattern="%m" /><Column name="exception" pattern="%ex{full}" /><Column name="level" pattern="%level" /><Column name="time" pattern="%d{yyyy-MM-dd HH:mm:ss.SSS}" /></JDBC>
3.定义日志管理类
/*** * 日志数据库数据源* * @author 00fly* @version [版本号, 2023年3月27日]* @see [相关类/方法]* @since [产品/模块版本]*/
public final class LogPoolManager
{private static DataSource dataSource;private LogPoolManager(){super();}/*** boot启动时指定的外部配置文件位置*/private static String configLocation;public static void setConfigLocation(String configLocation){LogPoolManager.configLocation = configLocation;}/*** 不能静态初始化 DataSource,否则无法加载外部配置文件*/public static synchronized void init(){try{// 加载外部配置文件if (StringUtils.isNotBlank(configLocation)){File file = new File(configLocation);String text = FileUtils.readFileToString(file, StandardCharsets.UTF_8.toString());Properties props = YamlUtils.yamlToProperties(text);dataSource = DataSourceBuilder.create().type(DruidDataSource.class).url(props.getProperty("spring.datasource.url")).username(props.getProperty("spring.datasource.username")).password(props.getProperty("spring.datasource.password")).build();}else{// TODO: 数据源通过spring.profiles.active指定或docker-compose环境变量注入,怎么改写下面的逻辑?Resource resource = new ClassPathResource("application.yml");String text = IOUtils.toString(resource.getURL(), StandardCharsets.UTF_8.toString());boolean dev = StringUtils.contains(text, "dev");Properties properties = PropertiesLoaderUtils.loadProperties(new ClassPathResource(dev ? "jdbc-h2.properties" : "jdbc-mysql.properties"));dataSource = DruidDataSourceFactory.createDataSource(properties);}}catch (Exception e){e.printStackTrace();}}/*** getConnection* * @return* @throws SQLException* @see [类、类#方法、类#成员]*/public static Connection getConnection()throws SQLException{if (dataSource == null){init();}Assert.notNull(dataSource, "dataSource can not be null");return dataSource.getConnection();}
}
4. 遗留问题
工程中log4j2组件的初始化一般早于springboot工程,这里采用log4j2.xml引入JDBCAppender,故LogPoolManager无法获取springboot管理的DataSource, 大家网上搜到的demo大部分采用写死数据库连接参数的形式,不利于维护。
上面采用的读取数据库配置文件的方式,在以下场景会导致无法读取正确的数据库配置,日志无法保存的问题:
- 数据源通过spring.profiles.active指定
- 数据源docker-compose环境变量注入
5. 解决办法
将第2部的log4j2.xml引入JDBCAppender改写为使用javaConfig方式。
Log4j2Configuration.java
@Component
public class Log4j2Configuration implements ApplicationListener<ContextRefreshedEvent>
{private final DataSource dataSource;public Log4j2Configuration(DataSource dataSource){this.dataSource = dataSource;}@Overridepublic void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent){final LoggerContext ctx = LoggerContext.getContext(false);final ColumnConfig[] cc ={ColumnConfig.newBuilder().setConfiguration(ctx.getConfiguration()).setName("event_id").setPattern("%X{id}").setUnicode(false).build(),ColumnConfig.newBuilder().setConfiguration(ctx.getConfiguration()).setName("event_date").setEventTimestamp(true).setUnicode(false).build(),ColumnConfig.newBuilder().setConfiguration(ctx.getConfiguration()).setName("thread").setPattern("%t %x").setUnicode(false).build(),ColumnConfig.newBuilder().setConfiguration(ctx.getConfiguration()).setName("class").setPattern("%C").setUnicode(false).build(),ColumnConfig.newBuilder().setConfiguration(ctx.getConfiguration()).setName("`function`").setPattern("%M").setUnicode(false).build(),ColumnConfig.newBuilder().setConfiguration(ctx.getConfiguration()).setName("message").setPattern("%m").setUnicode(false).build(),ColumnConfig.newBuilder().setConfiguration(ctx.getConfiguration()).setName("exception").setPattern("%ex{full}").setUnicode(false).build(),ColumnConfig.newBuilder().setConfiguration(ctx.getConfiguration()).setName("level").setPattern("%level").setUnicode(false).build(),ColumnConfig.newBuilder().setConfiguration(ctx.getConfiguration()).setName("time").setPattern("%d{yyyy-MM-dd HH:mm:ss.SSS}").setUnicode(false).build()};// 配置appenderfinal Appender appender = JdbcAppender.newBuilder().setName("databaseAppender").setIgnoreExceptions(false).setConnectionSource(new ConnectionFactory(dataSource)).setTableName("boot_log").setColumnConfigs(cc).setColumnMappings(new ColumnMapping[0]).build();appender.start();ctx.getConfiguration().addAppender(appender);// 指定哪些logger输出的日志保存在mysql中ctx.getConfiguration().getLoggerConfig("com.fly.core.log.job").addAppender(appender, Level.INFO, null);ctx.updateLoggers();}
}
ConnectionFactory.java
public class ConnectionFactory extends AbstractConnectionSource
{private final DataSource dataSource;public ConnectionFactory(DataSource dataSource){Assert.notNull(dataSource, "dataSource can not be null");this.dataSource = dataSource;}@Overridepublic Connection getConnection()throws SQLException{return dataSource.getConnection();}
}
6. 完整代码
改造前代码:
https://gitee.com/00fly/effict-side/tree/master/springboot-hello
改造后javaConfig代码:
https://gitee.com/00fly/effict-side/tree/master/springboot-hello-swagger2
有任何问题和建议,都可以向我提问讨论,大家一起进步,谢谢!
-over-