SpringBoot源码中使用的日志API为JCL(Java common log),JCL实际上只是一个日志门面,没有具体的日志功能实现。如果将SpringBoot中的spring-boot-start-logging依赖排除,可以看到启动项目时打印的是红色字体的JUL日志。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
如果不刻意的排除spring-boot-starter-logging,SpringBoot默认引入的logback日志,SpringBoot在一站式启动过程中,会发布各种通知事件,如项目正在启动事件,准备环境事件等,logback的加载就是通过事件监听者来响应上述事件从而触发的,
触发加载的原理与SpringBoot配置文件触发加载的过程是同理的,SpringBoot引入了一些新的监听器,从spring.factories文件中就能看到具体的监听器:
# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.ClearCachesApplicationListener,\
org.springframework.boot.builder.ParentContextCloserApplicationListener,\
org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor,\
org.springframework.boot.context.FileEncodingApplicationListener,\
org.springframework.boot.context.config.AnsiOutputApplicationListener,\
org.springframework.boot.context.config.ConfigFileApplicationListener,\
org.springframework.boot.context.config.DelegatingApplicationListener,\
org.springframework.boot.context.logging.ClasspathLoggingApplicationListener,\
org.springframework.boot.context.logging.LoggingApplicationListener,\
org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener` </pre>
其中ConfigFileApplicationListener就是加载SpringBoot配置文件的监听器,LoggingApplicationListener就是加载日志的监听器。可以看到类中onApplicationEvent方法就是处理监听到的各种事件的:
@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) {
onContextClosedEvent((ContextClosedEvent) event);
}
else if (event instanceof ApplicationFailedEvent) {
onApplicationFailedEvent();
}
}
ApplicationStartingEvent
private void onApplicationStartingEvent(ApplicationStartingEvent event) {
this.loggingSystem = LoggingSystem.get(event.getSpringApplication().getClassLoader());
this.loggingSystem.beforeInitialize();
}
LoggingSystem.get方法就是记载具体日志的过程:
public static LoggingSystem get(ClassLoader classLoader) {
String loggingSystemClassName = System.getProperty(SYSTEM_PROPERTY);
if (StringUtils.hasLength(loggingSystemClassName)) {
if (NONE.equals(loggingSystemClassName)) {
return new NoOpLoggingSystem();
}
return get(classLoader, loggingSystemClassName);
}
LoggingSystem loggingSystem = SYSTEM_FACTORY.getLoggingSystem(classLoader);
Assert.state(loggingSystem != null, "No suitable logging system located");
return loggingSystem;
}
这里其实是遍历SYSTEMS里面的entry,判断key代表的class是否存在,如果存在就把value代表的那个LoggingSystem给加载了,看下SYSTEMS里面都有啥:
private static final LoggingSystemFactory SYSTEM_FACTORY = LoggingSystemFactory.fromSpringFactories();
在spring-boot-2.7.12.jar 里的spring.factories中可以看到
# Logging Systems
org.springframework.boot.logging.LoggingSystemFactory=\
org.springframework.boot.logging.logback.LogbackLoggingSystem.Factory,\
org.springframework.boot.logging.log4j2.Log4J2LoggingSystem.Factory,\
org.springframework.boot.logging.java.JavaLoggingSystem.Factory
这里默认添加了3个日志框架,依次是logback、log4j2和jdk的log,因为spring-boot-starter-logging默认依赖了logback,因此,logback会被初始化使用,再这里产生了实例LogbackLoggingSystem。
SYSTEM_FACTORY.getLoggingSystem(classLoader)
会依次寻找定义的日志框架Factory的类,找到第一个返回构造LoggingSystem 返回。
接下来进入到 LogbackLoggingSystem#beforeInitialize 方法
@Override
public void beforeInitialize() {
LoggerContext loggerContext = getLoggerContext();
if (isAlreadyInitialized(loggerContext)) {
return;
}
super.beforeInitialize();
loggerContext.getTurboFilterList().add(FILTER);
}
通过 StaticLoggerBinder.getSingleton() 创建 LoggerContext
private LoggerContext getLoggerContext() {
ILoggerFactory factory = StaticLoggerBinder.getSingleton().getLoggerFactory();
Assert.isInstanceOf(LoggerContext.class, factory,
String.format(
"LoggerFactory is not a Logback LoggerContext but Logback is on "
+ "the classpath. Either remove Logback or the competing "
+ "implementation (%s loaded from %s). If you are using "
+ "WebLogic you will need to add 'org.slf4j' to "
+ "prefer-application-packages in WEB-INF/weblogic.xml",
factory.getClass(), getLocation(factory)));
return (LoggerContext) factory;
}
进入到静态代码块的 init 方法
void init() {
try {
try {
new ContextInitializer(defaultLoggerContext).autoConfig();
} catch (JoranException je) {
Util.report("Failed to auto configure default logger context", je);
}
// logback-292
if (!StatusUtil.contextHasStatusListener(defaultLoggerContext)) {
StatusPrinter.printInCaseOfErrorsOrWarnings(defaultLoggerContext);
}
contextSelectorBinder.init(defaultLoggerContext, KEY);
initialized = true;
} catch (Exception t) { // see LOGBACK-1159
Util.report("Failed to instantiate [" + LoggerContext.class.getName() + "]", t);
}
}
最终会执行下面的代码、尝试获取默认配置文件、如果不存在则通过 SPI 获取 Configurator 实现类、如果还是没有、则使用默认的配置 BasicConfigurator
public void autoConfig() throws JoranException {
StatusListenerConfigHelper.installIfAsked(loggerContext);
URL url = findURLOfDefaultConfigurationFile(true);
if (url != null) {
configureByResource(url);
} else {
Configurator c = EnvUtil.loadFromServiceLoader(Configurator.class);
if (c != null) {
try {
c.setContext(loggerContext);
c.configure(loggerContext);
} catch (Exception e) {
throw new LogbackException(String.format("Failed to initialize Configurator: %s using ServiceLoader", c != null ? c.getClass()
.getCanonicalName() : "null"), e);
}
} else {
BasicConfigurator basicConfigurator = new BasicConfigurator();
basicConfigurator.setContext(loggerContext);
basicConfigurator.configure(loggerContext);
}
}
}
ApplicationEnvironmentPreparedEvent
开始通过LogBackLoggingSystem配置LogBack,如果配置了自定义的日志配置文件就加载自己的文件,否则加载默认的配置文件。
Logback整合完成后,SpringBoot中的JCL API打印的日志是如何转变成logback格式的呢,这里看下spring-boot-starter-logging pom中引入的依赖:
<dependencies>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-to-slf4j</artifactId>
<version>2.13.3</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jul-to-slf4j</artifactId>
<version>1.7.30</version>
<scope>compile</scope>
</dependency>
</dependencies>
这里实际上涉及到纷繁复杂的日志体系中的日志转换,大致转换链路过程就是JCL->JUL->slf4j->logback classic->logback以及log4j->slf4j->logback classic->logback
private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
SpringApplication springApplication = event.getSpringApplication();
if (this.loggingSystem == null) {
this.loggingSystem = LoggingSystem.get(springApplication.getClassLoader());
}
initialize(event.getEnvironment(), springApplication.getClassLoader());
}
protected void initialize(ConfigurableEnvironment environment, ClassLoader classLoader) {
getLoggingSystemProperties(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);
}
主要就是从 environment 对象中设置相关属性值到 System 中、然后判断是否设置了 log file
initializeSystem
private void initializeSystem(ConfigurableEnvironment environment, LoggingSystem system, LogFile logFile) {
String logConfig = StringUtils.trimWhitespace(environment.getProperty(CONFIG_PROPERTY));
try {
LoggingInitializationContext initializationContext = new LoggingInitializationContext(environment);
if (ignoreLogConfig(logConfig)) {
system.initialize(initializationContext, null, logFile);
}
else {
system.initialize(initializationContext, logConfig, logFile);
}
}
catch (Exception ex) {
Throwable exceptionToReport = ex;
while (exceptionToReport != null && !(exceptionToReport instanceof FileNotFoundException)) {
exceptionToReport = exceptionToReport.getCause();
}
exceptionToReport = (exceptionToReport != null) ? exceptionToReport : 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 + "'");
exceptionToReport.printStackTrace(System.err);
throw new IllegalStateException(ex);
}
}
其中,
public static final String CONFIG_PROPERTY = "logging.config";
也就是我们定义的log系统的配置文件:
logging:
#config: classpath:logback-spring.xml
config: classpath:logback.xml
file.path: ${LOGGING_PATH}
register-shutdown-hook: false
我们仍以logback为例说说具体实现。
系统定义了logback的LoggingSystem实现类(这也是Springboot的默认实现)
LogbackLoggingSystem。
关于配置文件的加载核心代码块(AbstractLoggingSystem):
@Override
public void initialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) {
if (StringUtils.hasLength(configLocation)) {
initializeWithSpecificConfig(initializationContext, configLocation, logFile);
return;
}
initializeWithConventions(initializationContext, logFile);
}
这里,configLocation就是logging.config定义的配置文件,如果该文件存在,那么就直接去初始化logback,这里的配置文件名没有特定要求,只要是logback的.groovy或者.xml配置即可。但是为了与系统内部日志配置保持一致,建议用logback.xml。
如果我们没有定义logging.config配置文件,那么就去找系统默认配置文件,查找的核心代码如下:
private void initializeWithConventions(LoggingInitializationContext initializationContext, LogFile logFile) {
String config = getSelfInitializationConfig();
if (config != null && logFile == null) {
// self initialization has occurred, reinitialize in case of property changes
reinitialize(initializationContext);
return;
}
if (config == null) {
config = getSpringInitializationConfig();
}
if (config != null) {
loadConfiguration(initializationContext, config, logFile);
return;
}
loadDefaults(initializationContext, logFile);
}
首先是通过String config = getSelfInitializationConfig()来加载:
protected String getSelfInitializationConfig() {
return findConfig(getStandardConfigLocations());
}
private String findConfig(String[] locations) {
for (String location : locations) {
ClassPathResource resource = new ClassPathResource(location, this.classLoader);
if (resource.exists()) {
return "classpath:" + location;
}
}
return null;
}
protected abstract String[] getStandardConfigLocations();
在LogbackLoggingSystem的实现是
@Override
protected String[] getStandardConfigLocations() {
return new String[] { "logback-test.groovy", "logback-test.xml", "logback.groovy", "logback.xml" };
}
也就是说这些命名的配置文件都可以被加载。其中,logback.xml还可以与前面说的Springboot内部log共用。
如果这些文件都不存在,那么会执行config = getSpringInitializationConfig()去继续查找配置文件:
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;
}
也就是说,对"logback-test.groovy", "logback-test.xml", "logback.groovy", "logback.xml"这些文件名处理后再查找:"logback-test-spring.groovy", "logback-test-spring.xml", "logback-spring.groovy", "logback-spring.xml",这也是为什么网上很多教程要求大家配置logback-spring.xml文件名的原因。
如果这些处理后的文件还不存在,那就继续执行loadDefaults(initializationContext, logFile)去初始化logback配置:
protected abstract void loadDefaults(LoggingInitializationContext initializationContext, LogFile logFile);
在LogbackLoggingSystem中实现如下:
@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}}"));
context.putProperty(LoggingSystemProperties.ROLLING_FILE_NAME_PATTERN, environment
.resolvePlaceholders("${logging.pattern.rolling-file-name:${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz}"));
new DefaultLogbackConfiguration(initializationContext, logFile).apply(configurator);
context.setPackagingDataEnabled(true);
}
initializeFinalLoggingLevels
这一步是最后设置logginglevel, 在application.properties 里定义的logging.level将在这里覆盖上面初始化好的
see https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.logging.log-levels
The following example shows potential logging settings in application.properties:
logging.level.root=warn
logging.level.org.springframework.web=debug
logging.level.org.hibernate=error
Spring Boot allows you to define logging groups in your Spring Environment. For example, here is how you could define a “tomcat” group by adding it to your application.properties:
logging.group.tomcat=org.apache.catalina,org.apache.coyote,org.apache.tomcat
实现代码在下面
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));
}
*/
protected void setLogLevels(LoggingSystem system, ConfigurableEnvironment environment) {
BiConsumer<String, LogLevel> customizer = getLogLevelConfigurer(system);
Binder binder = Binder.get(environment);
Map<String, LogLevel> levels = binder.bind(LOGGING_LEVEL, STRING_LOGLEVEL_MAP).orElseGet(Collections::emptyMap);
levels.forEach((name, level) -> configureLogLevel(name, level, customizer));
}
LoggingApplicationListener 里有定义
private static final ConfigurationPropertyName LOGGING_LEVEL = ConfigurationPropertyName.of("logging.level");
private static final ConfigurationPropertyName LOGGING_GROUP = ConfigurationPropertyName.of("logging.group");
这些执行完毕之后,LoggingApplicationListener也起动起来了,同事还把前面定义的内部log系统更新为刚加载的log日志系统。
经过上面这些步骤,Springboot的日志系统算是正式启动起来了。
通过上面的分析,我们可以得出以下注意事项:
- Springboot系统中不允许引入commons-logging.jar,但是我们在使用日志系统的时候,最好使用Log logger = LogFactory.getLog(SpringApplication.class)来定义,org.apache.commons.logging.Log和org.apache.commons.logging.LogFactory便于兼容多种日志系统;
- log4j-api.jar、log4j-to-slf4j.jar、slf4j-api.jar可以共存,但是优先级是log4j > logback;**
- log4j只能启用2.x;
- Logback的配置文件命名最好使用logback.xml;
- 最好明确定义出logging.config: classpath:logback.xml;
- 可以通过继承LoggingSystem来定义自己的LoggingSystem,通过设置System.setProperty(全类名)进行加载;
- LoggingSystem加载完毕后,系统注册了3个单例bean:springBootLoggingSystem=LoggingSystem实例,springBootLogFile=LogFile实例,对应于logging.file.path配置,springBootLoggerGroups=LoggerGroups日志分组实例。**
- 由于我们修订的logback配置文件的名称为logback.xml,这样会导致系统未使用spring的方式加载logback,所以在logback.xml中的属性配置就不能再使用springProperty,而直接使用logback的标签property即可。
日志组
通常,对相关日志记录器进行分组非常有用,这样,就可以统一配置这些日志记录器。例如,需要修改Tomcat相关的所有日志记录器的日志级别,却又记不住顶层包名。
在Spring环境中,可以定义日志组。例如,在application.properties中,可以定义并添加tomcat日志组:
properties
logging.group.tomcat=org.apache.catalina,org.apache.coyote,org.apache.tomcat
yaml
logging:
group:
tomcat: "org.apache.catalina,org.apache.coyote,org.apache.tomcat"
定义日志组后,可以修改这个日志组中所有日志记录器的日志级别:
properties
logging.level.tomcat=trace
yaml
logging:
level:
tomcat: "trace"
Spring Boot包含以下预定义日志组:
名称 | 日志记录器 |
---|---|
web | org.springframework.core.codec org.springframework.http org.springframework.web org.springframework.boot.actuate.endpoint.web org.springframework.boot.web.servlet.ServletContextInitializerBeans |
sql | org.springframework.jdbc.core org.hibernate.SQL org.jooq.tools.LoggerListener |
如何用log4j替换logback?
有时候,我们需要使用log4j而不是logback,那我们该如何做呢?
- 根据上面的分析,我们清楚地知道,在classpath中存在log4j-api.jar并且不存在slf4j-api.jar时,第一阶段就会启用log4j日志系统,但是只能是启用2.x版;
- 在第二阶段,加载LoggingSystem时有如下代码
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=LoggingSystem.class.getName(),只要loggingSystem存在,系统就会加载该LoggingSystem,而不会再去classpath去查找其他信息了。而log4j对应的LoggingSystem是org.springframework.boot.logging.log4j2.Log4J2LoggingSystem,因此,我们只需要在main启动的时候,添加对应的属性值即可:
public static void main(String[] args) throws Exception {
System.setProperty("org.springframework.boot.logging.LoggingSystem",
"org.springframework.boot.logging.log4j2.Log4J2LoggingSystem");
configureApplication(new SpringApplicationBuilder()).run(args);
}
- 注意配置log的配置文件:logging.config: classpath:log4j.properties,另外注意引入log4j的依赖jar。
这样,Springboot的默认logback日志系统就会被log4j日志系统所替代。