在平时的项目中,会经常会遇到对项目日志的配置问题,比如log日志的存放位置、级别、单个 log 文件的大小,过期清理策略等等。
那么,这个是怎么配置的呢?配置文件放在哪里?springboot是怎么加载配置文件的?
这篇文章会从源码出发对这些问题进行一一探讨。
LoggingApplicationListener
这个监听器类在监听到环境中准备好事件发生后,会做出响应,对日志系统进行配置。
还是从监听入口出发:
1@Override
2public void onApplicationEvent(ApplicationEvent event) {
3 if (event instanceof ApplicationStartedEvent) {
4 onApplicationStartedEvent((ApplicationStartedEvent) event);
5 }
6 //监听到事件
7 else if (event instanceof ApplicationEnvironmentPreparedEvent) {
8 onApplicationEnvironmentPreparedEvent(
9 (ApplicationEnvironmentPreparedEvent) event);
10 }
11 else if (event instanceof ContextClosedEvent && ((ContextClosedEvent) event)
12 .getApplicationContext().getParent() == null) {
13 onContextClosedEvent();
14 }
15}
复制代码
第7行判断了是应用环境准备好事件发生,对此做出响应。
1private void onApplicationEnvironmentPreparedEvent(
2 ApplicationEnvironmentPreparedEvent event) {
3 //生成日志系统
4 if (this.loggingSystem == null) {
5 this.loggingSystem = LoggingSystem
6 .get(event.getSpringApplication().getClassLoader());
7 }
8 //初始化
9 initialize(event.getEnvironment(), event.getSpringApplication().getClassLoader());
10}
复制代码
LoggingSystem 是Springboot中对于日志的统一抽象,对外提供了日志相关操作,封装了底层日志的实现细节。
取到 LoggingSystem
我们看下get方法。
1public static LoggingSystem get(ClassLoader classLoader) {
2 /*SYSTEM_PROPERTY = LoggingSystem.class.getName()*/
3 //从系统变量中找到 loggingSystem 的类名,初始化
4 String loggingSystem = System.getProperty(SYSTEM_PROPERTY);
5 if (StringUtils.hasLength(loggingSystem)) {
6 return get(classLoader, loggingSystem);
7 }
8 //如果系统变量中没有,则从默认的日志系统中找一个存在的。
9 for (Map.Entry<String, String> entry : SYSTEMS.entrySet()) {
10 if (ClassUtils.isPresent(entry.getKey(), classLoader)) {
11 return get(classLoader, entry.getValue());
12 }
13 }
14 throw new IllegalStateException("No suitable logging system located");
15}
复制代码
可以看到代码总共分两块:
- 如果系统变量中配置了loggingSystem的类,则找到利用反射初始化
- 反之,则从类路径中存在的日志系统类,加载。
1private static final Map<String, String> SYSTEMS;
2
3static {
4 Map<String, String> systems = new LinkedHashMap<String, String>();
5 systems.put("ch.qos.logback.core.Appender",
6 "org.springframework.boot.logging.logback.LogbackLoggingSystem");
7 systems.put("org.apache.logging.log4j.LogManager",
8 "org.springframework.boot.logging.log4j2.Log4J2LoggingSystem");
9 systems.put("org.apache.log4j.PropertyConfigurator",
10 "org.springframework.boot.logging.log4j.Log4JLoggingSystem");
11 systems.put("java.util.logging.LogManager",
12 "org.springframework.boot.logging.java.JavaLoggingSystem");
13 SYSTEMS = Collections.unmodifiableMap(systems);
14}
复制代码
可以看到,自带了LogbackLoggingSystem、Log4J2LoggingSystem、Log4JLoggingSystem、JavaLoggingSystem 四种日志系统。
初始化
上一步取到了 LoggingSystem,接下来就是要对其进行初始化。
1protected void initialize(ConfigurableEnvironment environment,
2 ClassLoader classLoader) {
3 //PID_KEY = "PID"
4 if (System.getProperty(PID_KEY) == null) {
5 System.setProperty(PID_KEY, new ApplicationPid().toString());
6 }
7 initializeEarlyLoggingLevel(environment);
8 initializeSystem(environment, this.loggingSystem);
9 initializeFinalLoggingLevels(environment, this.loggingSystem);
10}
复制代码
这里的工作主要分为四步:
- 获取进程号
代码 4~6 行为系统变量 PID_KEY 设置了进程号内容。我们可以简单看下其获取进程号的方式:
1private String getPid() {
2 try {
3 String jvmName = ManagementFactory.getRuntimeMXBean().getName();
4 return jvmName.split("@")[0];
5 }
6 catch (Throwable ex) {
7 return null;
8 }
9}
复制代码
ManagementFactory 是 jdk 包里面的一个工厂类。我试着自己打印了下这个 jvmName 是个什么东西:
3208@DFN0S9W18H6NNAS
可以看到“@”符号前面的正是进程号。
- 对日志级别进行早期初始化
1if (this.parseArgs && this.springBootLogging == null) {
2 if (environment.containsProperty("debug")) {
3 this.springBootLogging = LogLevel.DEBUG;
4 }
5 if (environment.containsProperty("trace")) {
6 this.springBootLogging = LogLevel.TRACE;
7 }
8}
复制代码
如果要设置了要解析命令行参数且没有指定日志级别,则从环境中找 debug 或者 trace 的属性,如果找得到,则设置成相应的级别。
- 开始实际初始化日志系统
1private void initializeSystem(ConfigurableEnvironment environment,
2 LoggingSystem system) {
3 LogFile logFile = LogFile.get(environment);
4 //CONFIG_PROPERTY = "logging.config"
5 String logConfig = environment.getProperty(CONFIG_PROPERTY);
6 if (StringUtils.hasLength(logConfig)) {
7 try {
8 ResourceUtils.getURL(logConfig).openStream().close();
9 system.initialize(logConfig, logFile);
10 }
11 catch (Exception ex) {
12 this.logger.warn("Logging environment value '" + logConfig
13 + "' cannot be opened and will be ignored "
14 + "(using default location instead)");
15 system.initialize(null, logFile);
16 }
17 }
18 else {
19 system.initialize(null, logFile);
20 }
21}
复制代码
LogFile 是一个对log日志文件的引用。
在第3行,初始化一个 logFile 对象。我们可以看下get方法:
1public static LogFile get(PropertyResolver propertyResolver) {
2 //FILE_PROPERTY = "logging.file"
3 String file = propertyResolver.getProperty(FILE_PROPERTY);
4 //PATH_PROPERTY = "logging.path"
5 String path = propertyResolver.getProperty(PATH_PROPERTY);
6 if (StringUtils.hasLength(file) || StringUtils.hasLength(path)) {
7 return new LogFile(file, path);
8 }
9 return null;
10}
复制代码
如果用户配置了 logging.file 或者 logging.path 属性,则将会返回一个LogFile对象。
第5行,如果配置了logging.config属性,则利用该属性的值,找到对应的配置文件进行解析加载。
我们可以看下 system.initialize 方法。
此方法在一个抽象类中提供了模板。
1@Override
2public void initialize(String configLocation, LogFile logFile) {
3 if (StringUtils.hasLength(configLocation)) {
4 // Load a specific configuration
5 configLocation = SystemPropertyUtils.resolvePlaceholders(configLocation);
6 loadConfiguration(configLocation, logFile);
7 }
8 else {
9 String selfInitializationConfig = getSelfInitializationConfig();
10 if (selfInitializationConfig == null) {
11 // No self initialization has occurred, use defaults
12 loadDefaults(logFile);
13 }
14 else if (logFile != null) {
15 // Self initialization has occurred but the file has changed, reload
16 loadConfiguration(selfInitializationConfig, logFile);
17 }
18 else {
19 reinitialize();
20 }
21 }
22}
复制代码
如果用户提供了配置,则加载配置路径中的日志文件配置;反之,则加载默认的配置。
先看加载用户自定义配置的逻辑。
- 加载用户自定义配置
对于 loadConfiguration 方法不同的实现类有不同的加载方式,我们试着看下 logback 的加载方法:
1protected void loadConfiguration(String location, LogFile logFile) {
2 Assert.notNull(location, "Location must not be null");
3 if (logFile != null) {
4 logFile.applyToSystemProperties();
5 }
6 LoggerContext context = getLoggerContext();
7 stopAndReset(context);
8 try {
9 URL url = ResourceUtils.getURL(location);
10 new ContextInitializer(context).configureByResource(url);
11 }
12 catch (Exception ex) {
13 throw new IllegalStateException(
14 "Could not initialize Logback logging from " + location, ex);
15 }
16}
复制代码
LoggerContext
重点看下第6行,获取一个 LoggerContext 的上下文环境对象,该类位于 ch.qos.logback.classic 包,不在 spring工程里。
1private LoggerContext getLoggerContext() {
2 ILoggerFactory factory = StaticLoggerBinder.getSingleton().getLoggerFactory();
3 Assert.isInstanceOf(LoggerContext.class, factory,
4 String.format(
5 "LoggerFactory is not a Logback LoggerContext but Logback is on "
6 + "the classpath. Either remove Logback or the competing "
7 + "implementation (%s loaded from %s). If you are using "
8 + "Weblogic you will need to add 'org.slf4j' to "
9 + "prefer-application-packages in WEB-INF/weblogic.xml",
10 factory.getClass(), getLocation(factory)));
11 return (LoggerContext) factory;
12}
复制代码
在代码的第2行,获取到了一个 LoggerContext 对象。
第3行判断LoggerContext是否对ILoggerFactory进行了实现,如果不是,则报错。
LoggerFactory is not a Logback LoggerContext but Logback is on the classpath.
logback 在类路径下,但是加载出来的 LoggerContext 却不是 logback 的实现。
在错误提示的第 10 行中给出了加载的 LoggerContext 的类路径。
config
在获取到了 LoggerContext 对象之后,通过下面两行的核心代码对日志系统进行配置。
URL url = ResourceUtils.getURL(location); new ContextInitializer(context).configureByResource(url);
- 加载默认日志配置
1protected String getSelfInitializationConfig() {
2 for (String location : getStandardConfigLocations()) {
3 ClassPathResource resource = new ClassPathResource(location,
4 this.classLoader);
5 if (resource.exists()) {
6 return "classpath:" + location;
7 }
8 }
9 return null;
10}
复制代码
加载默认配置的方式,首先找到默认的日志 location。
getStandardConfigLocations 也是一个模板方法,不同日志系统有不同的位置,还是以 logback 为例:
1protected String[] getStandardConfigLocations() {
2 return new String[] { "logback-test.groovy", "logback-test.xml", "logback.groovy","logback.xml" };
3}
复制代码
可以看到,默认的日志配置文件名总共有这四种。
如果没有找到这四种配置文件的任意一种,则执行下面的逻辑:
1public void apply(LogbackConfigurator config) {
2 synchronized (config.getConfigurationLock()) {
3 base(config);
4 Appender<ILoggingEvent> consoleAppender = consoleAppender(config);
5 if (this.logFile != null) {
6 Appender<ILoggingEvent> fileAppender = fileAppender(config,
7 this.logFile.toString());
8 config.root(Level.INFO, consoleAppender, fileAppender);
9 }
10 else {
11 config.root(Level.INFO, consoleAppender);
12 }
13 }
14}
复制代码
可以看到默认的日志级别是 info,如果 logFile 为 null 的话,默认打印在控制台上。
第3行的base方法给出了 spring 工程包里面的默认配置的日志级别:
1private void base(LogbackConfigurator config) {
2 config.conversionRule("clr", ColorConverter.class);
3 config.conversionRule("wex", WhitespaceThrowableProxyConverter.class);
4 LevelRemappingAppender debugRemapAppender = new LevelRemappingAppender(
5 "org.springframework.boot");
6 config.start(debugRemapAppender);
7 config.appender("DEBUG_LEVEL_REMAPPER", debugRemapAppender);
8 config.logger("", Level.ERROR);
9 config.logger("org.apache.catalina.startup.DigesterFactory", Level.ERROR);
10 config.logger("org.apache.catalina.util.LifecycleBase", Level.ERROR);
11 config.logger("org.apache.coyote.http11.Http11NioProtocol", Level.WARN);
12 config.logger("org.apache.sshd.common.util.SecurityUtils", Level.WARN);
13 config.logger("org.apache.tomcat.util.net.NioSelectorPool", Level.WARN);
14 config.logger("org.crsh.plugin", Level.WARN);
15 config.logger("org.crsh.ssh", Level.WARN);
16 config.logger("org.eclipse.jetty.util.component.AbstractLifeCycle", Level.ERROR);
17 config.logger("org.hibernate.validator.internal.util.Version", Level.WARN);
18 config.logger("org.springframework.boot.actuate.autoconfigure."
19 + "CrshAutoConfiguration", Level.WARN);
20 config.logger("org.springframework.boot.actuate.endpoint.jmx", null, false,
21 debugRemapAppender);
22 config.logger("org.thymeleaf", null, false, debugRemapAppender);
23}
复制代码
- 对日志级别进行最终初始化
1private void initializeFinalLoggingLevels(ConfigurableEnvironment environment,
2 LoggingSystem system) {
3 if (this.springBootLogging != null) {
4 initializeLogLevel(system, this.springBootLogging);
5 }
6 setLogLevels(system, environment);
7}
复制代码
这里执行一些最终的 log级别的设置。
总结
本文对于 log 配置的主干逻辑进行了一些分析。这里的有些细节非常值得琢磨。后面可以专门针对这些细节在研究下。