最近在折腾 Spring Boot 项目的日志存储方案,踩了不少坑。尤其是想把日志直接写入数据库时,尝试了几种方式,最终发现 Log4j2 的 JDBC Appender 是一个比较实用的选择。
项目使用的 Spring Boot 版本是 2.2.3.RELEASE,日志框架方面,门面采用 SLF4J 1.7.30,具体实现则选择了 JUL 和 Log4j 2.12.1(记得排除自带的 Logback)。此外还需要 log4j-slf4j 桥接包,版本同样为 2.12.1。
参考官方文档,整体实现思路其实比较清晰。

官方文档表述得很直接:核心就是获取一个 java.sql.Connection,并且官方还提供了一个现成的配置示例。
Appenders 配置详解
首先看 Appenders 的配置。关键属性不多:tableName 指定数据库表名,class 指定获取 Connection 的工厂类,method 对应工厂类中的具体方法名。
在每个 Column 标签中,可以配置三种模式:literal 用来写死 SQL 字面量(例如数据库序列),pattern 用于从日志事件中提取字段,isEventTimestamp 用来标记时间戳字段。
下面是配置示例代码:
获取 Connection 的实现方式
Connection 的获取方式,官方也给出了一个完整示例,使用了 Apache DBCP 连接池。关键在于:Log4j2 只负责调用你提供的工厂方法,而连接池管理、事务控制、自动重连等都需要自己处理。
示例代码如下:
package net.example.db;
import ja va.sql.Connection;
import ja va.sql.SQLException;
import ja va.util.Properties;
import ja vax.sql.DataSource;
import org.apache.commons.dbcp.DriverManagerConnectionFactory;
import org.apache.commons.dbcp.PoolableConnection;
import org.apache.commons.dbcp.PoolableConnectionFactory;
import org.apache.commons.dbcp.PoolingDataSource;
import org.apache.commons.pool.impl.GenericObjectPool;
public class ConnectionFactory {
private static interface Singleton {
final ConnectionFactory INSTANCE = new ConnectionFactory();
}
private final DataSource dataSource;
private ConnectionFactory() {
Properties properties = new Properties();
properties.setProperty("user", "logging");
properties.setProperty("password", "abc123");
GenericObjectPool pool = new GenericObjectPool();
DriverManagerConnectionFactory connectionFactory = new DriverManagerConnectionFactory(
"jdbc:mysql://example.org:3306/exampleDb", properties);
new PoolableConnectionFactory(connectionFactory, pool, null, "SELECT 1", 3, false, false,
Connection.TRANSACTION_READ_COMMITTED);
this.dataSource = new PoolingDataSource(pool);
}
public static Connection getDatabaseConnection() throws SQLException {
return Singleton.INSTANCE.dataSource.getConnection();
}
} 这样配置似乎就完成了?别急,有几个关键坑必须提前说清楚。
第一,尽管 Log4j2 给出了连接池的使用建议,但它本身并不管理连接池、事务和自动重连。也就是说,你提供的 Connection 工厂方法必须在任何情况下都能返回一个有效连接。如果连接被关闭,框架不会自动重连,你需要自己编写逻辑来检测并重建连接。
第二,这一点在实际项目中尤其容易遇到。很多项目已经集成了 Druid 等数据库连接池,但 Log4j2 写入日志时,这些框架可能尚未加载完成(或未完成初始化),结果导致日志写入失败,程序却默默吞掉了异常。使用前一定要仔细考虑框架的加载时机。
再分享一个我自己在项目中使用的简单实现,比官方的 DBCP 示例更容易理解:
import ja va.sql.Connection;
import ja va.sql.DriverManager;
import ja va.sql.SQLException;
public class ConnectionFactory {
private static Connection connection = null;
public static Connection getDatabaseConnection() {
try {
if (connection == null || connection.isClosed()) {
synchronized (ConnectionFactory.class) {
if (connection == null || connection.isClosed()) {
connection = DriverManager.getConnection(
"jdbc:mysql://**.**.cn:3306/**?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai",
"**", "**");
}
}
}
} catch (SQLException e) {
e.printStackTrace();
}
return connection;
}
}对应的建表 SQL 也直接明了:
create table tbl_log (
ID int auto_increment primary key,
EVENT_DATE datetime null,
LEVEL varchar(20) null,
LOGGER varchar(20) null,
MESSAGE text null,
THROWABLE text null
);最后看看实际运行的效果:

整体思路并不复杂,但细节稍不注意就容易翻车。尤其是框架加载时序和连接管理这两部分,建议在实际项目中多测试几遍再上线。
