目录
2.1 ApplicationEnvironmentPreparedEvent
2.5 initializeFinalLoggingLevels方法
一、前话
建议看这篇之前先看一下(一)Logback-slf4j日志原理及源码启动分析文章,了解Logback的启动原理及流程。再者需要了解Springboot启动流程的一些基本流程,如Springboot的启动监听机制。
二、原理分析
1.LoggingApplicationListener
在Springboot启动流程中会读取spring.factories文件,并把其中的ApplicationListener实现子类一一加载缓存起来,以便后续直接使用。Springboot和Logback等日志集成的关键类便是实现子类之一LoggingApplicationListener。大致看一下其类的关键代码部分:
public class LoggingApplicationListener
implements GenericApplicationListener {
private static final ConfigurationPropertyName LOGGING_LEVEL =
ConfigurationPropertyName.of("logging.level");
private static final ConfigurationPropertyName LOGGING_GROUP =
ConfigurationPropertyName.of("logging.group");
private static final Bindable<Map<String, String>> STRING_STRING_MAP =
Bindable.mapOf(String.class, String.class);
private static final Bindable<Map<String, String[]>> STRING_STRINGS_MAP
= Bindable.mapOf(String.class, String[].class);
public static final int DEFAULT_ORDER =
Ordered.HIGHEST_PRECEDENCE + 20;
public static final String CONFIG_PROPERTY = "logging.config";
public static final String REGISTER_SHUTDOWN_HOOK_PROPERTY =
"logging.register-shutdown-hook";
public static final String LOGGING_SYSTEM_BEAN_NAME =
"springBootLoggingSystem";
private static final Map<String, List<String>> DEFAULT_GROUP_LOGGERS;
static {
MultiValueMap<String, String> loggers = new LinkedMultiValueMap<>();
loggers.add("web", "org.springframework.core.codec");
loggers.add("web", "org.springframework.http");
loggers.add("web", "org.springframework.web");
loggers.add("web", "org.springframework.boot.actuate.endpoint.web");
loggers.add("web",
"org.springframework.boot.web.servlet." +
"ServletContextInitializerBeans");
loggers.add("sql", "org.springframework.jdbc.core");
loggers.add("sql", "org.hibernate.SQL");
DEFAULT_GROUP_LOGGERS = Collections.unmodifiableMap(loggers);
}
private static final Map<LogLevel, List<String>> LOG_LEVEL_LOGGERS;
static {
MultiValueMap<LogLevel, String> loggers =
new LinkedMultiValueMap<>();
loggers.add(LogLevel.DEBUG, "sql");
loggers.add(LogLevel.DEBUG, "web");
loggers.add(LogLevel.DEBUG, "org.springframework.boot");
loggers.add(LogLevel.TRACE, "org.springframework");
loggers.add(LogLevel.TRACE, "org.apache.tomcat");
loggers.add(LogLevel.TRACE, "org.apache.catalina");
loggers.add(LogLevel.TRACE, "org.eclipse.jetty");
loggers.add(LogLevel.TRACE, "org.hibernate.tool.hbm2ddl");
LOG_LEVEL_LOGGERS = Collections.unmodifiableMap(loggers);
}
private static final Class<?>[] EVENT_TYPES =
{ ApplicationStartingEvent.class,
ApplicationEnvironmentPreparedEvent.class,
ApplicationPreparedEvent.class, ContextClosedEvent.class,
ApplicationFailedEvent.class };
private static final Class<?>[] SOURCE_TYPES =
{ SpringApplication.class, ApplicationContext.class };
private static final AtomicBoolean shutdownHookRegistered =
new AtomicBoolean(false);
private LoggingSystem loggingSystem;
private int order = DEFAULT_ORDER;
private boolean parseArgs = true;
private LogLevel springBootLogging = null;
}
从这些成员变量的值来看就可以猜个十有八九了,logging.level和logging.config分别是日志级别以及日志配置文件路径,DEFAULT_GROUP_LOGGERS和LOG_LEVEL_LOGGERS则分别是设置默认的日志分组以及给默认的logger添加默认日志级别。sql和web则分别对应了
2.ApplicationEvent
当Springboot入口已经执行到run方法时,最先调用的监听事件则是ApplicationStartingEvent,意味着Springboot已经开始初始化。而在这个事件被发布的时候,LoggingApplicationListener监听器将会监听这个事件,其监听方法关键源码如下:
public class LoggingApplicationListener
implements GenericApplicationListener {
@Override
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();
}
}
private void onApplicationStartingEvent(ApplicationStartingEvent event){
this.loggingSystem = LoggingSystem.get(event.getSpringApplication()
.getClassLoader());
this.loggingSystem.beforeInitialize();
}
private void onApplicationEnvironmentPreparedEvent(
ApplicationEnvironmentPreparedEvent event) {
if (this.loggingSystem == null) {
this.loggingSystem = LoggingSystem.get(event
.getSpringApplication().getClassLoader());
}
initialize(event.getEnvironment(), event.getSpringApplication()
.getClassLoader());
}
private void onApplicationPreparedEvent(ApplicationPreparedEvent event{
ConfigurableListableBeanFactory beanFactory = event
.getApplicationContext().getBeanFactory();
if (!beanFactory.containsBean(LOGGING_SYSTEM_BEAN_NAME)) {
beanFactory.registerSingleton(LOGGING_SYSTEM_BEAN_NAME,
this.loggingSystem);
}
}
private void onContextClosedEvent() {
if (this.loggingSystem != null) {
this.loggingSystem.cleanUp();
}
}
private void onApplicationFailedEvent() {
if (this.loggingSystem != null) {
this.loggingSystem.cleanUp();
}
}
}
上面列出了针对Springboot初始化启动时所监听的五种事件,下面分别介绍这五种事件调用方法的大致作用:
- ApplicationStartingEvent事件:将会调用到onApplicationStartingEvent方法中去,而在这个方法中则初始化了loggingSystem成员变量并调用了其beforeInitialize初始化前的前置方法。;
- ApplicationEnvironmentPreparedEvent事件:判断loggingSystem是否为空,为空则对其赋值,不为空则利用事件的属性进行初始化;
- ApplicationPreparedEvent事件:判断Spring上下文是否准备就绪,以及Spring工厂中是否含有springBootLoggingSystem类,如果没有则将loggingSystem对象注册进工厂;
- ContextClosedEvent和ApplicationFailedEvent事件:Spring程序已经关闭或启动失败,将会调用loggingSystem对象的cleanUp方法清除日志缓存以及内存中的对象。
2.1 ApplicationEnvironmentPreparedEvent
其中特别是针对ApplicationEnvironmentPreparedEvent事件的处理方法initialize需要特别说明一下,其方法源码如下:
public class LoggingApplicationListener
implements GenericApplicationListener {
protected void initialize(ConfigurableEnvironment environment,
ClassLoader classLoader) {
new LoggingSystemProperties(environment).apply();
LogFile logFile = LogFile.get(environment);
if (logFile != null) {
logFile.applyToSystemProperties();
}
initializeEarlyLoggingLevel(environment);
initializeSystem(environment, this.loggingSystem, logFile);
initializeFinalLoggingLevels(environment, this.loggingSystem);
registerShutdownHookIfNecessary(environment, this.loggingSystem);
}
}
2.2 LoggingSystemProperties
其中每个方法都值得仔细分析,先分析一下apply方法,其源码如下:
public class LoggingSystemProperties {
public static final String PID_KEY = "PID";
public static final String EXCEPTION_CONVERSION_WORD =
"LOG_EXCEPTION_CONVERSION_WORD";
public static final String LOG_FILE = "LOG_FILE";
public static final String LOG_PATH = "LOG_PATH";
public static final String CONSOLE_LOG_PATTERN = "CONSOLE_LOG_PATTERN";
public static final String FILE_LOG_PATTERN = "FILE_LOG_PATTERN";
public static final String FILE_MAX_HISTORY = "LOG_FILE_MAX_HISTORY";
public static final String FILE_MAX_SIZE = "LOG_FILE_MAX_SIZE";
public static final String LOG_LEVEL_PATTERN = "LOG_LEVEL_PATTERN";
public static final String LOG_DATEFORMAT_PATTERN =
"LOG_DATEFORMAT_PATTERN";
private final Environment environment;
public void apply() {
apply(null);
}
public void apply(LogFile logFile) {
PropertyResolver resolver = getPropertyResolver();
setSystemProperty(resolver, EXCEPTION_CONVERSION_WORD,
"exception-conversion-word");
setSystemProperty(PID_KEY, new ApplicationPid().toString());
setSystemProperty(resolver, CONSOLE_LOG_PATTERN, "pattern.console");
setSystemProperty(resolver, FILE_LOG_PATTERN, "pattern.file");
setSystemProperty(resolver, FILE_MAX_HISTORY, "file.max-history");
setSystemProperty(resolver, FILE_MAX_SIZE, "file.max-size");
setSystemProperty(resolver, LOG_LEVEL_PATTERN, "pattern.level");
setSystemProperty(resolver, LOG_DATEFORMAT_PATTERN,
"pattern.dateformat");
if (logFile != null) {
logFile.applyToSystemProperties();
}
}
}
在LoggingSystemProperties类里面,在这里完成从环境属性中读取pattern.console和file.max-size等配置,并添加进系统属性中,以便后续使用。
2.3 LogFile
再看到LogFile类方法中,其相关源码如下:
public class LogFile {
public static final String FILE_PROPERTY = "logging.file";
public static final String PATH_PROPERTY = "logging.path";
private final String file;
private final String path;
public static LogFile get(PropertyResolver propertyResolver) {
String file = propertyResolver.getProperty(FILE_PROPERTY);
String path = propertyResolver.getProperty(PATH_PROPERTY);
if (StringUtils.hasLength(file) || StringUtils.hasLength(path)) {
return new LogFile(file, path);
}
return null;
}
public void applyToSystemProperties() {
applyTo(System.getProperties());
}
public void applyTo(Properties properties) {
put(properties, LoggingSystemProperties.LOG_PATH, this.path);
put(properties, LoggingSystemProperties.LOG_FILE, toString());
}
}
其大致作用也可以看到,涉及到的配置就两个:logging.file和logging.path,其作用便是从环境中获得logging.file和logging.path配置,如果成功就将这两个配置再放进系统属性中。
2.4 获取早期日志级别以及初始化配置文件
其相关方法源码如下:
public class LoggingApplicationListener
implements GenericApplicationListener {
private void initializeEarlyLoggingLevel(
ConfigurableEnvironment environment) {
if (this.parseArgs && this.springBootLogging == null) {
if (isSet(environment, "debug")) {
this.springBootLogging = LogLevel.DEBUG;
}
if (isSet(environment, "trace")) {
this.springBootLogging = LogLevel.TRACE;
}
}
}
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) {
System.err.println("...");
ex.printStackTrace(System.err);
throw new IllegalStateException(ex);
}
}
}
}
Springboot有DEBUG以及TRACE两个开关,值为boolean类型,initializeEarlyLoggingLevel方法则是用来获取那两个配置属性的,并赋值给springBootLogging,这个对象值后续会用到。
再看到initializeSystem方法,这个方法的作用便是从环境属性中获取logging.config配置项配置的日志配置文件路径,获得之后再调用LoggingSystem的实现子类(这里是LogbackLoggingSystem)去完成读取配置文件和再初始化。
2.5 initializeFinalLoggingLevels方法
其方法源码如下:
public class LoggingApplicationListener
implements GenericApplicationListener {
private void initializeFinalLoggingLevels(
ConfigurableEnvironment environment, LoggingSystem system) {
if (this.springBootLogging != null) {
initializeLogLevel(system, this.springBootLogging);
}
setLogLevels(system, environment);
}
protected void initializeLogLevel(LoggingSystem system,
LogLevel level) {
LOG_LEVEL_LOGGERS.getOrDefault(level, Collections.emptyList())
.forEach((logger) -> initializeLogLevel(system, level,
logger));
}
private void initializeLogLevel(LoggingSystem system, LogLevel level,
String logger) {
List<String> groupLoggers = DEFAULT_GROUP_LOGGERS.get(logger);
if (groupLoggers == null) {
system.setLogLevel(logger, level);
return;
}
groupLoggers.forEach((groupLogger) ->
system.setLogLevel(groupLogger, level));
}
protected void setLogLevels(LoggingSystem system,
Environment environment) {
if (!(environment instanceof ConfigurableEnvironment)) {
return;
}
Binder binder = Binder.get(environment);
Map<String, String[]> groups = getGroups();
binder.bind(LOGGING_GROUP, STRING_STRINGS_MAP
.withExistingValue(groups));
Map<String, String> levels = binder.bind(LOGGING_LEVEL,
STRING_STRING_MAP).orElseGet(Collections::emptyMap);
levels.forEach((name, level) -> {
String[] groupedNames = groups.get(name);
if (ObjectUtils.isEmpty(groupedNames)) {
setLogLevel(system, name, level);
}
else {
setLogLevel(system, groupedNames, level);
}
});
}
private void setLogLevel(LoggingSystem system, String name,
String level) {
try {
name = name.equalsIgnoreCase(LoggingSystem.ROOT_LOGGER_NAME) ?
null : name;
system.setLogLevel(name, coerceLogLevel(level));
}
catch (RuntimeException ex) {
this.logger.error("...");
}
}
}
这里同样也可以分成两个部分:
- 如果springBootLogging不为空:springBootLogging对象是在initializeEarlyLoggingLevel方法中根据DEBUG和TRACE配置判断的,DEBUG优先级大于TRACE。进入到这里面后将会用到两个静态块被初始化的map,一个是记录了DEBUG和TRACE两个配置分别对应的groupName,另一个则记录了groupName对应的具体loggerName。后面的流程基本上就是先使用配置的DEBUG和TRACE获得对应的groupName,再使用groupName获得对应的loggerName,最后遍历这些loggerName去设置日志级别;
- 必走流程:使用Binder获得yml或properties文件中logging.level配置的键值对,再遍历这些键值对依次调用setLogLevel方法,最终将配置文件中的键值对日志级别配置应用到Logback的logger树中。
Binder的实现在这里便不做过多分析,它起到的作用便是将logging:level:ROOT: INFO转变成key=logging.level.ROOT,value=INFO的映射关系。同时这两个部分最终都会调用到LoggingSystem对象的setLogLevel方法中,这个方法将在下面进行分析。
3.LogbackLoggingSystem
Logback对LoggingSystem的实现自然是LogbackLoggingSystem类,这个类里面有很多我们熟悉的老朋友。其部分关键源码如下:
public class LogbackLoggingSystem extends Slf4JLoggingSystem {
private static final String CONFIGURATION_FILE_PROPERTY =
"logback.configurationFile";
private static final LogLevels<Level> LEVELS = new LogLevels<>();
static {
LEVELS.map(LogLevel.TRACE, Level.TRACE);
LEVELS.map(LogLevel.TRACE, Level.ALL);
LEVELS.map(LogLevel.DEBUG, Level.DEBUG);
LEVELS.map(LogLevel.INFO, Level.INFO);
LEVELS.map(LogLevel.WARN, Level.WARN);
LEVELS.map(LogLevel.ERROR, Level.ERROR);
LEVELS.map(LogLevel.FATAL, Level.ERROR);
LEVELS.map(LogLevel.OFF, Level.OFF);
}
@Override
protected String[] getStandardConfigLocations() {
return new String[] { "logback-test.groovy", "logback-test.xml",
"logback.groovy", "logback.xml" };
}
}
对于Logback而言,其官方给出的日志级别只有七种,分别是TRACE、ALL、DEBUG、INFO、WARN、ERROR和OFF,但是Springboot的日志级别和Logback的级别有点差别,分别是TRACE、DEBUG、INFO、WARN、ERROR、FATAL、OFF,因此在静态代码块中将日志级别的对应关系映射缓存起来。同时getStandardConfigLocations方法的返回值也说明了Logback默认的日志配置文件名称路径,这四个文件将会在默认情况下读取。
3.1 beforeInitialize()流程
在进行实际的刷新前将会调用这个方法,完成初始化时一些前置条件的准备。其相关源码如下:
public class LogbackLoggingSystem extends Slf4JLoggingSystem {
@Override
public void beforeInitialize() {
LoggerContext loggerContext = getLoggerContext();
if (isAlreadyInitialized(loggerContext)) {
return;
}
super.beforeInitialize();
loggerContext.getTurboFilterList().add(FILTER);
}
private LoggerContext getLoggerContext() {
ILoggerFactory factory = StaticLoggerBinder.getSingleton()
.getLoggerFactory();
Assert.isInstanceOf(LoggerContext.class, factory,
String.format("...", factory.getClass(), getLocation(factory)));
return (LoggerContext) factory;
}
private boolean isAlreadyInitialized(LoggerContext loggerContext) {
return loggerContext.getObject(LoggingSystem.class.getName())
!= null;
}
}
public abstract class Slf4JLoggingSystem extends AbstractLoggingSystem {
@Override
public void beforeInitialize() {
super.beforeInitialize();
configureJdkLoggingBridgeHandler();
}
private void configureJdkLoggingBridgeHandler() {
try {
if (isBridgeJulIntoSlf4j()) {
removeJdkLoggingBridgeHandler();
SLF4JBridgeHandler.install();
}
}
catch (Throwable ex) {
// Ignore. No java.util.logging bridge is installed.
}
}
}
可以看到这个方法流程很简单,无非做了两件事:一是调用StaticLoggerBinder.getSingleton()来获得初始化Logback上下文;二是注册SLF4JBridgeHandler类。
3.2 initialize()流程
这个方法将是Springboot配置Logback属性的地方。LogbackLoggingSystem类的initialize方法只是初始化的入口,重点戏在其父类实现中。相关方法源码如下:
public class LogbackLoggingSystem extends Slf4JLoggingSystem {
private static final String CONFIGURATION_FILE_PROPERTY =
"logback.configurationFile";
@Override
public void initialize(
LoggingInitializationContext initializationContext,
String configLocation, LogFile logFile) {
LoggerContext loggerContext = getLoggerContext();
if (isAlreadyInitialized(loggerContext)) {
return;
}
super.initialize(initializationContext, configLocation, logFile);
loggerContext.getTurboFilterList().remove(FILTER);
markAsInitialized(loggerContext);
if (StringUtils.hasText(
System.getProperty(CONFIGURATION_FILE_PROPERTY))) {
getLogger(LogbackLoggingSystem.class.getName()).warn("...");
}
}
private void markAsInitialized(LoggerContext loggerContext) {
loggerContext.putObject(LoggingSystem.class.getName(),
new Object());
}
}
可以看到Logback的实现类在初始化中的方法很简单,就几个步骤,但是需要注意的是两点:一是isAlreadyInitialized和markAsInitialized方法搭配使用就能判断Springboot对Logback初始化过没有;二是成员属性CONFIGURATION_FILE_PROPERTY在Springboot中将不会被使用,也就是说logback.configurationFile配置对Springboot而言是不会读取这个配置的值,只会在系统日志中说明这个配置将会被忽略。
接下来看到对这个方法的父类实现。相关源码如下:
public abstract class AbstractLoggingSystem extends LoggingSystem {
@Override
public void initialize(
LoggingInitializationContext initializationContext,
String configLocation, LogFile logFile) {
if (StringUtils.hasLength(configLocation)) {
initializeWithSpecificConfig(initializationContext,
configLocation, logFile);
return;
}
initializeWithConventions(initializationContext, logFile);
}
}
在父类initialize中暂时可以分为两条路线,一条是未配置Springboot的指定配置文件路径,即方法入口是initializeWithConventions,二是logging.config配置了Springboot指定的日志配置文件读取流程,方法入口是initializeWithSpecificConfig。先说下第一条路线。相关源码如下:
public abstract class AbstractLoggingSystem extends LoggingSystem {
private void initializeWithConventions(
LoggingInitializationContext initializationContext,
LogFile logFile) {
String config = getSelfInitializationConfig();
if (config != null && logFile == null) {
reinitialize(initializationContext);
return;
}
if (config == null) {
config = getSpringInitializationConfig();
}
if (config != null) {
loadConfiguration(initializationContext, config, logFile);
return;
}
loadDefaults(initializationContext, logFile);
}
protected String getSelfInitializationConfig() {
return findConfig(getStandardConfigLocations());
}
protected String getSpringInitializationConfig() {
return findConfig(getSpringConfigLocations());
}
protected String[] getSpringConfigLocations() {
String[] locations = getStandardConfigLocations();
for (int i = 0; i < locations.length; i++) {
String extension =StringUtils.getFilenameExtension(locations[i]);
locations[i] = locations[i].substring(0,
locations[i].length() - extension.length() - 1) +
"-spring." + extension;
}
return locations;
}
}
路线流程如下:
- 先调用getSelfInitializationConfig方法读取getStandardConfigLocations方法的返回值,并赋值给config对象,不同日志实现这个方法的返回值不同,而Logback这个方法的值在前面已经说过,不赘述;
- 如果config对象不为空(logFile一定是为空),则调用reinitialize方法进行由config指向的配置文件再次进行初始化;
- 如果config对象为空,则调用getSpringInitializationConfig方法,这个方法将会在获得实现日志的默认配置文件名称后面加上-spring,如果是logback.xml,将会变成logback-spring.xml;
- 当getSpringInitializationConfig的返回值不为空时,将会调用loadConfiguration进行配置加载和初始化操作;
- 如果前面几个步骤config都为空,也就意味着Springboot并没有日志配置文件,这种情况下将会调用loadDefaults方法来加载默认的日志实现。
接下来看一下配置了logging.config配置的执行路径,相关源码如下:
public abstract class AbstractLoggingSystem extends LoggingSystem {
private void initializeWithSpecificConfig(
LoggingInitializationContext initializationContext,
String configLocation, LogFile logFile) {
configLocation = SystemPropertyUtils
.resolvePlaceholders(configLocation);
loadConfiguration(initializationContext, configLocation, logFile);
}
}
可以看到这个执行路径就很简单了,将configLocation进行转换后立马调用loadConfiguration方法来加载配置路径。
第一条路径可以看成只是在第二条路径前加上了获取日志配置文件的流程,当获得了配置文件后都会调用loadConfiguration方法。当然,如果第一条路径没有获取到配置文件,则会加载默认的日志配置。
3.3 setLogLevel()流程
先看下其涉及到的方法源码:
public class LogbackLoggingSystem extends Slf4JLoggingSystem {
@Override
public void setLogLevel(String loggerName, LogLevel level) {
ch.qos.logback.classic.Logger logger = getLogger(loggerName);
if (logger != null) {
logger.setLevel(LEVELS.convertSystemToNative(level));
}
}
private ch.qos.logback.classic.Logger getLogger(String name) {
LoggerContext factory = getLoggerContext();
if (StringUtils.isEmpty(name) || ROOT_LOGGER_NAME.equals(name)) {
name = Logger.ROOT_LOGGER_NAME;
}
return factory.getLogger(name);
}
}
public final class Logger
implements org.slf4j.Logger, LocationAwareLogger,
AppenderAttachable<ILoggingEvent>, Serializable {
public synchronized void setLevel(Level newLevel) {
// 如果和原来的级别相当,则直接返回
if (level == newLevel) {
return;
}
// 如果是root节点并且新的日志级别为空则抛出异常
// ROOT节点是基本的节点,因此level一定不能为空
if (newLevel == null && isRootLogger()) {
throw new IllegalArgumentException("...");
}
level = newLevel;
// 如果新的级别为空则集成父类的级别,不为空则使用新的级别
if (newLevel == null) {
effectiveLevelInt = parent.effectiveLevelInt;
newLevel = parent.getEffectiveLevel();
} else {
effectiveLevelInt = newLevel.levelInt;
}
// 没有子节点则跳过
if (childrenList != null) {
int len = childrenList.size();
for (int i = 0; i < len; i++) {
Logger child = (Logger) childrenList.get(i);
// 这个方法是一个递归方法,将会递归该节点的所有子节点
// 然后再一个个的设置新的日志级别
child.handleParentLevelChange(effectiveLevelInt);
}
}
// 级别变动监听通知
loggerContext.fireOnLevelChange(this, newLevel);
}
}
方法调用流程十分简单,先试用LoggerContext获得具体的logger,再调用logger的setLogLevel方法,这个方法无非是从当前节点开始,把所有遍历到的节点设置新的日志级别。具体解释在代码中。
4.LogbackLoggingSystem加载配置流程
又回到了这个类中,当调用完父类的通用流程后将会调用具体实现子类的方法中来,以达到不同日志实现框架可以最大限度的实现自己特有的功能。相关源码如下:
public class LogbackLoggingSystem extends Slf4JLoggingSystem {
@Override
protected void loadDefaults(
LoggingInitializationContext initializationContext,
LogFile logFile) {
LoggerContext context = getLoggerContext();
stopAndReset(context);
boolean debug = Boolean.getBoolean("logback.debug");
if (debug) {
StatusListenerConfigHelper.addOnConsoleListenerInstance(context,
new OnConsoleStatusListener());
}
LogbackConfigurator configurator = debug ?
new DebugLogbackConfigurator(context)
: new LogbackConfigurator(context);
Environment environment = initializationContext.getEnvironment();
context.putProperty(LoggingSystemProperties.LOG_LEVEL_PATTERN,
environment.resolvePlaceholders(
"${logging.pattern.level:${LOG_LEVEL_PATTERN:%5p}}"));
context.putProperty(LoggingSystemProperties.LOG_DATEFORMAT_PATTERN,
environment.resolvePlaceholders(
"${logging.pattern.dateformat:" +
"${LOG_DATEFORMAT_PATTERN:yyyy-MM-dd HH:mm:ss.SSS}}"));
new DefaultLogbackConfiguration(initializationContext, logFile)
.apply(configurator);
context.setPackagingDataEnabled(true);
}
}
首先看到loadDefaults方法在没有日志配置文件的情况下进行的操作。可以看到这里面一共做了两件事,一是读取环境中的LOG_LEVEL_PATTERN和LOG_DATEFORMAT_PATTERN连个默认配置属性,二是调用DefaultLogbackConfiguration的apply方法完成对ROOT节点的consoleAppender的注册,以及INFO级别日志的配置。
接下来看到读取配置文件流程的入口方法loadConfiguration,相关源码如下:
public class LogbackLoggingSystem extends Slf4JLoggingSystem {
@Override
protected void loadConfiguration(
LoggingInitializationContext initializationContext,
String location, LogFile logFile) {
super.loadConfiguration(initializationContext, location, logFile);
LoggerContext loggerContext = getLoggerContext();
stopAndReset(loggerContext);
try {
configureByResourceUrl(initializationContext, loggerContext,
ResourceUtils.getURL(location));
}
catch (Exception ex) {
throw new IllegalStateException("..." + location, ex);
}
List<Status> statuses = loggerContext.getStatusManager()
.getCopyOfStatusList();
StringBuilder errors = new StringBuilder();
for (Status status : statuses) {
if (status.getLevel() == Status.ERROR) {
errors.append("...");
errors.append(status.toString());
}
}
if (errors.length() > 0) {
throw new IllegalStateException(String.format("...", errors));
}
}
private void stopAndReset(LoggerContext loggerContext) {
loggerContext.stop();
loggerContext.reset();
if (isBridgeHandlerInstalled()) {
addLevelChangePropagator(loggerContext);
}
}
private void configureByResourceUrl(
LoggingInitializationContext initializationContext,
LoggerContext loggerContext, URL url) throws JoranException {
if (url.toString().endsWith("xml")) {
JoranConfigurator configurator =
new SpringBootJoranConfigurator(initializationContext);
configurator.setContext(loggerContext);
configurator.doConfigure(url);
}
else {
new ContextInitializer(loggerContext).configureByResource(url);
}
}
}
public abstract class Slf4JLoggingSystem extends AbstractLoggingSystem {
@Override
protected void loadConfiguration(
LoggingInitializationContext initializationContext,
String location, LogFile logFile) {
Assert.notNull(location, "Location must not be null");
if (initializationContext != null) {
applySystemProperties(initializationContext.getEnvironment(),
logFile);
}
}
}
可以看到这个流程相关的方法比较多,但只列出了三条比较明显的分支:
- 第一条是调用父类的loadConfiguration方法,这个方法最终会调用到apply方法中,这个方法在前面已经分析过了,这里便不再分析;
- 调用stopAndReset方法,将LoggerContext中的记录清空,但是Logger树以及缓存不清空;
- 调用configureByResourceUrl方法,对不同后缀XML和Groovy进行处理,最后都是调用Configurator进行处理,这边便不做过多分析,第一篇已经分析过了。
需要注意的便是SpringBootJoranConfigurator,这个是Springboot自己实现的Configurator,其源码如下:
class SpringBootJoranConfigurator extends JoranConfigurator {
@Override
public void addInstanceRules(RuleStore rs) {
super.addInstanceRules(rs);
Environment environment =
this.initializationContext.getEnvironment();
rs.addRule(new ElementSelector("configuration/springProperty"),
new SpringPropertyAction(environment));
rs.addRule(new ElementSelector("*/springProfile"),
new SpringProfileAction(environment));
rs.addRule(new ElementSelector("*/springProfile/*"),
new NOPAction());
}
}
可以看到这个实现类在RuleStore中添加了springProfile标签处理方式,这是Springboot为了实现不同环境使用不同文件的特定标签,Springboot的spring.profile这里便不做过多的分析。这里引申出另外一个问题,那便是logback.xml文件没有dtd等文件格式限制,因为其实在太过灵活,实现一个子类Configurator添加不同的规则再实现对应的Action就可以对配置文件标签进行处理,因此不便于使用dtd文件来限制。
至此,Springboot和Logback的集成便分析完成,看完了Springboot对于Logback的集成初始化,再结合Logback的自启动过程,仔细一对比就能知道到底应该怎样配置才能够在日志这一方面启动消耗的时间减到最小。虽然Springboot将Logback的配置弄的十分简单化,但从性能上来说还是稍微有点浪费,毕竟Logback已经自启动过一次了,而如果Springboot和logback的默认配置一致的话,会导致读取分析两次配置文件,配置文件大的话无疑是有消耗的。