系列
实现自定义SpringBoot框架日志组件の一:日志系统
实现自定义SpringBoot框架日志组件の二:配置文件
实现自定义SpringBoot框架日志组件の三: 自定义pattern
实现自定义SpringBoot框架日志组件の四: 自适应
目录
前言
日志组件目标
版本
spring boot 日志系统
YellowLog4J2LoggingSystem
多环境支持
激活彩色日志
开启异步
如何加载
前言
最近工作中要实现一个日志组件,实现后觉得效果还不错,所以写一篇博客分享给大家
因为公司屏幕带水印,好多截图无法直接上传,所以只好回家重新实现一遍,代码也传到了 github
项目地址
日志组件目标
- 替代spring默认的log,达到一个依赖即可拥有默认级别、输出样式等效果
- 针对不同环境,设置不同的配置,如生产环境不输出console,级别为info等
- 自定义一个输出 pattern,做一些自定义的规则
本文旨在实现目标1
, 且日志组件的选型是 log4j2
版本
spring
: 2.6.11
java
: 11
spring boot 日志系统
想要实现自定义的日志组件,需要先了解一下 spring boot 的日志系统是怎么玩的
具体流程和源码看这篇博客 Springboot 源码分析之log配置加载
这篇博客spring的版本有点老了,和比较新的spring代码对不上,但是大致流程是一样的,不影响
并且本文用的spring版本也不是最新的,大家实际实现的时候也需要根据spring源码做相应的适配(如果需要的话)
这里简单介绍一下spring的日志系统流程:
- 初始化日志系统
LoggingSystem
入口在org.springframework.boot.context.logging.LoggingApplicationListener#onApplicationEnvironmentPreparedEvent
, 根据实际依赖的日志实现选择不同的日志组件,如logback、log4j2等 - 根据不同的日志系统加载不同的日志配置文件
- 配置文件是否指定
logging.config
, 如果指定,直接读取、加载,结束 - 没有指定的话,加载
LoggingSystem
实现类提供的文件名列表(log4j2.xml log4j2.json等),如果存在就加载 - 还是没有的话,加载带spring后缀的文件,log4j2-spring.xml,存在就加载
- 还是不存在,就加载默认的(
loadDefault
),配置文件在jar包里
根据上面的流程,我们想要达到目标,实现思路就是:
- 自定义一个日志系统
- 重载
loadDefault
, 加载我们自己的配置文件 - 根据不同的环境(spring.profile),加载不同的配置文件(功能点:多环境支持)
- 代码里直接开启log4j2的异步功能(默认需要配置文件主动开启)(功能点:异步)
- 代码里直接开启log4j2的彩色控制台功能(默认需要配置文件主动开启)(功能点:彩色日志)
YellowLog4J2LoggingSystem
因为我们的日志系统只需要重载 loadDefault
,其它功能都不变,那么直接继承Log4J2LoggingSystem
就行
我们先看一下Log4J2LoggingSystem
的loadDefault
代码
@Override
protected void loadDefaults(LoggingInitializationContext initializationContext, LogFile logFile) {
if (logFile != null) {
// 配置了日志输出文件
loadConfiguration(getPackagedConfigFile("log4j2-file.xml"), logFile, getOverrides(initializationContext));
}
else {
// 什么都没有配置
loadConfiguration(getPackagedConfigFile("log4j2.xml"), logFile, getOverrides(initializationContext));
}
}
可以看出,我们要修改的逻辑就是:改变else里面的逻辑
而且我们看源码可以发现,代码里的 log4j2.xml
文件其实在这个jar包里
到了这里,我们就有了下面的代码
package com.github.hwhaocool;
import org.springframework.boot.context.properties.bind.BindResult;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.logging.LogFile;
import org.springframework.boot.logging.LoggingInitializationContext;
import org.springframework.boot.logging.log4j2.Log4J2LoggingSystem;
import java.util.Collections;
import java.util.List;
/**
* @author yellowtail
* @since 2022/8/21 17:48
*/
public class YellowLog4J2LoggingSystem extends Log4J2LoggingSystem {
public YellowLog4J2LoggingSystem(ClassLoader classLoader) {
super(classLoader);
}
@Override
protected void loadDefaults(LoggingInitializationContext initializationContext, LogFile logFile) {
if (logFile != null) {
// 配置了日志输出文件
loadConfiguration(getPackagedConfigFile("log4j2-file.xml"), logFile, getOverrides(initializationContext));
}
else {
// 什么都没有配置
loadConfiguration(getPackagedConfigFile("log4j2.xml"), logFile, getOverrides(initializationContext));
}
}
// 直接复制的,没有做任何改变
private List getOverrides(LoggingInitializationContext initializationContext) {
BindResult> overrides = Binder.get(initializationContext.getEnvironment())
.bind("logging.log4j2.config.override", Bindable.listOf(String.class));
return overrides.orElse(Collections.emptyList());
}
}
多环境支持
logback
的配置文件里支持标签 SpringProfile
, 但是 log4j2 默认是不支持的,所以多环境必须通过多个文件来实现
还好LoggingInitializationContext initializationContext
里面有Environment
,里面有当前环境的信息
/**
* 得到当前的环境(spring.profile.active)
* @param environment
* @return
*/
private ProfileEnum getProfile(Environment environment) {
String[] activeProfiles = environment.getActiveProfiles();
if (null == activeProfiles || activeProfiles.length == 0) {
return ProfileEnum.LOCAL;
}
switch (activeProfiles[0]) {
case "local":
return ProfileEnum.LOCAL;
case "dev":
case "develop":
return ProfileEnum.DEV;
case "prod":
case "prd":
case "product":
default:
return ProfileEnum.PROD;
}
}
static enum ProfileEnum{
/**
* 本地debug环境
*/
LOCAL,
/**
* dev环境
*/
DEV,
/**
* 生产 环境
*/
PROD
}
不同环境使用不同的配置文件
private String getConfigFile(ProfileEnumprofile) {
switch (profile) {
case DEV:
return "log4j2-dev.xml";
case PROD:
return "log4j2-prod.xml";
case LOCAL:
default:
return "log4j2-local.xml";
}
}
激活彩色日志
默认是不开启的(因为影响性能),需要手动开启
/**
* 激活彩色日志
* @param profile
*/
private void enableColor(ProfileEnum profile) {
if (ProfileEnum.LOCAL == profile || ProfileEnum.DEV == profile) {
System.setProperty("log4j.skipJansi", "false");
}
}
开启异步
官方文档
默认是不开启的,需要手动开启
-Dlog4j2.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector or
用环境变量是无法直接开启的,需要通过配置文件log4j2.component.properties
(这个信息我是怎么知道的呢?去 github看源码,看一下log4j2.contextSelector
是从哪里读取的)
log4j2.component.properties
log4j2.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector
log4j2.formatMsgNoLookups=true
还需要主动引入依赖
com.lmax
disruptor
3.4.4
如何加载
现在我们实现完了,那么如何让spring加载它呢?或者说,因为我们是继承Log4J2LoggingSystem
的,我们如何先于默认的加载呢?
这里就要借助spring.factories
了
我们看一下Log4J2LoggingSystem
的 useage 就知道了
# Logging Systems
org.springframework.boot.logging.LoggingSystemFactory=\
org.springframework.boot.logging.logback.LogbackLoggingSystem.Factory,\
org.springframework.boot.logging.log4j2.Log4J2LoggingSystem.Factory,\
org.springframework.boot.logging.java.JavaLoggingSystem.Factory
可以看到每一行后面还有一个Factory
这里是一个内部静态类,是用来构建日志系统的,spring加载的时候会执行,不写会报错
@Order(Ordered.LOWEST_PRECEDENCE)
public static class Factory implements LoggingSystemFactory {
private static final boolean PRESENT = ClassUtils
.isPresent("org.apache.logging.log4j.core.impl.Log4jContextFactory", Factory.class.getClassLoader());
@Override
public LoggingSystem getLoggingSystem(ClassLoader classLoader) {
if (PRESENT) {
return new Log4J2LoggingSystem(classLoader);
}
return null;
}
}
代码很好理解
- PRESENT, 是否存在日志组件
- getLoggingSystem, 构建并返回日志系统
且我们看到有一个Order
注解,这个就是每个日志组件的优先级,spring提供和的是LOWEST_PRECEDENCE
, 就是最低优先级的,我们只需要改一下优先级就可以了
注意:order的优先级是 数值越大,优先级月底;数值越小,优先级越高
所以我们的代码最后如下
package com.github.hwhaocool;
import org.springframework.boot.context.properties.bind.BindResult;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.logging.LogFile;
import org.springframework.boot.logging.LoggingInitializationContext;
import org.springframework.boot.logging.LoggingSystem;
import org.springframework.boot.logging.LoggingSystemFactory;
import org.springframework.boot.logging.log4j2.Log4J2LoggingSystem;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.Environment;
import org.springframework.util.ClassUtils;
import java.util.Collections;
import java.util.List;
/**
* @author yellowtail
* @since 2022/8/21 17:48
*/
public class YellowLog4J2LoggingSystem extends Log4J2LoggingSystem {
public YellowLog4J2LoggingSystem(ClassLoader classLoader) {
super(classLoader);
}
@Override
protected void loadDefaults(LoggingInitializationContext initializationContext, LogFile logFile) {
if (logFile != null) {
// 配置了日志输出文件
loadConfiguration(getPackagedConfigFile("log4j2-file.xml"), logFile, getOverrides(initializationContext));
}
else {
// 什么都没有配置
// 1. 得到当前的环境(spring.profile.active)
ProfileEnum profile = getProfile(initializationContext.getEnvironment());
// 2. 当前环境应该加载的配置文件
String logConfigFile = getConfigFile(profile);
// 3. local dev 激活彩色console日志
enableColor(profile);
// 4. 开启异步
enableAsync();
// 5. 加载
loadConfiguration(getPackagedConfigFile(logConfigFile), logFile, getOverrides(initializationContext));
// 6. 自适应包名
autofixPackage();
}
}
private void autofixPackage() {
// TODO: 待实现
}
/**
* 开启异步
*/
private void enableAsync() {
System.setProperty("log4j2.contextSelector", "org.apache.logging.log4j.core.async.AsyncLoggerContextSelector");
}
/**
* 激活彩色日志
* @param profile
*/
private void enableColor(ProfileEnum profile) {
if (ProfileEnum.LOCAL == profile || ProfileEnum.DEV == profile) {
System.setProperty("log4j.skipJansi", "false");
}
}
/**
* 当前环境应该加载的配置文件
* @param profile
* @return
*/
private String getConfigFile(ProfileEnum profile) {
switch (profile) {
case DEV:
return "log4j2-dev.xml";
case PROD:
return "log4j2-prod.xml";
case LOCAL:
default:
return "log4j2-local.xml";
}
}
/**
* 得到当前的环境(spring.profile.active)
* @param environment
* @return
*/
private ProfileEnum getProfile(Environment environment) {
String[] activeProfiles = environment.getActiveProfiles();
if (null == activeProfiles || activeProfiles.length == 0) {
return ProfileEnum.LOCAL;
}
switch (activeProfiles[0]) {
case "local":
return ProfileEnum.LOCAL;
case "dev":
case "develop":
return ProfileEnum.DEV;
case "prod":
case "prd":
case "product":
default:
return ProfileEnum.PROD;
}
}
// 直接复制的,没有做任何改变
private List getOverrides(LoggingInitializationContext initializationContext) {
BindResult> overrides = Binder.get(initializationContext.getEnvironment())
.bind("logging.log4j2.config.override", Bindable.listOf(String.class));
return overrides.orElse(Collections.emptyList());
}
static enum ProfileEnum {
/**
* 本地debug环境
*/
LOCAL,
/**
* dev环境
*/
DEV,
/**
* 生产 环境
*/
PROD
}
/**
* 初始化YellowLog4J2LoggingSystem
* order 调整一下,稍微加一点优先级
* @author yellowtail
*/
@Order(Ordered.LOWEST_PRECEDENCE-100)
public static class Factory implements LoggingSystemFactory {
private static final boolean PRESENT = ClassUtils
.isPresent("org.apache.logging.log4j.core.impl.Log4jContextFactory", Log4J2LoggingSystem.Factory.class.getClassLoader());
@Override
public LoggingSystem getLoggingSystem(ClassLoader classLoader) {
if (PRESENT) {
return new YellowLog4J2LoggingSystem(classLoader);
}
return null;
}
}
}
其中6. 自适应包名
暂时没有实现,实现起来有点繁琐,后面讲到,先忽略
自适应包名:我们作为一个框架组件,其实是不知道使用方的 package 的,而我们日志配置文件里又都是通过 pakcage 来指定级别的,所以我们需要去获取包名,然后根据不同的环境去设置日志级别
并且有一个文件 src\main\resources\META-INF\spring.factories
# Logging Systems
org.springframework.boot.logging.LoggingSystemFactory=\
com.github.hwhaocool.YellowLog4J2LoggingSystem.Factory
到这里自定义日志系统就实现完成了,当然了我们目前还缺一些功能
- 各个
Log4j2.xml
- 自适应包名
下一篇的博客会讲到