Apache Log4j 2是对Log4j的升级,它比其前身Log4j 1.x提供了重大改进,并提供了Logback中可用的许多改进,同时修复了Logback架构中的一些问题。是目前最优秀的Java日志框架,没有之一。
官方Appenders提供了日志的多种输出方式实现。
下面我们以 JDBCAppender 为例来说明如何在项目中实现系统日志保存到数据库。
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;
<configuration status="off" monitorInterval="0">
<properties>
<property name="LOG_HOME">../logsproperty>
<property name="PROJECT">springproperty>
<property name="FORMAT">%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%nproperty>
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>
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();
}
}
@Slf4j
@Component
@Configuration
public class ScheduleJob
{
@Value("${welcome.message:hello, 00fly in java!}")
private String welcome;
@Autowired
private 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");
}
}
@Bean
public ScheduledExecutorService scheduledExecutorService()
{
// return Executors.newScheduledThreadPool(5);
return new ScheduledThreadPoolExecutor(5, new CustomizableThreadFactory("schedule-pool-"));
}
}
mysql 数据库日志数据如下在log4j2.xml中设置了 bufferSize=“20”,这边日志容量达到20才执行一次批量保存。
https://gitee.com/00fly/java-code-frame/tree/master/springmvc-dbutils
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)
);
<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>
/**
*
* 日志数据库数据源
*
* @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();
}
}
工程中log4j2组件的初始化一般早于springboot工程,这里采用log4j2.xml引入JDBCAppender,故LogPoolManager无法获取springboot管理的DataSource, 大家网上搜到的demo大部分采用写死数据库连接参数的形式,不利于维护。
上面采用的读取数据库配置文件的方式,在以下场景会导致无法读取正确的数据库配置,日志无法保存的问题:
java -jar -Dspring.profiles.active=dev springboot-hello.jar --spring.config.location=./application-other.yml
java -jar springboot-hello.jar --spring.profiles.active=dev --spring.config.location=./application-other.yml
services:
hello:
image: registry.cn-shanghai.aliyuncs.com/00fly/springboot-hello-swagger2:1.0.0
container_name: hello-random
deploy:
resources:
limits:
cpus: '1'
memory: 200M
reservations:
memory: 180M
ports:
- 8080:8082
entrypoint: 'sh wait-for.sh 172.88.88.11:3306 -- java -jar /app.jar'
environment:
JAVA_OPTS: -server -Xms200m -Xmx200m -Djava.security.egd=file:/dev/./urandom
SPRING_DATASOURCE_URL: jdbc:mysql://172.88.88.11:3306/hello?useSSL=false&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true
SPRING_DATASOURCE_DRIVERCLASSNAME: com.mysql.cj.jdbc.Driver
SPRING_DATASOURCE_USERNAME: root
SPRING_DATASOURCE_PASSWORD: root123
restart: on-failure
logging:
driver: json-file
options:
max-size: 5m
max-file: '1'
将第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;
}
@Override
public 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()};
// 配置appender
final 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;
}
@Override
public Connection getConnection()
throws SQLException
{
return dataSource.getConnection();
}
}
改造前代码:
https://gitee.com/00fly/effict-side/tree/master/springboot-hello
改造后javaConfig代码:
https://gitee.com/00fly/effict-side/tree/master/springboot-hello-swagger2
有任何问题和建议,都可以向我提问讨论,大家一起进步,谢谢!
-over-