SpringBoot日志机制
在使用SpringBoot的时候,我们通过在WEB-iNFO/resource
目录下,放入logback.xml
等文件,或者在application.properties
或者applicationo.yml
文件中,设置logging.config
属性值来指定日志配置文件位置的方式,来初始化应用的日志输出规则。甚至以操作都不做,SpringBoot也会默认使用logback来配置日志格式。本文通过跟踪SpringBoot的启动过程,来探究SpringBoot加载日志组件机制。
1. SpringBoot启动流程
以SpringBoot的2.1.4.RELEASE版本为例。
SpringBoot系统是通过在main方法中,执行run方法开始。一个简单SpringBoot系统的main方法代码如下:
@SpringBootApplication
public class GeektimeApplication {
public static void main(String[] args) {
SpringApplication.run(GeektimeApplication.class, args);
}
}
进入到main方法之后,最终会跟踪到
org.springframework.boot.SpringApplication#run(java.lang.String...)
这个方法中,该方法的源码如下:
public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
Collection exceptionReporters = new ArrayList<>();
configureHeadlessProperty();
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
.......
}
在 getRunListeners(args)
中,会读取spring-boot-2.1.4.RELEASE.jar!\META-INF\spring.factories
文件,该文件配置了SpringBoot启动中加载的组件。其中listeners中配置了org.springframework.boot.context.logging.LoggingApplicationListener
。该类即为日志组件初始化的入口。
2. LoggingApplicationListener类
该类的org.springframework.boot.context.logging.LoggingApplicationListener#onApplicationEvent
方法为主要入口方法 。该类的方法如下:
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ApplicationStartingEvent) {
onApplicationStartingEvent((ApplicationStartingEvent) event);
}
else if (event instanceof ApplicationEnvironmentPreparedEvent) {
onApplicationEnvironmentPreparedEvent(
(ApplicationEnvironmentPreparedEvent) event);
}
else if (event instanceof ApplicationPreparedEvent) {
onApplicationPreparedEvent((ApplicationPreparedEvent) event);
}
else if (event instanceof ContextClosedEvent && ((ContextClosedEvent) event)
.getApplicationContext().getParent() == null) {
onContextClosedEvent();
}
else if (event instanceof ApplicationFailedEvent) {
onApplicationFailedEvent();
}
}
第3行调用org.springframework.boot.context.logging.LoggingApplicationListener#onApplicationStartingEvent
方法,改法继续调用org.springframework.boot.logging.LoggingSystem#get(java.lang.ClassLoader)
。LoggingSystem类很重要。它相当于一个工厂类,通过get方法去生成不同日志组件的实例。该方法的实现如下:
public static LoggingSystem get(ClassLoader classLoader) {
String loggingSystem = System.getProperty(SYSTEM_PROPERTY);
if (StringUtils.hasLength(loggingSystem)) {
if (NONE.equals(loggingSystem)) {
return new NoOpLoggingSystem();
}
return get(classLoader, loggingSystem);
}
return SYSTEMS.entrySet().stream()
.filter((entry) -> ClassUtils.isPresent(entry.getKey(), classLoader))
.map((entry) -> get(classLoader, entry.getValue())).findFirst()
.orElseThrow(() -> new IllegalStateException("No suitable logging system located"));
}
其中常量SYSTEM_PROPERTY和SYSTEMS
的值如下:
public static final String SYSTEM_PROPERTY = LoggingSystem.class.getName();
private static final Map SYSTEMS;
static {
Map systems = new LinkedHashMap<>();
systems.put("ch.qos.logback.core.Appender",
"org.springframework.boot.logging.logback.LogbackLoggingSystem");
systems.put("org.apache.logging.log4j.core.impl.Log4jContextFactory",
"org.springframework.boot.logging.log4j2.Log4J2LoggingSystem");
systems.put("java.util.logging.LogManager",
"org.springframework.boot.logging.java.JavaLoggingSystem");
SYSTEMS = Collections.unmodifiableMap(systems);
}
所以,如果在程序启动时,不指定参数-Dorg.springframework.boot.logging.LoggingSystem
的,会默认从集合SYSTEMS
取第一个。因为LogbackLoggingSystem
是第一个实现组件,所以会被默认使用。
找到该组件之后,再回到org.springframework.boot.context.logging.LoggingApplicationListener#onApplicationEvent
方法,就会发现该方法中的onApplicationxxx
方法最终执行的都是LogbackLoggingSystem
类中的beforeInitialize、initialize
等方法。其中在调用LogbackLoggingSystem
的initialize
方法之前,会先调用自己的私有org.springframework.boot.context.logging.LoggingApplicationListener#initialize
方法,在该方法中会调用到org.springframework.boot.context.logging.LoggingApplicationListener#initializeSystem
方法,该方法的源码如下:
private void initializeSystem(ConfigurableEnvironment environment,
LoggingSystem system, LogFile logFile) {
LoggingInitializationContext initializationContext = new LoggingInitializationContext(
environment);
String logConfig = environment.getProperty(CONFIG_PROPERTY);
if (ignoreLogConfig(logConfig)) {
system.initialize(initializationContext, null, logFile);
}
else {
try {
ResourceUtils.getURL(logConfig).openStream().close();
system.initialize(initializationContext, logConfig, logFile);
}
catch (Exception ex) {
// NOTE: We can't use the logger here to report the problem
System.err.println("Logging system failed to initialize "
+ "using configuration from '" + logConfig + "'");
ex.printStackTrace(System.err);
throw new IllegalStateException(ex);
}
}
}
其中常量CONFIG_PROPERTY
值即为logging.config
。如果我们配置了logging.config
,则会取该配置的值进行初始化。但是此处有个限制,即logging.config
配置的值,必须是已xml
或者groovy
,否则就会报错。具体的加载逻辑,在LogbackLoggingSystem的initialize`中完成。
至此,SpringBoot的日志组件创建并加载配置就完成了。
3.如何自定义加载自己的配置文件
通过以上分析,如果想加载自己配置文件,可以通过配置logging.config
文件即可。一般该值配置的为磁盘上某个位置的配置文件,比如d:/logback.xml
。
但是,现在的分布式系统中,一般都会要求配置统一从一个配置中心获取,便于统一管理。所以,如果要加载一个远程配置怎么配置。
方式一 配置远程访问地址:
其实logging.config
配置的值可以是一个网络请求的,只要我们在请求的结尾贬值该请求返回的是xml还是groovy即可。比如,如果使用nacos作为配置中心的话,可以配置如下的值:
http://172.16.10.77:8848/nacos/v1/cs/configs?dataId=自定义的dataId&group=自定义group&xml
通过该方式,也是可以初始化成功的。
以上的方式虽然可以,但是有个局限条件,如果配置中心增加了权限控制的话,就不能通过http请求直接获取配置信息了。所以可以采用以下的方式,即自定义日志组件。
方式二 自定义日志组件:
我们可以试下自己的日志组件,只要继承了SpringBoot提供的日志组件抽象类AbstractLoggingSystem
,实现其抽象方法即可。然后在启动的时候,通过指定启动变量-Dorg.springframework.boot.logging.LoggingSystem
的值为自定义日志组件的全类名称即可。比如:
-Dorg.springframework.boot.logging.LoggingSystem=com.zhlong.springboot.learning.geektime.configuration.MyLogbackLoggingSystem