在平時的項目中,會常常會遇到對項目日誌的配置問題,好比log日誌的存放位置、級別、單個 log 文件的大小,過時清理策略等等。java
那麼,這個是怎麼配置的呢?配置文件放在哪裏?springboot是怎麼加載配置文件的?web
這篇文章會從源碼出發對這些問題進行一一探討。spring
這個監聽器類在監聽到環境中準備好事件發生後,會作出響應,對日誌系統進行配置。apache
仍是從監聽入口出發:tomcat
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行判斷了是應用環境準備好事件發生,對此作出響應。springboot
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中對於日誌的統一抽象,對外提供了日誌相關操做,封裝了底層日誌的實現細節。app
咱們看下get方法。eclipse
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}
複製代碼
能夠看到代碼總共分兩塊:ssh
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 四種日誌系統。jvm
上一步取到了 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 配置的主幹邏輯進行了一些分析。這裏的有些細節很是值得琢磨。後面能夠專門針對這些細節在研究下。