spring日志系统
spring
通过门面模式来应对项目中依赖不同的组件时,各组件使用不同的日志框架,有log4j
、log4j2
、lockback
等,门面有sl4j
和commons-logging
。
在开发的过程中,经常需要使用到日志框架来打印一些信息,来帮助开发者定位产生的相关问题等,在使用时只需要通过log.info()
等方式就可以打印出一条日志信息,但是这条日志是如何打印出来,理清日志框架的工作过程及原理,能有效帮助开发者扩展日志功能以及优雅的进行日志打印等级切换等等。
springboot日志框架
springboot
的默认日志框架是logback
。具官网介绍logback
的性能高于log4j
,而log4j2
是在logback
之后诞生的,性能高于logback
。springboot
中默认引入的是logback
日志框架,如果需要在springboot
项目中使用sl4j2
作为日志框架,需要将原生框架中的logback
依赖排除。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </exclusion> </exclusions> </dependency>
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-log4j2</artifactId> </dependency>
|
然后就可以在resource
下创建sl4j2.xml
配置文件,配置一些打印相关操作,比如指定多个打印器,指定打印路径,指定打印格式、打印等级等等。当然,也可以不创建这个配置文件,在springboot
有兜底配置。
接着一步步来了解一下,以及springboot
是如果设计,达到可以自由切换日志框架的目的。以及sl4j2
的一些工作原理。
如果项目中结合了lombok
可以发现,在实体上标记了@Slf4j
之后,就可以通过log
对象来打印日志,而这个打印日志的log
对象就是org.slf4j.Logger
类对象,它仅仅是定义了一系列的日志打印接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public interface Logger {
public void info(String msg);
public void info(String format, Object arg);
public void info(String format, Object... arguments);
public void error(String format, Object... arguments); public void error(String msg, Throwable t); }
|
如果工程是使用sl4j2
,那么此时就会进入到org.apache.logging.slf4j.Log4jLogger
,它是org.slf4j.Logger
门面接口的实现,它是在依赖org.apache.logging.log4j:log4j-slf4j-impl
中。
如果工程是使用logback
,那么此时就会进入到ch.qos.logback.classic.Logger
,它也是org.slf4j.Logger
门面对象的实现,他是在依赖ch.qos.logback:logback-classis
中,这里要注意,loback
对门面接口类的实现的类的名称也是叫Logger
。
log4j2的简单工作原理
从上述可以知道,其实打印日志都是获取Logger
对象,通过它来进行日志打印操作。在log4j2
中的Logger
对象有两个属性PrivateConfig
和LoggerContext
。
PrivateConfig
中包含了通过log4j2.xml
文件解析出来的appender
和具体的logger
。
LoggerContext
是根据配置规则,缓存了每个类打印时需要使用到的logger
和appender
的关系。
在配置文件中的<Appenders>标签下每增加一项,解析得到的Configuration
中的Appenders
就多一个Appender
。在<Loggers>标签下每增加一项,解析得到的Configuration
中的loggerConfigs
就多一项。
在LoggerConfig
中持有所有Appender
的引用,而解析后的Configuration
中的loggerConfigs
表示的是某一个logger
持有的Appender
引用。
如果需要使用到打印,都会先到LoggerContext
的注册器LoggerRegistry
中获取,如果没有,则会创建出一个Logger
并注册到LoggerRegistry
中。
比方说我这个工程中的这个类cn.com.xiaocainiaoya.common.CustomizeConfiguration
,如果需要打印就会从这个注册器中获取到这个Logger
,然后通过这个Logger
的日志等级配置以及Appender
等等进行打印操作。
Springboot日志启动机制
Springboot
的日志启动依赖于springboot
的事件监听机制,在spring
的启动过程中,需要进行一系列的准备、加载等动作,所以Spring
在对容器最终加载完成的整个生命周期中,设置了很多监听事件,比如刚刚启动容器、Enviroment
准备完成、ApplicatonContext
准备完成等等。日志启动是依赖于LoggingApplicationListener
类,它实现了ApplicationListener
的封装类GenericApplicationListener
,所以它能接收到多种消息事件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| @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(); } }
|
1.监听到ApplicationStartingEvent
事件
在Springboot
中操作日志最关键的一个对象就是LoggingSystem
,这个对象在整个生命周期中掌握着日志。
1 2 3 4
| private void onApplicationStartingEvent(ApplicationStartingEvent event) { this.loggingSystem = LoggingSystem.get(event.getSpringApplication().getClassLoader()); this.loggingSystem.beforeInitialize(); }
|
这里在接收到这个事件通知之后,就到系统属性中去读取LoggingSystem
具体的实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| static { Map<String, String> 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); }
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")); }
|
拿到LogginSystem
之后,就会调用它的beforeInitialize()
,这个方法的主要作用就是进行一些事前初始化动作,这里添加了一个日志过滤器,但实际上是一个阻断器,这个阻断器拒绝了所有的日志打印。所以到达这一步,仍然是无法打印出日志的,就算日志框架接收到需要打印日志的请求,也会被阻断器拒绝。这样的设计是为了防止日志系统在完成初始化之前打印出不可控的日志信息。
1 2 3 4 5 6 7 8 9
| @Override public void beforeInitialize() { LoggerContext loggerContext = getLoggerContext(); if (isAlreadyInitialized(loggerContext)) { return; } super.beforeInitialize(); loggerContext.getConfiguration().addFilter(FILTER); }
|
2.监听到ApplicationEnvironmentPreparedEvent
事件
当Environment
准备完成之后,继续进行日志框架的初始化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) { if (this.loggingSystem == null) { this.loggingSystem = LoggingSystem.get(event.getSpringApplication().getClassLoader()); } initialize(event.getEnvironment(), event.getSpringApplication().getClassLoader()); }
protected void initialize(ConfigurableEnvironment environment, ClassLoader classLoader) { new LoggingSystemProperties(environment).apply(); this.logFile = LogFile.get(environment); if (this.logFile != null) { this.logFile.applyToSystemProperties(); } this.loggerGroups = new LoggerGroups(DEFAULT_GROUP_LOGGERS); initializeEarlyLoggingLevel(environment); initializeSystem(environment, this.loggingSystem, this.logFile); initializeFinalLoggingLevels(environment, this.loggingSystem); registerShutdownHookIfNecessary(environment, this.loggingSystem); }
|
重点来看下LoggingSystem
的初始化方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| 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{ } }
@Override public void initialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) { LoggerContext loggerContext = getLoggerContext(); if (isAlreadyInitialized(loggerContext)) { return; } loggerContext.getConfiguration().removeFilter(FILTER); super.initialize(initializationContext, configLocation, logFile); markAsInitialized(loggerContext); }
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); }
|
之后就会将日志打印器logger
进行分组。这样做的好处是便于管理,分组之后,如果需要对某个组改日志级别,只需要将这个组的日志级别进行修改,就不需要逐个去找打印器并进行修改。
1 2 3 4 5 6 7 8 9 10
| logging: level: common: debug user: info group: common: - cn.com.xiaocainiaoya.common user: - com.lee.xiaocainiaoya.user
|
如果设置了trace=true
或者是bebud=true
,那么springboot
会将自己的某些类的打印开启。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| private void initializeFinalLoggingLevels(ConfigurableEnvironment environment, LoggingSystem system) { bindLoggerGroups(environment); if (this.springBootLogging != null) { initializeLogLevel(system, this.springBootLogging); } setLogLevels(system, environment); }
protected void initializeLogLevel(LoggingSystem system, LogLevel springBootLogging) { initializeSpringBootLogging(system, springBootLogging); }
protected void initializeSpringBootLogging(LoggingSystem system, LogLevel springBootLogging) { BiConsumer<String, LogLevel> configurer = getLogLevelConfigurer(system); SPRING_BOOT_LOGGING_LOGGERS.getOrDefault(springBootLogging, Collections.emptyList()) .forEach((name) -> configureLogLevel(name, springBootLogging, configurer)); }
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"); SPRING_BOOT_LOGGING_LOGGERS = Collections.unmodifiableMap(loggers); }
private void configureLogLevel(String name, LogLevel level, BiConsumer<String, LogLevel> configurer) { if (this.loggerGroups != null) { LoggerGroup group = this.loggerGroups.get(name); if (group != null && group.hasMembers()) { group.configureLogLevel(level, configurer); return; } } configurer.accept(name, level); }
|
3.监听到ApplicationPreparedEvent
事件
日志级别热更新
其实对应的日志框架都已经提供了对应的日志级别更新方法,只需要获取到LoggingSystem
类,并调用setLogLevel
方法即可。
1
| loggingSystem.setLogLevel("cn.com.xiaocainiaoya.BootStrap", LogLevel.DEBUG);
|
这里简单分析一下过程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| @Override public void setLogLevel(String loggerName, LogLevel logLevel) { Level level = LEVELS.convertSystemToNative(logLevel); LoggerConfig loggerConfig = getLoggerConfig(loggerName); if (loggerConfig == null) { loggerConfig = new LoggerConfig(loggerName, level, true); getLoggerContext().getConfiguration().addLogger(loggerName, loggerConfig); } else { loggerConfig.setLevel(level); } getLoggerContext().updateLoggers(); }
public void updateLoggers(final Configuration config) { final Configuration old = this.configuration; for (final Logger logger : loggerRegistry.getLoggers()) { logger.updateConfiguration(config); } firePropertyChangeEvent(new PropertyChangeEvent(this, PROPERTY_CONFIG, old, config)); }
protected void updateConfiguration(final Configuration newConfig) { this.privateConfig = new PrivateConfig(newConfig, this); }
|
其实我刚看到这个地方时候,我是有点困惑的,在我标记⭐️的地方。在更新完loggerConfig
的的日志级别之后,尽然需要将所有的loggerRegistry
都取出来进行重新创建PrivateConfig
。难道不是loggerConfig
的日志级别修改了之后持有这个对象的引用获取到信息也是更新之后的值。但是其实这里是有可能获取不到loggerConfig
,那么就需要创建一个新的loggerConfig
到Configuration
池子里。根据日志名称匹配规则,新增了一个loggerConfig
那么其实对缓存中的所有信息有所影响,比如某个类的全限定类名是cn.com.xiaocainiao.FeignUtil
,原本它是使用名称为root
的打印器,但是我调用setLogLevel("cn.com.xiaocainiao", "DEBUG")
那么这时会创建一个新的打印器器,也就导致了cn.com.xiaocainiao.FeignUtil
应该使用新创建的这个打印器,而不是根打印器,所以这里的将所有的logger
进行了更新。