3万字 SpringBoot 日志全解析:快速掌握!

Spring Boot - 日志文件配置,logback 配置(超详细) 我们的Java程序在IDEA中执行完后,都会编译生成一个target文件,这个文件就是项目最终执行的代码(去掉注释等一系列工作后的文件),那么想要知道lombok的实现原理,就需要去观察这个文件~在生产环境下需要将日志信息保留下来,便于出现问题以后容易追溯问题,将日志保存下来这个活动叫做日志持久化。我们只需要在配置文件中指定日志的存储目录或者是指定日志保存文件名即可,如下。Ps:在没有指定文件名两次保存或者指定相同文件名进行两次保存的情况下,Ps:logger 对象是属于 org.slf4j 包下! 阅读详情

前言

日志,是开发中熟悉又陌生的伙伴,熟悉是因为我们经常会在各种场合打印日志,陌生是因为大部分时候我们都不太关心日志是怎么打印出来的,因为打印一条日志,在我们看来是一件太平常不过的事情了,特别是在宇宙第一框架Springboot的加持下,日志打印是怎么工作的就更没人关注了。

但是了解日志框架怎么工作,以及学会Springboot怎么和Log4j2或Logback等日志框架集成,对我们扩展日志功能以及优雅打印日志大有好处,甚至在有些场景,还能通过调整日志的打印策略来提升我们的系统吞吐量。

所以本文将以Springboot集成Log4j2为例,详细说明Springboot框架下Log4j2是如何工作的,你可能会担心,如果是使用Logback日志框架该怎么办呢,其实Log4j2和Logback极其相似,Springboot在启动时处理Log4j2和处理Logback也几乎是一样的套路,所以学会Springboot框架下Log4j2如何工作,切换成Logback也是轻轻松松的。

本文遵循一个该深则深,该浅则浅的整体指导方针,全方位的阐述Springboot中日志怎么工作,思维导图如下所示。

图片

  • Springboot版本:2.7.2

  • Log4j2版本:2.17.2

一. Log4j2简单工作原理分析

使用Log4j2打印日志时,我们自己接触最多的就是Logger对象了,Logger对象叫做日志打印器,负责打印日志,一个Logger对象,结构简单示意如下。

图片

实际打印日志的是Logger对象使用的Appender对象,至于Appender对象怎么打印日志,不在我们本文的关注范围内。特别注意,在Log4j2中,Logger对象实际只是一个壳子,灵魂是其持有的LoggerConfig对象,LoggerConfig决定打印时使用哪些Appender对象,以及Logger的级别。

LoggerConfig和Appender通常是在Log4j2的配置文件中定义出来的,配置文件通常命名为Log4j2.xml,Log4j2框架在初始化时,会去加载这个配置文件并解析成一个配置对象Configuration,示意如下。

图片

我们每在配置文件的<Appenders>标签下增加一项,解析得到的Configuration的appenders中就多一个Appender,每在<Loggers>标签下增加一项,解析得到的Configuration的loggerConfigs中就多一个LoggerConfig,并且LoggerConfig解析出来时,其和Appender的关系也就确认了。

在Log4j2中,还有一个LoggerContext对象,这个对象持有上述的Configuration对象,我们使用的每一个Logger,一开始都会先去LoggerContext的loggerRegistry中获取,如果没有,则会创建一个Logger出来再缓存到LoggerContext的loggerRegistry中,同时我们在创建Logger时其实核心就是要为这个创建的Logger找到它对应的LoggerConfig,那么去哪里找LoggerConfig呢,当然就是去Configuration中找,所以Logger,LoggerContext和Configuration的关系可以描述成下面这样子。

图片

所以Log4j2在这种结构下,要修改日志打印器是十分方便的,我们通过LoggerContext就可以拿到Configuration,拿到Configuration之后,我们就可以方便的操作LoggerConfig了,例如最常用的日志打印器级别热更新就是这么完成的。

在继续阅读后文之前,有一个很重要的概念需要阐述清楚,那就是对于Springboot来说,Springboot在操作Logger时,操作的对象就是一个Logger,比如要给一个名字为com.honey.Login的Logger设置级别为DEBUG,那么在Springboot看来,它就是在设置名字为com.honey.Login的Logger的级别为DEBUG,但是具体到Log4j2框架,其实底层是在设置名字为com.honey.Login的LoggerConfig的级别为DEBUG,而具体到Logback框架,就是在设置名字为com.honey.Login的Logger的级别为DEBUG。

二. Springboot日志简单配置说明

我们在Springboot中使用Log4j2时,虽然大部分时候我们还是会提供一个Log4j2.xml文件来供Log4j2框架读取,但是Springboot也提供了一些配置来供我们使用,在分析Springboot日志启动机制前,先学习一下里面的若干配置项可以方便我们后续的机制理解。

1. logging.file.name

假如我们像下面这样配置。

logging:
  file:
    name: test.log

那么Springboot会把日志内容输出一份到当前项目根路径下的test.log文件中。

2. logging.file.path

假如我们像下面这样配置。

logging:
  file:
    path: /

那么Springboot会把日志内容输出一份到指定目录下的spring.log文件中。

3. logging.level

假如我们像下面这样配置。

logging:
  level:
    com.pww.App: warn

那么我们可以指定名称为com.pww.App的日志打印器的级别为warn级别。

三. Springboot日志启动机制分析

通常我们使用Springboot时,就算不提供Log4j2.xml配置文件,Springboot也能输出很漂亮的日志,那么Springboot肯定在背后有帮我们完成Log4j2或Logback等框架的初始化,那么本节就刨析一下Springboot中的日志启动机制。

Springboot中的日志启动主要依赖于LoggingApplicationListener,这个监听器在Springboot启动流程中主要会监听如下三个事件。

  • ApplicationStartingEvent: 在启动SpringApplication之后就发布该事件,先于Environmen和ApplicationContext可用之前发布;

  • ApplicationEnvironmentPreparedEvent: 在Environmen准备好之后立即发布;

  • ApplicationPreparedEvent: 在ApplicationContext完全准备好之后但刷新容器之前发布。

下面依次分析下监听到这些事件后,LoggingApplicationListener会完成一些什么事情来帮助初始化日志框架。

1. 监听到ApplicationStartingEvent

LoggingApplicationListener的onApplicationStartingEvent() 方法如下所示。

private void onApplicationStartingEvent(ApplicationStartingEvent event) {
    // 读取org.springframework.boot.logging.LoggingSystem系统属性来加载得到LoggingSystem
    this.loggingSystem = LoggingSystem.get(event.getSpringApplication().getClassLoader());
    // 调用LoggingSystem的beforeInitialize()方法提前做一些初始化准备工作
    this.loggingSystem.beforeInitialize();
}

Springboot中操作日志的最关键的一个对象就是LoggingSystem,这个对象会在Springboot的整个生命周期中掌控着日志,在LoggingApplicationListener监听到ApplicationStartingEvent事件后,第一件事情就是先读取org.springframework.boot.logging.LoggingSystem系统属性,得到要加载的LoggingSystem的全限定名,然后完成加载。

如果是使用Log4j2框架,对应的LoggingSystem是Log4J2LoggingSystem,如果是使用Logback框架,对应的LoggingSystem是LogbackLoggingSystem,当然我们也可以在LoggingApplicationListener监听到ApplicationStartingEvent事件之前,提前把org.springframework.boot.logging.LoggingSystem设置为我们自己提供的LoggingSystem的全限定名,这样我们就可以对Springboot中的日志初始化做一些定制修改。

拿到LoggingSystem后,就会调用其beforeInitialize() 方法来完成日志框架初始化前的一些准备,这里看一下Log4J2LoggingSystem的beforeInitialize() 方法实现,如下所示。

@Override
public void beforeInitialize() {
    LoggerContext loggerContext = getLoggerContext();
    if (isAlreadyInitialized(loggerContext)) {
        return;
    }
    super.beforeInitialize();
    // 添加一个过滤器
    // 这个过滤器会阻止所有日志的打印
    loggerContext.getConfiguration().addFilter(FILTER);
}

上述方法最关键的就是添加了一个过滤器,虽然叫做过滤器,但是实则为阻断器,因为这个FILTER会阻止所有日志打印,Springboot这样设计是为了防止日志系统在完全完成初始化前打印出不可控的日志。

所以小结一下,LoggingApplicationListener监听到ApplicationStartingEvent之后,主要完成两件事情。

  • 从系统属性中拿到LoggingSystem的全限定名并完成加载;

  • 调用LoggingSystem的beforeInitialize() 方法来添加会拒绝打印任何日志的过滤器以阻止日志打印。

2. 监听到ApplicationEnvironmentPreparedEvent

LoggingApplicationListener的onApplicationEnvironmentPreparedEvent() 方法如下所示。

private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
    SpringApplication springApplication = event.getSpringApplication();
    if (this.loggingSystem == null) {
        this.loggingSystem = LoggingSystem.get(springApplication.getClassLoader());
    }
    // 因为此时Environment已经完成了加载
    // 获取到Environment并继续调用initialize()方法
    initialize(event.getEnvironment(), springApplication.getClassLoader());
}

继续跟进LoggingApplicationListener的initialize() 方法。

protected void initialize(ConfigurableEnvironment environment, ClassLoader classLoader) {
    // 把通过logging.xxx配置的值设置到系统属性中
    getLoggingSystemProperties(environment).apply();
    this.logFile = LogFile.get(environment);
    if (this.logFile != null) {
        // 把logging.file.name和logging.file.path的值设置到系统属性中
        this.logFile.applyToSystemProperties();
    }
    // 基于预置的web和sql日志打印器初始化LoggerGroups
    this.loggerGroups = new LoggerGroups(DEFAULT_GROUP_LOGGERS);
    // 读取配置中的debug和trace是否设置为true
    // 哪个为true就把springBootLogging级别设置为什么
    // 同时设置为true则trace优先级更高
    initializeEarlyLoggingLevel(environment);
    // 调用到具体的LoggingSystem实际初始化日志框架
    initializeSystem(environment, this.loggingSystem, this.logFile);
    // 完成日志打印器组和日志打印器的级别的设置
    initializeFinalLoggingLevels(environment, this.loggingSystem);
    registerShutdownHookIfNecessary(environment, this.loggingSystem);
}

上述方法概括下来就是做了三部分的事情。

1、把日志相关配置设置到系统属性中。例如我们可以通过logging.pattern.console来配置标准输出日志格式,但是在XML文件里面没办法读取到logging.pattern.console配置的值,此时就需要设置一个系统属性,属性名是CONSOLE_LOG_PATTERN,属性值是logging.pattern.console配置的值,后续在XML文件中就可以通过${sys:CONSOLE_LOG_PATTERN}读取到logging.pattern.console配置的值。

下表是Springboot中日志配置和系统属性名的对应关系:

图片

2、调用LoggingSystem的initialize() 方法来完成日志框架初始化。这里就是实际完成Log4j2或Logback等框架的初始化;

3、在日志框架完成初始化后基于logging.level的配置来设置日志打印器组和日志打印器的级别。

上述第2点是Springboot如何完成具体的日志框架的初始化,这个在后面章节中会详细分析。上述第3点是日志框架初始化完毕后,Springboot如何帮助我们完成日志打印器组或日志打印器的级别的设置,这里就扯出来一个概念:日志打印器组,也就是LoggerGroup。

我们如果要操作一个Logger,那么实际就是要拿着这个Logger的名称,去找到Logger,然后再进行操作,这在Logger不多的时候是没问题的,但是假如我有几十上百个Logger呢,一个一个去找到Logger再操作无疑是很不现实的,一个实际的场景就是修改Logger的级别,如果是通过Logger的名字去找到Logger再修改级别,那么是很痛苦的一件事情,但是如果能够把所有Logger按照功能进行分组,我们一组一组的去修改,一下子就优雅起来了,LoggerGroup就是干这个事情的。

一个LoggerGroup,有三个字段,说明如下。

  • name: 表示LoggerGroup的名字,要操作LoggerGroup时,就通过name来唯一确定一个LoggerGroup,假如有一个LoggerGroup名字为login,那么我们可以通过logging.level.loggin=debug,将这个LoggerGroup下所有的Logger的级别设置为debug;

  • members: 是当前LoggerGroup里所有Logger的名字的集合;

  • configuredLevel: 表示最近一次给LoggerGroup设置的级别。

在Springboot中,通过logging.group可以配置LoggerGroup,示例如下。

logging:
  group:
    login:
      - com.lee.controller.LoginController
      - com.lee.service.LoginService
      - com.lee.dao.LoginDao
    common:
      - com.lee.util
      - com.lee.config

结合logging.level可以直接给一组Logger设置级别,示例如下。

logging:
  level:
    login: info
    common: debug
  group:
    login:
      - com.lee.controller.LoginController
      - com.lee.service.LoginService
      - com.lee.dao.LoginDao
    common:
      - com.lee.util
      - com.lee.config

那么此时名称为login的LoggerGroup表示如下。

{
    "name": "login",
    "members": [
        "com.lee.controller.LoginController",
        "com.lee.service.LoginService",
        "com.lee.dao.LoginDao"
    ],
    "configuredLevel": "INFO"
}

名称为common的LoggerGroup表示如下。

{
    "name": "common",
    "members": [
        "com.lee.util",
        "com.lee.config"
    ],
    "configuredLevel": "DEBUG"
}

最后再看一下Springboot中预置的LoggerGroup,有两个,名字分别为web和sql,如下所示。

{
    "name": "web",
    "members": [
        "org.springframework.core.codec",
        "org.springframework.http",
        "org.springframework.web",
        "org.springframework.boot.actuate.endpoint.web",
        "org.springframework.boot.web.servlet.ServletContextInitializerBeans"
    ],
    "configuredLevel": ""
}
{
    "name": "sql",
    "members": [
        "org.springframework.jdbc.core",
        "org.hibernate.SQL",
        "org.jooq.tools.LoggerListener"
    ],
    "configuredLevel": ""
}

至于web和sql这两个LoggerGroup的级别是什么,有两种手段来指定,第一种是通过配置debug=true来将web和sql这两个LoggerGroup的级别指定为DEBUG,第二种是通过logging.level.web和logging.level.sql来指定web和sql这两个LoggerGroup的级别,其中第二种优先级高于第一种。

上面最后讲的这一点,其实就是告诉我们怎么来控制Springboot自己的相关的日志的打印级别,如果配置debug=true,那么如下的Springboot自己的LoggerGroup和Logger级别会设置为debug。

sql
web
org.springframework.boot

如果配置trace=true,那么如下的Springboot自己的Logger级别会设置为trace。

org.springframework
org.apache.tomcat
org.apache.catalina
org.eclipse.jetty
org.hibernate.tool.hbm2ddl

现在小结一下,监听到ApplicationEnvironmentPreparedEvent事件后,Springboot主要完成三件事情。

  • 把通过配置文件配置的日志相关属性设置为系统属性;

  • 实际完成日志框架的初始化;

  • 设置Springboot和用户自定义的LoggerGroup与Logger级别。

3. 监听到ApplicationPreparedEvent

LoggingApplicationListener的onApplicationPreparedEvent() 方法如下所示。

private void onApplicationPreparedEvent(ApplicationPreparedEvent event) {
    ConfigurableListableBeanFactory beanFactory = event.getApplicationContext().getBeanFactory();
    if (!beanFactory.containsBean(LOGGING_SYSTEM_BEAN_NAME)) {
        // 把实际加载的LoggingSystem注册到容器中
        beanFactory.registerSingleton(LOGGING_SYSTEM_BEAN_NAME, this.loggingSystem);
    }
    if (this.logFile != null && !beanFactory.containsBean(LOG_FILE_BEAN_NAME)) {
        // 把实际使用的LogFile注册到容器中
        beanFactory.registerSingleton(LOG_FILE_BEAN_NAME, this.logFile);
    }
    if (this.loggerGroups != null && !beanFactory.containsBean(LOGGER_GROUPS_BEAN_NAME)) {
        // 把保存着所有LoggerGroup的LoggerGroups注册到容器中
        beanFactory.registerSingleton(LOGGER_GROUPS_BEAN_NAME, this.loggerGroups);
    }
}

主要就是把之前加载的LoggingSystem,LogFile和LoggerGroups添加到Spring容器中,进行到这里,其实整个日志框架已经完成初始化了,这里只是把一些和日志密切相关的一些对象注册为容器中的bean。

最后,本节以下图对Springboot日志启动流程做一个总结。

图片

四. Springboot集成Log4j2原理说明

在Springboot中使用Log4j2时,我们不提供Log4j2的配置文件也能打印日志,而我们提供了Log4j2的配置文件后日志打印行为又会以我们提供的配置文件为准,这里面其实Springboot为我们做了很多事情,当我们不提供Log4j2配置文件时,Springboot会加载其预置的配置文件,并且会根据我们是否配置了logging.file.xxx自动决定是加载预置的log4j2.xml还是log4j2-file.xml,而与此同时Springboot也会尽可能的去搜索我们提供的配置文件,无论我们在classpath下提供的配置文件名字是Log4j2.xml还是Log4j2-spring.xml,都是能够被Springboot搜索到并加载的。

上述的Springboot集成Log4j2的行为,全部发生在Log4J2LoggingSystem中,本节将对这里面的流程和原理进行说明。

在第三节中已经知道,Springboot启动时,当LoggingApplicationListener监听到ApplicationEnvironmentPreparedEvent事件后,最终会调用到LoggingApplicationListener的initializeSystem() 方法来完成日志框架的初始化,所以我们先看一下这里的逻辑是什么,源码实现如下。

private void initializeSystem(ConfigurableEnvironment environment, LoggingSystem system, LogFile logFile) {
    // 读取环境变量中的logging.config作为用户提供的配置文件路径
    String logConfig = StringUtils.trimWhitespace(environment.getProperty(CONFIG_PROPERTY));
    try {
        // 创建LoggingInitializationContext用于传递Environment对象
        LoggingInitializationContext initializationContext = new LoggingInitializationContext(environment);
        if (ignoreLogConfig(logConfig)) {
            // 1. 没有配置logging.config
            system.initialize(initializationContext, null, logFile);
        } else {
            // 2. 配置了logging.config
            system.initialize(initializationContext, logConfig, logFile);
        }
    } catch (Exception ex) {
        // 省略异常处理
    }
}

LoggingApplicationListener的initializeSystem() 方法会读取logging.config环境变量得到用户提供的配置文件路径,然后带着配置文件路径,调用到Log4J2LoggingSystem的initialize() 方法,所以后续分两种情况讨论,即没配置logging.config和有配置logging.config。

1. 没配置logging.config

Log4J2LoggingSystem的initialize() 方法如下所示。

@Override
public void initialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) {
    LoggerContext loggerContext = getLoggerContext();
    // 判断LoggerContext的ExternalContext是不是当前LoggingSystem的全限定名
    // 如果是则表明当前LoggingSystem已经执行过初始化逻辑
    if (isAlreadyInitialized(loggerContext)) {
        return;
    }
    // 移除之前添加的防噪过滤器
    loggerContext.getConfiguration().removeFilter(FILTER);
    // 调用到父类AbstractLoggingSystem的initialize()方法
    // 注意因为没有配置logging.config所以这里configLocation为null
    super.initialize(initializationContext, configLocation, logFile);
    // 将当前LoggingSystem的全限定名设置给LoggerContext的ExternalContext
    // 表明当前LoggingSystem已经对LoggerContext执行过初始化逻辑
    markAsInitialized(loggerContext);
}

上述方法会继续调用到AbstractLoggingSystem的initialize() 方法,并且因为没有配置logging.config,所以传递过去的configLocation参数为null,下面看一下AbstractLoggingSystem的initialize() 方法的实现,如下所示。

@Override
public void initialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) {
    if (StringUtils.hasLength(configLocation)) {
        initializeWithSpecificConfig(initializationContext, configLocation, logFile);
        return;
    }
    // 基于约定寻找配置文件并完成初始化
    initializeWithConventions(initializationContext, logFile);
}

因为configLocation为null,所以会继续调用到initializeWithConventions() 方法完成初始化,并且初始化使用到的配置文件,Springboot会按照约定的名字去classpath寻找,下面看一下initializeWithConventions() 方法的实现。

private void initializeWithConventions(LoggingInitializationContext initializationContext, LogFile logFile) {
    // 搜索标准日志配置文件路径
    String config = getSelfInitializationConfig();
    if (config != null && logFile == null) {
        reinitialize(initializationContext);
        return;
    }
    if (config == null) {
        // 搜索Spring日志配置文件路径
        config = getSpringInitializationConfig();
    }
    if (config != null) {
        // 如果搜索到约定的配置文件则进行配置文件加载
        loadConfiguration(initializationContext, config, logFile);
        return;
    }
    // 如果搜索不到则使用LoggingSystem同目录下的配置文件
    loadDefaults(initializationContext, logFile);
}

上述方法中,首先会去搜索标准日志配置文件路径,其实就是判断classpath下是否存在如下名字的配置文件。

log4j2-test.properties
log4j2-test.json
log4j2-test.jsn
log4j2-test.xml
log4j2.properties
log4j2.json
log4j2.jsn
log4j2.xml

如果不存在,则再去搜索Spring日志配置文件路径,也就是判断classpath下是否存在如下名字的配置文件。

log4j2-test-spring.properties
log4j2-test-spring.json
log4j2-test-spring.jsn
log4j2-test-spring.xml
log4j2-spring.properties
log4j2-spring.json
log4j2-spring.jsn
log4j2-spring.xml

如果都找不到,此时Springboot就会将Log4J2LoggingSystem同目录下的log4j2.xml(无LogFile)或log4j2-file.xml(有LogFile)作为日志配置文件,所以不用担心找不到配置文件,有Springboot为我们进行兜底。

在获取到配置文件路径后,最终会调用到Log4J2LoggingSystem如下的加载配置的方法。

protected void loadConfiguration(String location, LogFile logFile, List<String> overrides) {
    Assert.notNull(location, "Location must not be null");
    try {
        List<Configuration> configurations = new ArrayList<>();
        LoggerContext context = getLoggerContext();
        // 根据配置文件路径加载得到Configuration并添加到集合中
        configurations.add(load(location, context));
        // 加载logging.log4j2.config.override配置的配置文件为Configuration
        // 所有加载的Configuration都要添加到configurations集合中
        for (String override : overrides) {
            configurations.add(load(override, context));
        }
        // 如果得到了大于1个的Configuration则基于所有Configuration创建CompositeConfiguration
        Configuration configuration = (configurations.size() > 1) ? createComposite(configurations)
                : configurations.iterator().next();
        // 将加载得到的Configuration启动并设置给LoggerContext
        // 这里会将加载得到的Configuration覆盖LoggerContext持有的老的Configuration
        context.start(configuration);
    } catch (Exception ex) {
        throw new IllegalStateException("Could not initialize Log4J2 logging from " + location, ex);
    }
}

上述方法中实际就会拿着配置文件的路径去加载得到Configuration,与此同时还会拿到所有通过logging.log4j2.config.override配置的路径,去加载得到Configuration,最终如果得到大于1个的Configuration,则将这些Configuration创建为CompositeConfiguration。

这里可能会有疑问,logging.log4j2.config.override到底是一个什么东西,其实不难发现,无论是通过logging.config指定了配置文件路径,还是按照Springboot约定提供了配置文件,亦或者使用了Springboot预置的配置文件,其实最终都只能得到一个配置文件路径然后得到一个Configuration,那么怎么才能加载多份配置文件呢,那就要通过logging.log4j2.config.override来指定多个配置文件路径,使用示例如下。

logging:
  config: classpath:Log4j2.xml
  log4j2:
    config:
      override:
        - classpath:Log4j2-custom1.xml
        - classpath:Log4j2-custom2.xml

如果按照上面这样配置,那么最终就会加载得到三个Configuration,然后再基于这三个Configuration创建得到一个CompositeConfiguration。

在加载得到Configuration之后,就会调用到LoggerContext的start() 方法完成Log4j2框架的初始化,那么这里其实会做如下三件事情。

  • 调用Configuration的start() 方法完成配置对象的初始化。 这里其实就是将我们在配置文件中定义的各种Appedner和LoggerConfig等都创建出来并完成启动;

  • 将启动完毕的Configuration设置给LoggerContext。 这里会把LoggerContext持有的老的Configuration覆盖掉,所以如果LoggerContext之前持有其它的Configuration,那么其实在Springboot日志初始化完毕后老的Configuration会被丢弃掉;

  • 更新Logger。 如果之前有已经创建好的Logger,那么就基于新的Configuration替换掉这些Logger持有的LoggerConfig。

至此,没配置logging.config时的初始化逻辑就分析完毕。

2. 有配置logging.config

有配置logging.config时,情况就变得简单了。还是从Log4J2LoggingSystem的initialize() 方法出发,跟一下源码。

@Override
public void initialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) {
    LoggerContext loggerContext = getLoggerContext();
    if (isAlreadyInitialized(loggerContext)) {
        return;
    }
    loggerContext.getConfiguration().removeFilter(FILTER);
    // 调用到父类AbstractLoggingSystem的initialize()方法
    // 注意因为配置了logging.config所以这里configLocation不为null
    super.initialize(initializationContext, configLocation, logFile);
    markAsInitialized(loggerContext);
}

继续跟进AbstractLoggingSystem的initialize() 方法,如下所示。

@Override
public void initialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) {
    if (StringUtils.hasLength(configLocation)) {
        // 基于指定的配置文件完成初始化
        initializeWithSpecificConfig(initializationContext, configLocation, logFile);
        return;
    }
    initializeWithConventions(initializationContext, logFile);
}

由于指定了配置文件,所以会调用到AbstractLoggingSystem的initializeWithSpecificConfig() 方法,该方法没有什么额外逻辑,最终会执行到和没配置logging.config时一样的Log4J2LoggingSystem的加载配置的方法,如下所示。

protected void loadConfiguration(String location, LogFile logFile, List<String> overrides) {
    Assert.notNull(location, "Location must not be null");
    try {
        List<Configuration> configurations = new ArrayList<>();
        LoggerContext context = getLoggerContext();
        // 根据配置文件路径加载得到Configuration并添加到集合中
        configurations.add(load(location, context));
        // 加载logging.log4j2.config.override配置的配置文件为Configuration
        // 所有加载的Configuration都要添加到configurations集合中
        for (String override : overrides) {
            configurations.add(load(override, context));
        }
        // 如果得到了大于1个的Configuration则基于所有Configuration创建CompositeConfiguration
        Configuration configuration = (configurations.size() > 1) ? createComposite(configurations)
                : configurations.iterator().next();
        // 将加载得到的Configuration启动并设置给LoggerContext
        // 这里会将加载得到的Configuration覆盖LoggerContext持有的老的Configuration
        context.start(configuration);
    } catch (Exception ex) {
        throw new IllegalStateException("Could not initialize Log4J2 logging from " + location, ex);
    }
}

所以配置了logging.config时,就会以logging.config指定的配置文件作为最终使用的配置文件,而不会去基于约定搜索配置文件,同时也不会去使用LoggingSystem同目录下预置的配置文件。

小结一下,Springboot集成Log4j2日志框架时,主要分为两种情况:

  • 没配置logging.config。 这种情况下,Springboot会基于约定努力去寻找符合的配置文件,如果找不到则会使用预置的配置文件且预置的配置文件需要在LoggingSystem的同目录下,拿到配置文件后就会加载为Configuration然后替换掉LoggerContext里的旧的Configuration,此时就完成日志框架初始化;

  • 有配置logging.config。 这种情况下,会将logging.config指定的配置文件加载为Configuration,然后替换掉LoggerContext里的旧的Configuration,此时就完成日志框架初始化。

无论有没有配置logging.config,都只能加载一个配置文件为Configuration,如果想加载多个Configuration,那么需要通过logging.log4j2.config.override配置多个配置文件路径,此时就能加载多个Configuration来初始化Log4j2日志框架了。

Springboot集成Log4j2日志框架的流程图如下所示。

图片

五. Springboot日志打印器级别热更新

在日志打印中,一条日志在发起打印时,会根据我们的指定携带一个日志级别,同时打印日志的日志打印器,也有一个级别,日志打印器只能打印级别高于或等于自身的日志。

由于日志打印时,日志级别是由代码决定的,所以日志级别除非改代码,否则无法改变,但是日志打印器的级别是可以随时更改的,最简单的方式就是通过配置环境变量来更改logging.level,此时我们的应用进程所处的容器就会重启,就可以读取到我们更改后的logging.level,最终完成日志打印器级别的修改。

但是这种方式会使应用重启,导致流量受损,我们更希望的是通过一种热更新的方式来修改日志打印器的级别,spring-boot-actuator包中提供了LoggersEndpoint来完成日志打印器级别热更新,所以本节将结合LoggersEndpoint的简单使用和实现原理,说明一下Springboot中,如何热更新日志打印器级别。

1. LoggersEndpoint简单使用

LoggersEndpoint由spring-boot-actuator提供,可以暴露一些端点用于获取Springboot应用中的所有日志打印器信息及其级别信息以及热更新日志打印器级别,由于默认情况下,LoggersEndpoint暴露的端点只能通过JMX的方式访问,所以想要通过HTTP请求的方式访问到LoggersEndpoint,需要做如下配置。

management:
  server:
    address: 127.0.0.1
    port: 10999
  endpoints:
    web:
      base-path: /actuator
      exposure:
        include: loggers    # 设置LoggersEndpoint可以通过HTTP方式访问
  endpoint:
    loggers:
      enabled: true     # 打开LoggersEndpoint

按照上述这么配置,我们可以通过GET调用如下接口拿到当前所有的日志打印器的相关数据。

“

http://localhost:10999/actuator/loggers

获取数据如下所示。

{
    "levels": [
        "OFF",
        "FATAL",
        "ERROR",
        "WARN",
        "INFO",
        "DEBUG",
        "TRACE"
    ],
    "loggers": {
        "ROOT": {
            "configuredLevel": null,
            "effectiveLevel": "INFO"
        },
        "org.springframework.boot.actuate.autoconfigure.web.server": {
            "configuredLevel": null,
            "effectiveLevel": "DEBUG"
        },
        "org.springframework.http.converter.ResourceRegionHttpMessageConverter": {
            "configuredLevel": null,
            "effectiveLevel": "ERROR"
        }
    },
    "groups": {
        "web": {
            "configuredLevel": null,
            "members": [
                "org.springframework.core.codec",
                "org.springframework.http",
                "org.springframework.web",
                "org.springframework.boot.actuate.endpoint.web",
                "org.springframework.boot.web.servlet.ServletContextInitializerBeans"
            ]
        },
        "login": {
            "configuredLevel": "INFO",
            "members": [
                "com.lee.controller.LoginController",
                "com.lee.service.LoginService",
                "com.lee.dao.LoginDao"
            ]
        },
        "common": {
            "configuredLevel": "DEBUG",
            "members": [
                "com.lee.util",
                "com.lee.config"
            ]
        },
        "sql": {
            "configuredLevel": null,
            "members": [
                "org.springframework.jdbc.core",
                "org.hibernate.SQL",
                "org.jooq.tools.LoggerListener"
            ]
        }
    }
}

上述内容中,返回的levels表示当前支持的日志级别,返回的loggers表示当前所有日志打印器的级别信息,返回的groups表示当前所有日志打印器组的级别信息,但是请注意,上述示例中的loggers其实做了大量的删减,实际调用接口时得到的loggers里面的内容会非常非常多,因为所有的日志打印器的信息都会被输出出来。

此外,上述内容中出现的configuredLevel字段表示当前日志打印器或日志打印器组被设置过的级别,也就是只要通过LoggersEndpoint给某个日志打印器或日志打印器组设置过级别,那么对应的configuredLevel字段就有值,最后上述内容中出现的effectiveLevel字段表示当前日志打印器正在生效的级别。

如果只想看某个日志打印器或日志打印器组的级别信息,可以调用如下的GET接口。

“

http://localhost:10999/actuator/loggers/{日志打印器名或日志打印器组名}

如果pathVariable是日志打印器名,那么会得到如下结果。

{
    "configuredLevel": null,
    "effectiveLevel": "INFO"
}

如果pathVariable是日志打印器组名,那么会得到如下结果。

{
    "configuredLevel": null,
    "members": [
        "org.springframework.core.codec",
        "org.springframework.http",
        "org.springframework.web",
        "org.springframework.boot.actuate.endpoint.web",
        "org.springframework.boot.web.servlet.ServletContextInitializerBeans"
    ]
}

除了查询日志打印器或日志打印器组的级别信息,LoggersEndpoint更重要的功能是设置级别,比如可以通过如下POST接口来设置级别。

“

http://localhost:10999/actuator/loggers/{日志打印器名或日志打印器组名}

{
 "configuredLevel": "DEBUG"
}

此时对应的日志打印器或日志打印器组的级别就会更新为设置的级别,并且其configuredLevel也会更新为设置的级别。

2. LoggersEndpoint原理分析

这里主要关注LoggersEndpoint如何实现日志打印器级别的热更新。LoggersEndpoint实现日志打印器级别的热更新对应的端点方法如下所示。

@WriteOperation
public void configureLogLevel(@Selector String name, @Nullable LogLevel configuredLevel) {
    Assert.notNull(name, "Name must not be empty");
    // 先尝试获取到LoggerGroup
    LoggerGroup group = this.loggerGroups.get(name);
    if (group != null && group.hasMembers()) {
        // 如果能获取到LoggerGroup则对组下每个Logger热更新级别
        group.configureLogLevel(configuredLevel, this.loggingSystem::setLogLevel);
        return;
    }
    // 获取不到LoggerGroup则按照Logger来处理
    this.loggingSystem.setLogLevel(name, configuredLevel);
}

上述方法的name即可以是Logger的名称,也可以是LoggerGroup的名称,如果是Logger的名称,那么就基于LoggingSystem的setLogLevel() 方法来设置这个Logger的级别,如果是LoggerGroup的名称,那么就遍历这个组下所有的Logger,每个遍历到的Logger都基于LoggingSystem的setLogLevel() 方法来设置级别。

所以实际上LoggersEndpoint热更新日志打印器级别,还是依赖的对应日志框架的LoggingSystem。

3. Log4J2LoggingSystem热更新原理

由于本文是基于Log4j2日志框架进行讨论,所以这里选择分析Log4J2LoggingSystem的setLogLevel() 方法,来探究Logger级别如何热更新。

在开始分析前,有一点需要重申,那就是对于Log4j2来说,Logger只是壳子,灵魂是Logger持有的LoggerConfig,所以更新Log4j2里面的Logger的级别,其实就是要去更新其持有的LoggerConfig的级别。

Log4J2LoggingSystem的setLogLevel() 方法如下所示。

@Override
public void setLogLevel(String loggerName, LogLevel logLevel) {
    // 将LogLevel转换为Level
    setLogLevel(loggerName, LEVELS.convertSystemToNative(logLevel));
}

LogLevel是Springboot中的日志级别对象,Level是Log4j2的日志级别对象,所以需要先将LogLevel转换为Level,然后继续调用如下方法。

private void setLogLevel(String loggerName, Level level) {
    // 从Configuration中根据loggerName获取到对应的LoggerConfig
    LoggerConfig logger = getLogger(loggerName);
    if (level == null) {
        // 2. 移除LoggerConfig或设置LoggerConfig级别为null
        clearLogLevel(loggerName, logger);
    } else {
        // 1. 添加LoggerConfig或设置LoggerConfig级别
        setLogLevel(loggerName, logger, level);
    }
    // 3. 更新Logger
    getLoggerContext().updateLoggers();
}

通过第一节知道,Log4j2的Configuration对象有一个字段叫做loggerConfigs,所以上面首先就是通过loggerName去loggerConfigs中匹配对应的LoggerConfig,那么这里就会存在一个问题,那就是配置文件里面每配一个Logger,loggerConfigs才会增加一个LoggerConfig,所以实际上loggerConfigs里面的LoggerConfig并不会很多,比如我们提供了如下一个Log4j2.xml文件。

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="INFO">
    <Appenders>
        <Console name="MyConsole"/>
    </Appenders>

    <Loggers>
        <Root level="INFO">
            <Appender-ref ref="MyConsole"/>
        </Root>
        <Logger name="com.honey" level="WARN">
            <Appender-ref ref="MyConsole"/>
        </Logger>
        <Logger name="com.honey.auth.Login" level="DEBUG">
            <Appender-ref ref="MyConsole"/>
        </Logger>
    </Loggers>
</Configuration>

那么实际加载得到的Configuration的loggerConfigs只有下面这几个名字的LoggerConfig。

""
com.honey
com.honey.auth.Login

其中空字符串是根日志打印器(rootLogger)的名字。此时如果在调用Log4J2LoggingSystem的setLogLevel() 方法时传入的loggerName是com.honey.auth.Login,我们可以很顺利的从Configuration的loggerConfigs中拿到名字是com.honey.auth.Login的LoggerConfig,可要是传入的loggerName是com.honey.auth.Logout呢,那么获取出来的LoggerConfig肯定是null,此时该怎么处理呢,难道就不设置日志打印器的级别了吗?

当然不是的,Springboot在这里做了一个巨巧妙的设计,就是如果热更新Log4j2时通过loggerName没有获取到LoggerConfig,那么Springboot就会创建一个LevelSetLoggerConfig(LoggerConfig的子类)然后添加到Configuration的loggerConfigs中。

下面先看一下LevelSetLoggerConfig长什么样。

private static class LevelSetLoggerConfig extends LoggerConfig {

    LevelSetLoggerConfig(String name, Level level, boolean additive) {
        super(name, level, additive);
    }

}

既然我们往Configuration的loggerConfigs中添加了一个名字是com.honey.auth.Logout的LevelSetLoggerConfig,那么名字是com.honey.auth.Logout的Logger理所应当的就会持有名字是com.honey.auth.Logout的LevelSetLoggerConfig,但是聪明的人就发现了,这个新创建出来的LevelSetLoggerConfig也是没有灵魂的,为什么呢,因为LevelSetLoggerConfig不引用任何的Appedner,没有Appedner怎么打日志嘛,不过不用担心,只要在创建LevelSetLoggerConfig时,将additive指定为true,这个问题就解决了。

在Log4j2中,LoggerConfig之间是有父子关系的,假如Configuration的loggerConfigs有下面这几个名字的LoggerConfig。

""
com.honey
com.honey.auth.Login

那么名字是com.honey.auth.Login的LoggerConfig会依次按照com.honey.auth,com.honey,com和 "" 去寻找自己的父LoggerConfig,所以每个LoggerConfig都有自己的父LoggerConfig,而additive参数的含义就是,当前日志是否还需要由父LoggerConfig打印,如果某个LoggerConfig的additive是true,那么一条日志除了让自己的所有Appedner打印,还会让父LoggerConfig的所有Appender来打印。

所以只要在创建LevelSetLoggerConfig时,将additive指定为true,就算LevelSetLoggerConfig自己没有Appender,父亲也是可以打印日志的。下面举个例子来加深理解,还是假如Configuration的loggerConfigs有下面这几个名字的LoggerConfig。

""
com.honey
com.honey.auth.Login

我们已经有一个名字为com.honey.auth.Logout的Logger,并且按照Logger寻找LoggerConfig的规则,我们知道名字为com.honey.auth.Logout的Logger会持有名字为com.honey的LoggerConfig,那么现在我们要热更新名字为com.honey.auth.Logout的Logger的级别,此时拿着com.honey.auth.Logout从Configuration的loggerConfigs中获取出来的LoggerConfig肯定为null,所以我们会创建一个名字为com.honey.auth.Logout的LevelSetLoggerConfig,并且这个LevelSetLoggerConfig的additive为true,此时Configuration的loggerConfigs有下面这几个名字的LoggerConfig。

""
com.honey
com.honey.auth.Login
com.honey.auth.Logout

此时我们重新让名字为com.honey.auth.Logout的Logger去寻找自己应该持有的LoggerConfig,那么肯定就会找到名字为com.honey.auth.Logout的LevelSetLoggerConfig,由于Log4j2中,Logger的级别跟着LoggerConfig走,所以名字为com.honey.auth.Logout的Logger的级别就更新了,现在使用名字为com.honey.auth.Logout的Logger打印日志,首先会让其持有的LoggerConfig引用的Appedner来打印,由于没有引用Appedner,所以不会打印日志,然后再让其父LoggerConfig引用的Appedner来打印日志,而名字为com.honey.auth.Logout的LevelSetLoggerConfig的父亲其实就是名字为com.honey的LoggerConfig,所以最终还是让名字为com.honey的LoggerConfig引用的Appedner完成了日志打印。

到这里仿佛好像逐渐偏离了本小节的主题,其实不是的,我们现在再回看Log4J2LoggingSystem的setLogLevel() 方法,如下所示。

private void setLogLevel(String loggerName, Level level) {
    // 从Configuration中根据loggerName获取到对应的LoggerConfig
    LoggerConfig logger = getLogger(loggerName);
    if (level == null) {
        // 2. 移除LoggerConfig或设置LoggerConfig级别为null
        clearLogLevel(loggerName, logger);
    } else {
        // 1. 添加LoggerConfig或设置LoggerConfig级别
        setLogLevel(loggerName, logger, level);
    }
    // 3. 更新Logger
    getLoggerContext().updateLoggers();
}

首先是第1点,在传入的level不为空时,我们就会去设置对应的LoggerConfig的级别,如果获取到的LoggerConfig为空,那么就会创建一个名字为loggerName,级别为level的LevelSetLoggerConfig并加到Configuration的loggerConfigs中,如果获取到的LoggerConfig不为空,则直接修改LoggerConfig的level字段。

其次是第2点,传入level为空时,此时要求能通过loggerName找到LoggerConfig,否则抛空指针异常。如果通过loggerName找到的LoggerConfig不为空,此时需要判断一下LoggerConfig的类型,如果LoggerConfig实际类型是LevelSetLoggerConfig,那么就从Configuration的loggerConfigs中将其移除,如果LoggerConfig实际类型就是LoggerConfig,那么就设置LoggerConfig的level字段为null。

最后是第3点,在前面第1和第2点,我们已经让目标LoggerConfig的级别完成了更新,此时就需要让LoggerContext里面所有的Logger重新去匹配一次自己的LoggerConfig,至此就完成了Logger的级别的更新。

相信到这里,Log4J2LoggingSystem热更新原理就阐释清楚了,小结一下就是通过loggerName找LoggerConfig,找到了就更新其level,找不到就创建一个名字为loggerName的LevelSetLoggerConfig,最后让所有Logger去重新匹配一下自己的LoggerConfig,此时我们的目标Logger就会持有更新过级别的LoggerConfig了。

最后给出基于LoggersEndpoint热更新Log4j2日志打印器的流程图,如下所示。

图片

六. 自定义Springboot下日志打印器级别热更新

有些时候,使用spring-boot-actuator包提供的LoggersEndpoint来热更新日志打印器级别,是有点不方便的,因为想要热更新日志级别而引入spring-boot-actuator包,大部分时候这个操作都有点重,而通过上面的分析,我们发现其实热更新日志打印器级别的原理特别简单,就是通过LoggingSystem来操作Logger,所以我们可以自己提供一个接口,通过这个接口来操作Logger的级别。

@RestController
public class HotModificationLevel {

    private final LoggingSystem loggingSystem;

    public HotModificationLevel(LoggingSystem loggingSystem) {
        this.loggingSystem = loggingSystem;
    }

    @PostMapping("/logger/level")
    public void setLoggerLevel(@RequestBody SetLoggerLevelParam levelParam) {
        loggingSystem.setLogLevel(levelParam.getLoggerName(), levelParam.getLoggerLevel());
    }

    public static class SetLoggerLevelParam {
        private String loggerName;
        private LogLevel loggerLevel;

        // 省略getter和setter
    }

}

通过调用上述接口使用LoggingSystem就能够完成指定日志打印器的级别热更新。

总结

对于Log4j2日志框架,我们需要知道Logger只是一个壳子,灵魂是Logger持有的LoggerConfig。

Springboot框架启动时,日志的初始化的发起点是LoggingApplicationListener,但是实际去寻找日志框架的配置文件并完成日志框架初始化是LoggingSystem。

在Springboot中提供日志框架的配置文件时,我们可以将配置文件命名为约定的名字然后放在classpath下,也可以通过logging.config显示的指定要使用的配置文件的路径,甚至可以完全不自己提供配置文件而使用Springboot预置的配置文件,因此使用Springboot框架,想打印日志是十分容易的。

Springboot框架中,为了统一的管理一组Logger,定义了一个日志打印器组LoggerGroup,通过操作LoggerGroup,可以方便的操作一组Logger,我们可以使用logging.group.xxx来定义LoggerGroup,而xxx就是组名,后续拿着组名就可以找到LoggerGroup并操作。

所谓日志打印器级别热更新,其实就是不重启应用的情况下修改日志打印器的级别,核心思路就是通过LoggingSystem去操作底层的日志框架,因为LoggingSystem可以为我们屏蔽底层的日志框架的细节,所以通过LoggingSystem修改日志打印器级别,是十分容易的

跳槽涨薪、简历修改、进大厂、转行、提升技术、职业生涯迷茫点击此处咨询icon-default.png?t=O83Ahttps://gaoneng.bilibili.com/tetris/page/?pageId=962039073551478784&pre_render_global=__PRE_RENDER_GLOBAL__&auto_media_playback=1&navhide=0&is_preview=1&event_source_type=4&mid=207657314&buvid=C0D1E632-09AE-5DD6-B55D-35ECAEF3FB6E86180infoc&content_value=BV15uB4YKE7h&content_type=video

SpringBoot整合ELK Stack日志全栈指南 ELK Stack与SpringBoot集成实践摘要 ELK Stack(Elasticsearch、Logstash、Kibana)是一套强大的日志管理解决方案。核心组件分工明确:Elasticsearch负责存储检索,Logstash处理日志管道,Kibana提供可视化界面。现代架构通常加入轻量级Filebeat进行日志采集。 与SpringBoot集成时,关键步骤包括: 配置Logback输出JSON格式结构化日志 使用Filebeat监控日志文件并传输至Logstash Logstash处理后存入E 阅读详情

相关推荐

全栈快速开发脚手架:Springboot + Vue 3 + Typescript + Mybatis 5.6

Spring Boot是由Pivotal团队提供的全新框架,其设计目的是用来简化Spring应用的初始搭建以及开发过程。它使用“约定优于配置”的原则,提供了大量的默认配置,旨在减少开发人员的工作量。Spring Boot应用能够创建独立的、生产级别的基于Spring框架的应用,并且可直接运行。

weixin_28746457的博客 1273

springboot 日志 logging.file.name、logging.file.path和actuator/logfile

首先对 logging.file.name 和 logging.file.path 进行一下说明,但是它俩的区别并不是本文的重点。 1、logging.file.name 设置具体输出的日志名称,可以是绝对路径或者基于当前运行目录的相对路径,例如:logging.file.name=app.log、logging.file.name=/var/log/hello-service/app.log 2、logging.file.path 设置输出的日志被写入到的目录,默认文件名为 spring.log,例如:l

小单的博客专栏 9026

深信服PT1-AF实验考试

深信服PT1-AF实验考试实验练习可以去深信服的在线实验平台(链接如下):深信服在线实验平台 (sangfor.com.cn)实验考试其实就是把所有的每一部分实验综合了起来以下内容仅做参考,不确保一样。

m0_72019240的博客 7122

Spring Boot 中的日志

除此之外,日志还有其他作用:在此之前也许会经常使用 System.out.println 来作为日志打印信息,看程序运行到何处出现了问题,但是该方法所包含的信息太少,没有办法将信息持久化,没有办法控制日志等级从而进行信息的过滤…通过本节将介绍真正的高效日志的输出与存储日志等级的划分方便了程序员对信息的筛选trace 和 debug 这两种日志一般使用来记录系统运行状态,通过该日志,我们可以查看操纵的每一步执行的详细过程info 这种日志用来记录系统正常运行状态,比如系统初始化,请求成功执行…warn 这种日

weixin_46103589的博客 3309

Ubuntu20.04系统笔记本网卡AX211无法找到wifi问题

之前电脑的系统是18.04基本上安装好了系统,可以直接连上wifi,在买了拯救者y9000p2022版本之后,因为网卡太新,安装ubuntu18.04或者20.04之后会没有wifi连接,非常不方便。于是上网找了各种帖子去解决这个问题。具体实现有所不同。 主要参考了这个帖子来解决的:http://t.csdn.cn/7807R 首先: sudo apt install flex bison 然后: git clone https://github.com/intel/backpor.

weixin_42825962的博客 1万+

银河麒麟V10-ARM架构-postgresql安装与部署指南

本文详细介绍应用源码进行pgsql的安装步骤,本文以postgresql-12.0为例。提示:以下是本篇文章正文内容,下面案例可供参考银河麒麟系统安装程序比较繁琐,潜在问题比较多,个人建议均通过源码自定义编译安装。国产系统还需努力啊!

idols_man的博客 1万+

日志配置——logging.file.name与logging.file.path的使用

日志配置——logging.file.name与logging.file.path的使用 logging.file.name可以指定日志文件的路径与名字; 例如:logging.file.name:D://logs/ex.log 则该日志文件的路径为D://logs 日志文件的名字是ex.log logging.file.path可以指定日志文件的路径,而不能指定名字 例如:logging.file.path:D://logs 则该日志文件的路径为D://logs 日志文件的名字是默认的sprin

Delusion的博客 8854

一文看懂分布式存储架构

目录一、集中存储结构二、分布式存储1 、分布式存储的兴起2 、分布式存储的重要性3 、分布式存储的种类和比较三、分布式理论浅析1 、一致性和可用性2 、数据分布3 、复制4 、分布式协议5、跨机房部署四、分布式文件系统1、 Google 文件系统( GFS )2、 Taobao 文件系统( TFS )3、 Fackbook Haystack 文件系统4、 CDN 内容分发网络五、分布式键值系统1、 Amazon Dynamo2、 Taobao Tiar3、 ETCD4 、产品选型比较( Etcd , Zoo

weixin_70730532的博客 2万+

SpringBoot之日志

SpringBoot之日志 1.关于日志级别 error > warn > info > debug > trace springboot默认日志级别是 info 我们可以在springboot的默认配置文件中修改日志级别 logging.level是map类型, 我们可以指定不同包下使用不同的日志级别 logging: level: com.atguigu: trace 2.以文件形式打印日志logging.file.name springboot默认日志的输出方.

疯狂的菜鸡 4975

使用trae自动生成前后端分离的web应用程序

使用ai自动生成前后端分离的web应用程序

一介书生 2529

PCL基础:getTransformation函数全流程解析与应用实例,一篇文章精通点云变换

在 Eigen 和 PCL 中,getTransformation是一个常用的函数,用于根据输入的平移和旋转信息(如欧拉角或四元数)生成一个仿射变换矩阵(Eigen::Affine3f或 Eigen::Affine3d)。1)在 Eigen 中,可以通过四元数和平移向量构造一个变换矩阵。2)讲解了从四元数转换到欧拉角,或者从欧拉角转换到四元数的方法。

weixin_42896234的博客 1368

进大厂必看!拼多多大佬总结出来的10万字Springboot经典学习笔记PDF版本,分享给大家!

大家好,我是武哥,今天给大家免费分享我写的 5 万字 Spring Boot 学习笔记(带完整目录)以及对应的源码。这是我之前在 优快云 GitChat 开的一门课,所以笔记非常详细完整。课程现在已经下架了,我准备将资料分享出来给大家免费学习,相信大家看完一定会有所收获。 版权申明:该资料的版权属于 优快云 和作者,仅给读者学习使用,未经允许,不得将其用于商业用途和其他任何形式的不正当用途,否则追究法律责任。 该笔记总共有19章: 导读:课程概览 第01课:Spring Boot开发环境搭建和项

2万+

ethernet eth0: Could not attach to PHY

整个流程看下来就是MAC connect PHY的过程失败了,失败的具体原因根据config_init,上图(bcm54xx_config_init) ,从此方向入手去定位原因;我这里的问题是has_gmac4没有置1,根据 has_gmac4: uses GMAC4 core.按照上面的调用逻辑,添加了部分打印,定位到__phy_read接口返回失败。stmmac_mdio_read 大部分都是linux的标准接口。最终取决于平台用的是哪家的phy,调用config_init。

无线互联&网络介绍 2704

springboot快速入门一篇文章全

目录1、pom.xml2、项目层次3、启动文件【com.item/Action.java】4、controller文件5、启动测试(数据是跨域的) 2、项目层次 其中@RestController = @Controller + @ResponseBody; 5、启动测试(数据是跨域的) 访问路径:【http://127.0.0.1:8080/GetInfo】目录1、修改打包方式2、修改启动文件【Action.java】3、打包命令4、打包效果:在【pom.xml】文件中修改默认的打包方式

红目香薰 5万+

24、汽车黑客技术全解析:诊断代码、车库创建与系统攻防

本文深入探讨了汽车黑客技术的核心内容,包括诊断代码模式与参数ID、创建开放车库社区、汽车系统相关技术、硬件与软件工具、攻击与防御手段,以及车辆通信与网络安全。无论你是汽车安全研究人员、爱好者还是开发者,都能从中获取全面的技术解析与实践指南。

Apple的专栏 229

IMSL CNL 7.0.0 (x86-64)Crack

IMSL CNL 7.0.0 (x86-64)Crack intel的算法库破解文件,这是C++版本的破解文件

10 万字 Spring Boot 学习笔记(带完整目录)以及对应的源码免费分享给大家

大家好,我是武哥,今天给大家免费分享我写的 5 万字 Spring Boot 学习笔记(带完整目录)以及对应的源码。这是我之前在 优快云 GitChat 开的一门课,所以笔记非常详细完整。课程现在已经下架了,我准备将资料分享出来给大家免费学习,相信大家看完一定会有所收获。 版权申明:该资料的版权属于 优快云 和作者,仅给读者学习使用,未经允许,不得将其用于商业用途和其他任何形式的不正当用途,...

2万+

C# WinForm 基于DirectShow.dll编写的程序,可边预览边保存视频,音频和图片

基于微软的DirectShow.dll的二次开发1.自动识别连接的摄像头跟麦克风且可以对其进行选择(默认选中第一个)2.可录制wmv和avi两种格式的视频并且都带有音频,avi可以录制的分辨率和帧数进行选择,wmv默认进行了淸晰度优化如不喜欢可去除代码中有相关提示3.可在预览点开之后进行拍照,也可边录制视频边拍照4.添加了视频和音频的压缩功能(酌情使用)这次二次开发发现C#对DirectShow的相关信息太少,这次做完也不敢藏私共享出来,还望大家指正

SpringBoot-3:集成SLF4J+logback进行日志记录

SLF4J+logback进行日志记录1 yml配置文件2 logback.xml常用配置详解2.1 根节点(configuration)2.1.1 读取配置文件:springProperty2.1.2 设置上下文名称:contextName2.1.3 设置变量:property2.2 appender节点2.2.1 控制台输出2.2.1 rollingPolicy2.2.2 filter2.3 ...

云深不知处 2万+

老版QQ原生表情gif动态

这是老版的QQ表情,不过全是动态的的,Windows照片查看器是不会动的,可以右击用浏览器打开查看,再说一遍是老版的,只是分享给宁愿用老版也要动态的人。

迈瑞BS系列全自动生化分析仪LIS协议接口手册(内含HL7和ASTM)

迈瑞BS系列全自动生化分析仪LIS协议接口手册,内含HL7和ASTM)

七万字速通SpringBoot

REST(Representational State Transfer),表现形式转换YAML(YAML Ain't Markup Language),一种数据序列化格式容易阅读容易与脚本语言交互以数据为核心,重数据轻格式YANL文件扩展名.yml(主流).yaml几种数据格式比较application.yaml配置文件中的信息#创建类,用于封装下面的数据#有spring带我们去加载数据到对象中,且告诉spring加载这组信息#使用时从spring中直接获取信息使用。

weixin_60719453的博客 5241

SpringBoot3—核心特性:快速入门

SpringBoot3—核心特性:快速入门

weixin_74019079的博客 1382

SpringBoot+logback优雅的配置日志!

SpringBoot+logback优雅地配置日志! 前言 今天博主将为大家分享SpringBoot+logback优雅地配置日志!不喜勿喷,如有异议欢迎讨论! SpringBoot中如何通过sl4j日志组件优雅地记录日志。其实,我们入门 JAVA 的第一行代码就是一行日志,那你现在还在使用System.out.println(“Hello,baby!”)记录日志吗? 日志组件 我最开始接触...

Mr_chen的博客 2万+

【SpringBoot】springboot日志配置

主要介绍了下springboot中的日志记录

Black_Customer的博客 2万+

SpringBoot日志体系全解析:Logback配置与异步日志优化

日志在应用程序开发和运维过程中起着至关重要的作用。它可以帮助开发人员快速定位和解决问题,记录系统的运行状态和业务流程,还可以用于性能分析和安全审计等。一个好的日志体系可以大大提高开发和运维的效率。

梵心白莲的博客 1090

SpringBoot 日志深度解析:轻松上手,快速精通!

在 Spring Boot 中可以自定义实现日志打印器级别的热更新。1、实现思路创建一个自定义的配置类,用于监听外部的配置变化事件,比如通过 HTTP 请求或者特定的配置文件变化来触发日志级别更新。在配置类中,使用 Spring 的注解来绑定外部配置源的属性到 Java 对象中,以便获取新的日志级别设置。利用 Spring 的发布一个自定义的事件,通知应用程序中的其他组件日志级别发生了变化。创建一个事件监听器,在接收到自定义事件时,更新日志打印器的级别。

Yaml4的博客 1228

SpringBoot 日志 快速使用

Windows下 SpringBoot日志 快速使用

东边的山坡的博客 1261

快速搭建SpringBoot3.x项目

上一小节中我们从0到1 使用Vite搭建了一个Vue3项目,并集成了Element Plus 实现了一个简单的增删改查页面。这一篇中我们将使用IDEA快速搭建一个SpringBoot3.x的项目。

再小的帆也能远航,把分享变成一种习惯 2793

SpringBoot日志全解析:从调试到持久化

我们发现 debug 和 trace 级别的日志没有被打印---这与⽇志级别的配置有关,⽇志的输出级别默认是 info级别(下面的内容中会讲到如何配置), 所以只会打印⼤于等于此级别的⽇志,也就 是 info , warn 和 error.我们刷新后就可以发现在logger这个目录下出现了 Spring.log (日志文件的默认名称)这个日志文件 (注意: logging.file.path: 只能设置日志文件的路径,不能设置文件名称)(每个类都可以获取属于自己的独立日志对象)

2401_89176842的博客 578
上一篇: 高薪热门专业!计算机类就业方向深度解读,你想知道的都在这里!
下一篇: 基于SpringBoot的社区养老服务系统(源码+L文+说明文档)
JavaASDN
博客等级 码龄2年 236粉丝 31原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值