精尽 Spring Boot 源码分析 —— 日志系统

1. 概述

在使用 Spring Boot 时,默认就已经提供了日志功能,使用 Logback 作为默认的日志框架。本文,我们就来一起研究下,Spring Boot 是如何自动初始化好日志系统的。

不了解 Spring Boot 日志功能的胖友,可以先看看 《一起来学 SpringBoot 2.x | 第三篇:SpringBoot 日志配置》 文章。

2. LoggingApplicationListener

Spring Boot 提供日志功能,关键在于 LoggingApplicationListener 类。在 《精尽 Spring Boot 源码分析 —— ApplicationListener》 中,我们已经简单介绍过它:

org.springframework.boot.context.logging.LoggingApplicationListener ,实现 GenericApplicationListener 接口,实现根据配置初始化日志系统 Logger 。

2.1 supportsEventType

实现 #supportsEventType(ResolvableType resolvableType) 方法,判断是否是支持的事件类型。代码如下:

// LoggingApplicationListener.javaprivate static final Class<?>[] EVENT_TYPES = { ApplicationStartingEvent.class,ApplicationEnvironmentPreparedEvent.class, ApplicationPreparedEvent.class,ContextClosedEvent.class, ApplicationFailedEvent.class };@Override
public boolean supportsEventType(ResolvableType resolvableType) {return isAssignableFrom(resolvableType.getRawClass(), EVENT_TYPES);
}private boolean isAssignableFrom(Class<?> type, Class<?>... supportedTypes) {if (type != null) {for (Class<?> supportedType : supportedTypes) {if (supportedType.isAssignableFrom(type)) {return true;}}}return false;
}

2.2 supportsSourceType

实现 #supportsSourceType(Class<?> sourceType) 方法,判断是否是支持的事件来源。代码如下:

// LoggingApplicationListener.javaprivate static final Class<?>[] SOURCE_TYPES = { SpringApplication.class,ApplicationContext.class };@Override
public boolean supportsSourceType(Class<?> sourceType) {return isAssignableFrom(sourceType, SOURCE_TYPES);
}

2.3 onApplicationEvent

实现 #onApplicationEvent(ApplicationEvent event) 方法,处理事件。代码如下:

// LoggingApplicationListener.java@Override
public void onApplicationEvent(ApplicationEvent event) {// 在 Spring Boot 应用启动的时候if (event instanceof ApplicationStartingEvent) {onApplicationStartingEvent((ApplicationStartingEvent) event);// 在 Spring Boot 的 Environment 环境准备完成的时候} else if (event instanceof ApplicationEnvironmentPreparedEvent) {onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event);// 在 Spring Boot 容器的准备工作已经完成(并未启动)的时候} else if (event instanceof ApplicationPreparedEvent) {onApplicationPreparedEvent((ApplicationPreparedEvent) event);// 在 Spring Boot 容器关闭的时候} else if (event instanceof ContextClosedEvent&& ((ContextClosedEvent) event).getApplicationContext().getParent() == null) {onContextClosedEvent();// 在 Spring Boot 容器启动失败的时候} else if (event instanceof ApplicationFailedEvent) {onApplicationFailedEvent();}
}
  • 不同的事件,对应不同的处理方法。下文,我们一一来看。

2.4 onApplicationStartingEvent

#onApplicationStartingEvent(ApplicationStartingEvent event) 方法,代码如下:

// LoggingApplicationListener.javaprivate LoggingSystem loggingSystem;private void onApplicationStartingEvent(ApplicationStartingEvent event) {// <1> 创建 LoggingSystem 对象this.loggingSystem = LoggingSystem.get(event.getSpringApplication().getClassLoader());// <2> LoggingSystem 的初始化的前置处理this.loggingSystem.beforeInitialize();
}
  • <1> 处,调用 LoggingSystem#get(ClassLoader classLoader) 方法,创建(获得) LoggingSystem 对象。关于这个,可以先看看 「3.1 get」 小节。
    • 通过 LoggingSystem 的抽象,对应不同日志框架对应的 LoggingSystem 实现,达到方便透明的接入不同的日志框架~
  • <2> 处,调用 LoggingSystem#beforeInitialize() 方法,执行 LoggingSystem 的初始化的前置处理。关于这个,可以先看看 「3.2 beforeInitialize」 小节。

2.5 onApplicationEnvironmentPreparedEvent

#onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) 方法,代码如下:

// LoggingApplicationListener.javaprivate 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) {// <1> 初始化 LoggingSystemProperties 配置new LoggingSystemProperties(environment).apply();// <2> 初始化 LogFileLogFile logFile = LogFile.get(environment);if (logFile != null) {logFile.applyToSystemProperties(); // <2.1>}// <3> 初始化早期的 Spring Boot Logging 级别initializeEarlyLoggingLevel(environment);// <4> 初始化 LoggingSystem 日志系统initializeSystem(environment, this.loggingSystem, logFile);// <5> 初始化最终的 Spring Boot Logging 级别initializeFinalLoggingLevels(environment, this.loggingSystem);// <6>  registerShutdownHookIfNecessary(environment, this.loggingSystem);
}
  • <1> 处,调用 LoggingSystemProperties#apply() 方法,初始化 LoggingSystemProperties 配置。关于这个,可以先看看 「4. LoggingSystemProperties」 小节。
  • <2> 处,调用 LogFile#get(environment) 方法,创建(获得)LogFile 。关于这个,可以先看看 「5. LogFile」 小节。
    • <2.1> 处,调用 LogFile#applyToSystemProperties() 方法,应用 LogFile.path 和 LogFile.file 到系统属性中。
  • <3> 处,调用 #initializeEarlyLoggingLevel(ConfigurableEnvironment environment) 方法,初始化早期的 Spring Boot Logging 级别。详细解析,见 「2.5.1 initializeEarlyLoggingLevel」 中。
  • <4> 处,调用 #initializeSystem(ConfigurableEnvironment environment, LoggingSystem system, LogFile logFile) 方法,初始化 LoggingSystem 日志系统。详细解析,见 「2.5.2 initializeSystem」 中。
  • <5> 处,调用 #initializeFinalLoggingLevels(ConfigurableEnvironment environment, LoggingSystem system) 方法,初始化最终的 Spring Boot Logging 级别。详细解析,见 「2.5.3 initializeFinalLoggingLevels」 中。
  • <6> 处,调用 #registerShutdownHookIfNecessary(Environment environment, LoggingSystem loggingSystem) 方法,注册 ShutdownHook 。详细解析,见 「2.5.4」 中。

2.5.1 initializeEarlyLoggingLevel

#initializeEarlyLoggingLevel(ConfigurableEnvironment environment) 方法,初始化早期的 Spring Boot Logging 级别。代码如下:

// LoggingApplicationListener.javaprivate boolean parseArgs = true;private LogLevel springBootLogging = null;private void initializeEarlyLoggingLevel(ConfigurableEnvironment environment) {if (this.parseArgs && this.springBootLogging == null) {if (isSet(environment, "debug")) {this.springBootLogging = LogLevel.DEBUG;}if (isSet(environment, "trace")) {this.springBootLogging = LogLevel.TRACE;}}
}private boolean isSet(ConfigurableEnvironment environment, String property) {String value = environment.getProperty(property);return (value != null && !value.equals("false"));
}
  • 可以通过在启动 jar 的时候,跟上 --debug 或 --trace 。
  • 也可以在配置文件中,添加 debug=true 或 trace=true 。
  • 关于日志级别,可以先看看 「6. LogLevel」 。

2.5.2 initializeSystem

#initializeSystem(ConfigurableEnvironment environment, LoggingSystem system, LogFile logFile) 方法,初始化 LoggingSystem 日志系统。代码如下:

// LoggingApplicationListener.javapublic static final String CONFIG_PROPERTY = "logging.config";private void initializeSystem(ConfigurableEnvironment environment, LoggingSystem system, LogFile logFile) {// <1> 创建 LoggingInitializationContext 对象LoggingInitializationContext initializationContext = new LoggingInitializationContext(environment);// <2> 获得日志组件的配置文件String logConfig = environment.getProperty(CONFIG_PROPERTY);// <3> 如果没配置,则直接初始化 LoggingSystemif (ignoreLogConfig(logConfig)) {system.initialize(initializationContext, null, logFile);// <3> 如果有配置,先尝试加载指定配置文件,然后在初始化 LoggingSystem} else {try {ResourceUtils.getURL(logConfig).openStream().close();system.initialize(initializationContext, logConfig, logFile); // <X>} catch (Exception ex) {// NOTE: We can't use the logger here to report the problemSystem.err.println("Logging system failed to initialize " + "using configuration from '" + logConfig + "'");ex.printStackTrace(System.err);throw new IllegalStateException(ex);}}
}
  • <1> 处,创建 LoggingInitializationContext 对象。其中,org.springframework.boot.logging.LoggingInitializationContext ,LoggingSystem 初始化时的 Context 。代码如下:

    // LoggingInitializationContext.javapublic class LoggingInitializationContext {private final ConfigurableEnvironment environment;public LoggingInitializationContext(ConfigurableEnvironment environment) {this.environment = environment;}public Environment getEnvironment() {return this.environment;}}
    
    • 虽然目前只有 environment 属性。但是未来可以在后面增加新的参数,而无需改动 LoggingSystem#initialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) 方法。
  • <2> 处,从 environment 中获得 "logging.config" ,即获得日志组件的配置文件。一般情况下,我们无需配置。因为根据不同的日志系统,Spring Boot 按如下“约定规则”组织配置文件名加载日志配置文件:

日志框架配置文件
Logbacklogback-spring.xml, logback-spring.groovy, logback.xml, logback.groovy
Log4jlog4j-spring.properties, log4j-spring.xml, log4j.properties, log4j.xml
Log4j2log4j2-spring.xml, log4j2.xml
JDK (Java Util Logging)logging.properties
  • <3> 和 <4> 处,调用 LoggingSystem#initialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) 方法,初始化 LoggingSystem 日志系统。详细解析,可以先看看 「3.3 initialize」 。
  • <3> 和 <4> 处,差异点在于后者多了 ResourceUtils.getURL(logConfig).openStream().close() 代码块,看着有点奇怪哟?它的作用是,尝试去加载 logConfig 对应的配置文件,看看是否真的存在~

2.5.3 initializeFinalLoggingLevels

#initializeFinalLoggingLevels(ConfigurableEnvironment environment, LoggingSystem system) 方法,初始化最终的 Spring Boot Logging 级别。代码如下:

// LoggingApplicationListener.javaprivate void initializeFinalLoggingLevels(ConfigurableEnvironment environment, LoggingSystem system) {// <1> 如果 springBootLogging 非空,则设置到日志级别if (this.springBootLogging != null) {initializeLogLevel(system, this.springBootLogging);}// <2> 设置 environment 中配置的日志级别setLogLevels(system, environment);
}
  • <1> 处,如果 springBootLogging 非空,则调用 #initializeLogLevel(LoggingSystem system, LogLevel level) 方法,设置日志级别。代码如下:

    // LoggingApplicationListener.javaprivate static final Map<LogLevel, List<String>> LOG_LEVEL_LOGGERS;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");LOG_LEVEL_LOGGERS = Collections.unmodifiableMap(loggers);
    }protected void initializeLogLevel(LoggingSystem system, LogLevel level) {List<String> loggers = LOG_LEVEL_LOGGERS.get(level);if (loggers != null) {for (String logger : loggers) {system.setLogLevel(logger, level);}}
    }
    
    • 遍历的 loggers ,是 LOG_LEVEL_LOGGERS 中对应的 level 的值。
    • 调用 LoggingSystem#setLogLevel(String loggerName, LogLevel level) 方法,设置指定 loggerName 的日志级别。详细解析,见 「3.4 setLogLevel」 。
  • <2> 处,调用 #setLogLevels(LoggingSystem system, Environment environment) 方法,设置 environment 中配置的日志级别。代码如下:

    // LoggingApplicationListener.javaprivate static final ConfigurationPropertyName LOGGING_LEVEL = ConfigurationPropertyName.of("logging.level");
    private static final ConfigurationPropertyName LOGGING_GROUP = ConfigurationPropertyName.of("logging.group");private static final Bindable<Map<String, String>> STRING_STRING_MAP = Bindable.mapOf(String.class, String.class);
    private static final Bindable<Map<String, String[]>> STRING_STRINGS_MAP = Bindable.mapOf(String.class, String[].class);protected void setLogLevels(LoggingSystem system, Environment environment) {if (!(environment instanceof ConfigurableEnvironment)) {return;}// 创建 Binder 对象Binder binder = Binder.get(environment);// <1> 获得日志分组的集合Map<String, String[]> groups = getGroups(); // <1.1>binder.bind(LOGGING_GROUP, STRING_STRINGS_MAP.withExistingValue(groups)); // <1.2>// <2> 获得日志级别的集合Map<String, String> levels = binder.bind(LOGGING_LEVEL, STRING_STRING_MAP).orElseGet(Collections::emptyMap);// <3> 遍历 levels 集合,逐个设置日志级别levels.forEach((name, level) -> {String[] groupedNames = groups.get(name);if (ObjectUtils.isEmpty(groupedNames)) {setLogLevel(system, name, level);} else {setLogLevel(system, groupedNames, level);}});
    }
    
    • <1> 处,获得日志分组的集合。

      • <1.1> 处,调用 #getGroups() 方法,获得默认的日志分组集合。代码如下:

        // LoggingApplicationListener.javaprivate static final Map<String, List<String>> DEFAULT_GROUP_LOGGERS;
        static {MultiValueMap<String, String> loggers = new LinkedMultiValueMap<>();loggers.add("web", "org.springframework.core.codec");loggers.add("web", "org.springframework.http");loggers.add("web", "org.springframework.web");loggers.add("web", "org.springframework.boot.actuate.endpoint.web");loggers.add("web", "org.springframework.boot.web.servlet.ServletContextInitializerBeans");loggers.add("sql", "org.springframework.jdbc.core");loggers.add("sql", "org.hibernate.SQL");DEFAULT_GROUP_LOGGERS = Collections.unmodifiableMap(loggers);
        }private Map<String, String[]> getGroups() {Map<String, String[]> groups = new LinkedHashMap<>();DEFAULT_GROUP_LOGGERS.forEach((name, loggers) -> groups.put(name, StringUtils.toStringArray(loggers)));return groups;
        }
        
        • 实际上,就是把我们日常配置的 loggerName 进行了分组。默认情况下,内置了 sqlweb 分组。
      • <1.2> 处,从 environment 中读取 logging.group 配置的日志分组。举个例子,在配置文件里增加 logging.group.demo=xxx.Dog,yyy.Cat 。
    • <2> 处,从 environment 中读取 logging.level 配置的日志分组。举两个例子,在配置文件里添加:

      • logging.level.web=INFO
      • logging.level.xxx.Dog=INFO
    • <3> 处,遍历 levels 集合,逐个设置日志级别。涉及的方法,代码如下:

      // LoggingApplicationListener.javaprivate void setLogLevel(LoggingSystem system, String[] names, String level) {// 遍历 names 数组for (String name : names) {setLogLevel(system, name, level);}
      }private void setLogLevel(LoggingSystem system, String name, String level) {try {// 获得 loggerNamename = name.equalsIgnoreCase(LoggingSystem.ROOT_LOGGER_NAME) ? null : name;// 设置日志级别system.setLogLevel(name, coerceLogLevel(level));} catch (RuntimeException ex) {this.logger.error("Cannot set level '" + level + "' for '" + name + "'");}
      }/*** @param level 日志级别字符串* @return 将字符串转换成 {@link LogLevel}*/
      private LogLevel coerceLogLevel(String level) {String trimmedLevel = level.trim();if ("false".equalsIgnoreCase(trimmedLevel)) { // false => OFFreturn LogLevel.OFF;}return LogLevel.valueOf(trimmedLevel.toUpperCase(Locale.ENGLISH));
      }
      
      • 比较简单,胖友瞅瞅~

2.5.4 registerShutdownHookIfNecessary

#registerShutdownHookIfNecessary(Environment environment, LoggingSystem loggingSystem) 方法,注册 ShutdownHook 。代码如下:

// LoggingApplicationListener.java/*** The name of the Spring property that controls the registration of a shutdown hook* to shut down the logging system when the JVM exits.* @see LoggingSystem#getShutdownHandler*/
public static final String REGISTER_SHUTDOWN_HOOK_PROPERTY = "logging.register-shutdown-hook";private void registerShutdownHookIfNecessary(Environment environment, LoggingSystem loggingSystem) {// 获得 logging.register-shutdown-hook 对应的配置值boolean registerShutdownHook = environment.getProperty(REGISTER_SHUTDOWN_HOOK_PROPERTY, Boolean.class, false);// 如果开启if (registerShutdownHook) {// <x> 获得 shutdownHandler 钩子Runnable shutdownHandler = loggingSystem.getShutdownHandler();// 注册 ShutdownHook(if (shutdownHandler != null&& shutdownHookRegistered.compareAndSet(false, true)) {registerShutdownHook(new Thread(shutdownHandler));}}
}void registerShutdownHook(Thread shutdownHook) {Runtime.getRuntime().addShutdownHook(shutdownHook);
}
  • <X> 处,所注册的 ShutdownHook ,通过调用 LoggingSystem#getShutdownHandler() 方法,进行获得。详细解析,见 「3.5 getShutdownHandler」。

2.6 onApplicationPreparedEvent

#onApplicationPreparedEvent(ApplicationPreparedEvent event) 方法,代码如下:

// LoggingApplicationListener.java/*** The name of the {@link LoggingSystem} bean.*/
public static final String LOGGING_SYSTEM_BEAN_NAME = "springBootLoggingSystem";private void onApplicationPreparedEvent(ApplicationPreparedEvent event) {ConfigurableListableBeanFactory beanFactory = event.getApplicationContext().getBeanFactory();if (!beanFactory.containsBean(LOGGING_SYSTEM_BEAN_NAME)) {beanFactory.registerSingleton(LOGGING_SYSTEM_BEAN_NAME, this.loggingSystem);}
}
  • 将创建的 LoggingSystem 对象,注册到 Spring 容器中。

2.7 onContextClosedEvent

#onContextClosedEvent() 方法,代码如下:

// LoggingApplicationListener.javaprivate void onContextClosedEvent() {if (this.loggingSystem != null) {this.loggingSystem.cleanUp();}
}
  • 调用 LoggingSystem#cleanUp() 方法,执行清理。详细解析,见 「3.6 cleanUp」 中。

2.8 onApplicationFailedEvent

#onApplicationFailedEvent() 方法,代码如下:

// LoggingApplicationListener.javaprivate void onApplicationFailedEvent() {if (this.loggingSystem != null) {this.loggingSystem.cleanUp();}
}

至此,我们需要来看看 LoggingSystem 的实现类。具体的,可以跳到 「7. LoggingSystem 的实现类」 中。

3. LoggingSystem

org.springframework.boot.logging.LoggingSystem ,日志系统抽象类。每个日志框架,都会对应一个实现类。如下图所示:LoggingSystem 实现类

3.1 get

#get(ClassLoader classLoader) 方法,创建(获得) LoggingSystem 对象。代码如下:

// LoggingApplicationListener.java/*** A System property that can be used to indicate the {@link LoggingSystem} to use.*/
public static final String SYSTEM_PROPERTY = LoggingSystem.class.getName();/*** The value of the {@link #SYSTEM_PROPERTY} that can be used to indicate that no* {@link LoggingSystem} should be used.*/
public static final String NONE = "none";private static final Map<String, String> SYSTEMS;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) {// <1> 从系统参数 org.springframework.boot.logging.LoggingSystem 获得 loggingSystem 类型String loggingSystem = System.getProperty(SYSTEM_PROPERTY);// <2> 如果非空,说明配置了if (StringUtils.hasLength(loggingSystem)) {// <2.1> 如果是 none ,则创建 NoOpLoggingSystem 对象if (NONE.equals(loggingSystem)) {return new NoOpLoggingSystem();}// <2.2> 获得 loggingSystem 对应的 LoggingSystem 类,进行创建对象return get(classLoader, loggingSystem);}// <3> 如果为空,说明未配置,则顺序查找 SYSTEMS 中的类。如果存在指定类,则创建该类。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"));
}
  • <1> 处,从系统参数 org.springframework.boot.logging.LoggingSystem 获得 loggingSystem 类型。
  • <2> 处,如果非空,说明配置了。

    • <2.1> 处,如果是 none ,则创建 NoOpLoggingSystem 对象。
    • <2.2> 处,调用 #get(ClassLoader classLoader, String loggingSystemClass) 方法,获得 loggingSystem 对应的 LoggingSystem 类,进行创建对象。代码如下:

      // LoggingSystem.javaprivate static LoggingSystem get(ClassLoader classLoader, String loggingSystemClass) {try {Class<?> systemClass = ClassUtils.forName(loggingSystemClass, classLoader);return (LoggingSystem) systemClass.getConstructor(ClassLoader.class).newInstance(classLoader);} catch (Exception ex) {throw new IllegalStateException(ex);}
      }
      
      • systemClass 中的 VALUES ,就是 loggingSystem 对应的类。
  • <3> 处,如果为空,说明未配置,则顺序查找 SYSTEMS 中的类。如果存在指定类,则创建该类。

3.2 beforeInitialize

#beforeInitialize() 抽象方法,初始化的前置方法。代码如下:

// LoggingSystem.java/*** Reset the logging system to be limit output. This method may be called before* {@link #initialize(LoggingInitializationContext, String, LogFile)} to reduce* logging noise until the system has been fully initialized.*/
public abstract void beforeInitialize();

3.3 initialize

#initialize() 方法,初始化。代码如下:

// LoggingSystem.java/*** Fully initialize the logging system.* @param initializationContext the logging initialization context* @param configLocation a log configuration location or {@code null} if default* initialization is required* @param logFile the log output file that should be written or {@code null} for* console only output*/
public void initialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) {
}
  • 目前是个空方法,需要子类来实现。
  • 我们先不着急看子类的实现,等后面继续看。

3.4 setLogLevel

#setLogLevel(String loggerName, LogLevel level) 方法,设置指定 loggerName 的日志级别。代码如下:

// LoggingSystem.java/*** Sets the logging level for a given logger.* @param loggerName the name of the logger to set ({@code null} can be used for the* root logger).* @param level the log level ({@code null} can be used to remove any custom level for* the logger and use the default configuration instead)*/
public void setLogLevel(String loggerName, LogLevel level) {throw new UnsupportedOperationException("Unable to set log level");
}
  • 目前是个空方法,需要子类来实现。
  • 我们先不着急看子类的实现,等后面继续看。

3.5 getShutdownHandler

#getShutdownHandler() 方法,获得 ShutdownHook 的 Runnable 对象。代码如下:

// LoggingSystem.java/*** Returns a {@link Runnable} that can handle shutdown of this logging system when the* JVM exits. The default implementation returns {@code null}, indicating that no* shutdown is required.* @return the shutdown handler, or {@code null}*/
public Runnable getShutdownHandler() {return null;
}
  • 目前是个空方法,需要子类来实现。
  • 我们先不着急看子类的实现,等后面继续看。

3.6 cleanUp

#cleanUp() 方法,清理。代码如下:

// LoggingSystem.java/*** Clean up the logging system. The default implementation does nothing. Subclasses* should override this method to perform any logging system-specific cleanup.*/
public void cleanUp() {
}
  • 目前是个空方法,需要子类来实现。
  • 我们先不着急看子类的实现,等后面继续看。

4. LoggingSystemProperties

org.springframework.boot.logging.LoggingSystemProperties ,LoggingSystem 的配置类。

4.1 构造方法

// LoggingSystemProperties.javaprivate final Environment environment;public LoggingSystemProperties(Environment environment) {Assert.notNull(environment, "Environment must not be null");this.environment = environment;
}

4.2 apply

#apply() 方法,解析 environment 的配置变量到系统属性中。代码如下:

// LoggingSystemProperties.javapublic void apply() {apply(null);
}public void apply(LogFile logFile) {// <1> 获得 PropertyResolver 对象PropertyResolver resolver = getPropertyResolver();// <2> 解析配置文件到系统属性中setSystemProperty(resolver, EXCEPTION_CONVERSION_WORD, "exception-conversion-word");setSystemProperty(PID_KEY, new ApplicationPid().toString()); // 应用进程编号setSystemProperty(resolver, CONSOLE_LOG_PATTERN, "pattern.console");setSystemProperty(resolver, FILE_LOG_PATTERN, "pattern.file");setSystemProperty(resolver, FILE_MAX_HISTORY, "file.max-history");setSystemProperty(resolver, FILE_MAX_SIZE, "file.max-size");setSystemProperty(resolver, LOG_LEVEL_PATTERN, "pattern.level");setSystemProperty(resolver, LOG_DATEFORMAT_PATTERN, "pattern.dateformat");// <3> 如果 logFile 非空,则应用配置if (logFile != null) {logFile.applyToSystemProperties();}
}
  • <1> 处,调用 #getPropertyResolver() 方法,获得 PropertyResolver 对象。代码如下:
    // LoggingSystemProperties.javaprivate PropertyResolver getPropertyResolver() {if (this.environment instanceof ConfigurableEnvironment) {PropertySourcesPropertyResolver resolver = new PropertySourcesPropertyResolver(((ConfigurableEnvironment) this.environment).getPropertySources());resolver.setIgnoreUnresolvableNestedPlaceholders(true);return resolver;}return this.environment;
    }
    
  • <2> 处,调用 #setSystemProperty(PropertyResolver resolver, String systemPropertyName, String propertyName) 方法,解析配置文件到系统属性中。代码如下:

    // LoggingSystemProperties.javapublic static final String PID_KEY = "PID";
    public static final String EXCEPTION_CONVERSION_WORD = "LOG_EXCEPTION_CONVERSION_WORD";
    public static final String LOG_FILE = "LOG_FILE";
    public static final String LOG_PATH = "LOG_PATH";
    public static final String FILE_LOG_PATTERN = "FILE_LOG_PATTERN";
    public static final String FILE_MAX_HISTORY = "LOG_FILE_MAX_HISTORY";
    public static final String FILE_MAX_SIZE = "LOG_FILE_MAX_SIZE";
    public static final String LOG_LEVEL_PATTERN = "LOG_LEVEL_PATTERN";
    public static final String LOG_DATEFORMAT_PATTERN = "LOG_DATEFORMAT_PATTERN";private void setSystemProperty(PropertyResolver resolver, String systemPropertyName, String propertyName) {setSystemProperty(systemPropertyName, resolver.getProperty("logging." + propertyName)); // <X>
    }private void setSystemProperty(String name, String value) {if (System.getProperty(name) == null && value != null) {System.setProperty(name, value);}
    }
    
    • <X> 处,读取的是 environment 中的 logging. 开头的配置属性。

5. LogFile

org.springframework.boot.logging.LogFile ,日志文件。

5.1 构造方法

// LogFile.java/*** 文件名*/
private final String file;
/*** 文件路径*/
private final String path;

5.2 applyToSystemProperties

#applyToSystemProperties() 方法,应用 filepath 到系统属性。代码如下:

// LogFile.javapublic static final String FILE_NAME_PROPERTY = "logging.file.name";
public static final String FILE_PATH_PROPERTY = "logging.file.path";public void applyToSystemProperties() {applyTo(System.getProperties());
}public void applyTo(Properties properties) {put(properties, LoggingSystemProperties.LOG_PATH, this.path);put(properties, LoggingSystemProperties.LOG_FILE, toString());
}
  • #toString() 方法,返回文件名。代码如下:
    // LogFile.java@Override
    public String toString() {if (StringUtils.hasLength(this.file)) {return this.file;}return new File(this.path, "spring.log").getPath();
    }
    
  • #put(Properties properties, String key, String value) 方法,添加属性值到系统属性。代码如下:
    // LogFile.javaprivate void put(Properties properties, String key, String value) {if (StringUtils.hasLength(value)) {properties.put(key, value);}
    }
    

5.3 get

#get(PropertyResolver propertyResolver) 方法,获得(创建)LogFile 对象。代码如下:

// LogFile.javapublic static LogFile get(PropertyResolver propertyResolver) {// <1> 获得 file 和 path 属性String file = getLogFileProperty(propertyResolver, FILE_NAME_PROPERTY, FILE_PROPERTY);String path = getLogFileProperty(propertyResolver, FILE_PATH_PROPERTY, PATH_PROPERTY);// <2> 创建 LogFile 对象if (StringUtils.hasLength(file) || StringUtils.hasLength(path)) {return new LogFile(file, path);}return null;
}
  • <1> 处,调用 #getLogFileProperty(PropertyResolver propertyResolver, String propertyName, String deprecatedPropertyName) 方法,获得 file 和 path 属性。代码如下:
    // LogFile.javaprivate static String getLogFileProperty(PropertyResolver propertyResolver, String propertyName, String deprecatedPropertyName) {String property = propertyResolver.getProperty(propertyName);if (property != null) {return property;}return propertyResolver.getProperty(deprecatedPropertyName);
    }
    
  • <2> 处,创建 LogFile 对象。

6. LogLevel

org.springframework.boot.logging.LogLevel ,Spring Boot 日志枚举类。代码如下:

// LogLevel.javapublic enum LogLevel {TRACE, DEBUG, INFO, WARN, ERROR, FATAL, OFF}

每个日志框架,都有其日志级别。通过 LogLevel 枚举类,和它们映射。

7. LoggingSystem 的实现类

7.1 NoOpLoggingSystem

NoOpLoggingSystem ,是 LoggingSystem 的内部静态类,继承 LoggingSystem 类,空操作的 LoggingSystem 实现类,用于禁用日志系统的时候。代码如下:

// LoggingSystem#NoOpLoggingSystem.javastatic class NoOpLoggingSystem extends LoggingSystem {@Overridepublic void beforeInitialize() {}@Overridepublic void setLogLevel(String loggerName, LogLevel level) {}@Overridepublic List<LoggerConfiguration> getLoggerConfigurations() {return Collections.emptyList();}@Overridepublic LoggerConfiguration getLoggerConfiguration(String loggerName) {return null;}}

7.2 AbstractLoggingSystem

org.springframework.boot.logging.AbstractLoggingSystem ,继承 LoggingSystem 抽象类,是 LoggingSystem 的抽象基类。

7.2.1 构造方法

// AbstractLoggingSystem.javaprivate final ClassLoader classLoader;public AbstractLoggingSystem(ClassLoader classLoader) {this.classLoader = classLoader;
}

7.2.2 initialize

实现 #initialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) 方法,提供模板化的初始化逻辑。代码如下:

// AbstractLoggingSystem.java@Override
public void initialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) {// <1> 有自定义的配置文件,则使用指定配置文件进行初始化if (StringUtils.hasLength(configLocation)) {initializeWithSpecificConfig(initializationContext, configLocation, logFile);return;}// <2> 无自定义的配置文件,则使用约定配置文件进行初始化initializeWithConventions(initializationContext, logFile);
}
  • <1> 处,有自定义的配置文件,则调用 #initializeWithSpecificConfig(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) 方法,使用指定配置文件进行初始化。详细解析,见 「7.2.2.1 initializeWithSpecificConfig」 。
  • <2> 处,无自定义的配置文件,则调用 #initializeWithConventions(LoggingInitializationContext initializationContext, LogFile logFile) 方法,使用约定配置文件进行初始化。详细解析,见 7.2.2.2 initializeWithConventions 。

7.2.2.1 initializeWithSpecificConfig

#initializeWithSpecificConfig(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) 方法,使用指定配置文件进行初始化。代码如下:

// AbstractLoggingSystem.javaprivate void initializeWithSpecificConfig(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) {// <1> 获得配置文件(可能有占位符)configLocation = SystemPropertyUtils.resolvePlaceholders(configLocation);// <2> 加载配置文件loadConfiguration(initializationContext, configLocation, logFile);
}
  • <1> 处,获得配置文件(可能有占位符)。
  • <2> 处,调用 #loadConfiguration(LoggingInitializationContext initializationContext, String location, LogFile logFile)) 抽象方法,加载配置文件。代码如下:
    // AbstractLoggingSystem.java/*** Load a specific configuration.* @param initializationContext the logging initialization context* @param location the location of the configuration to load (never {@code null})* @param logFile the file to load or {@code null} if no log file is to be written*/
    protected abstract void loadConfiguration(LoggingInitializationContext initializationContext, String location, LogFile logFile);
    

7.2.2.2 initializeWithConventions

#initializeWithConventions(LoggingInitializationContext initializationContext, LogFile logFile) 方法,使用约定配置文件进行初始化。代码如下:

// AbstractLoggingSystem.javaprivate void initializeWithConventions(LoggingInitializationContext initializationContext, LogFile logFile) {// <1> 获得约定配置文件String config = getSelfInitializationConfig();// <2> 如果获取到,结果 logFile 为空,则重新初始化if (config != null && logFile == null) {// self initialization has occurred, reinitialize in case of property changesreinitialize(initializationContext);return;}// <3> 如果获取不到,则尝试获得约定配置文件(带 spring 后缀)if (config == null) {config = getSpringInitializationConfig();}// <4> 如果获取到,则加载配置文件if (config != null) {loadConfiguration(initializationContext, config, logFile);return;}// <5> 如果获取不到,则加载默认配置loadDefaults(initializationContext, logFile);
}
  • <1> 处,调用 #getSelfInitializationConfig() 方法,获得约定配置文件。代码如下:

    // AbstractLoggingSystem.javaprotected String getSelfInitializationConfig() {return findConfig(getStandardConfigLocations());
    }protected abstract String[] getStandardConfigLocations();private String findConfig(String[] locations) {// 遍历 locations 数组,逐个判断是否存在。若存在,则返回for (String location : locations) {ClassPathResource resource = new ClassPathResource(location, this.classLoader);if (resource.exists()) {return "classpath:" + location;}}return null;
    }
    
    • #getStandardConfigLocations() 抽象方法,获得约定的配置文件。例如说:LogbackLoggingSystem 返回的是 "logback-test.groovy""logback-test.xml"、 "logback.groovy""logback.xml" 。
  • <2> 处,如果获取到,结果 logFile 为空,则调用 #reinitialize(LoggingInitializationContext initializationContext) 方法,重新初始化。代码如下:

    // AbstractLoggingSystem.java/*** Reinitialize the logging system if required. Called when* {@link #getSelfInitializationConfig()} is used and the log file hasn't changed. May* be used to reload configuration (for example to pick up additional System* properties).* @param initializationContext the logging initialization context*/
    protected void reinitialize(LoggingInitializationContext initializationContext) {
    }
    
    • 一般情况下,logFile 非空~
  • <3> 处,如果获取不到,则调用 #getSpringInitializationConfig() 方法,尝试获得约定配置文件(带 -spring 后缀)。代码如下:

    // AbstractLoggingSystem.java/*** Return any spring specific initialization config that should be applied. By default* this method checks {@link #getSpringConfigLocations()}.* @return the spring initialization config or {@code null}*/
    protected String getSpringInitializationConfig() {return findConfig(getSpringConfigLocations());
    }/*** Return the spring config locations for this system. By default this method returns* a set of locations based on {@link #getStandardConfigLocations()}.* @return the spring config locations* @see #getSpringInitializationConfig()*/
    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;
    }
    
    • 例如说:LogbackLoggingSystem 返回的是 "logback-test-spring.groovy""logback-test-spring.xml"、 "logback-spring.groovy""logback-spring.xml" 。
  • <4> 处,如果获取到,则调用 #loadConfiguration(LoggingInitializationContext initializationContext, String location, LogFile logFile)) 抽象方法,加载配置文件。

  • <5> 处,如果获取不到,则调用 #loadDefaults(LoggingInitializationContext initializationContext, LogFile logFile) 抽象方法,加载默认配置。代码如下:
    // AbstractLoggingSystem.java/*** Load sensible defaults for the logging system.* @param initializationContext the logging initialization context* @param logFile the file to load or {@code null} if no log file is to be written*/
    protected abstract void loadDefaults(LoggingInitializationContext initializationContext, LogFile logFile);
    

7.2.3 LogLevels

LogLevels ,是 AbstractLoggingSystem 的内部静态类,用于 Spring Boot LogLevel 和日志框架的 LogLevel 做映射。代码如下:

// AbstractLoggingSystem#LogLevels.java/*** Maintains a mapping between native levels and {@link LogLevel}.** @param <T> the native level type*/
protected static class LogLevels<T> {private final Map<LogLevel, T> systemToNative;private final Map<T, LogLevel> nativeToSystem;public LogLevels() {this.systemToNative = new EnumMap<>(LogLevel.class);this.nativeToSystem = new HashMap<>();}public void map(LogLevel system, T nativeLevel) {if (!this.systemToNative.containsKey(system)) {this.systemToNative.put(system, nativeLevel);}if (!this.nativeToSystem.containsKey(nativeLevel)) {this.nativeToSystem.put(nativeLevel, system);}}public LogLevel convertNativeToSystem(T level) {return this.nativeToSystem.get(level);}public T convertSystemToNative(LogLevel level) {return this.systemToNative.get(level);}public Set<LogLevel> getSupported() {return new LinkedHashSet<>(this.nativeToSystem.values());}}

7.3 Slf4JLoggingSystem

org.springframework.boot.logging.Slf4JLoggingSystem ,继承 AbstractLoggingSystem 抽象类,基于 Slf4J 的 LoggingSystem 的抽象基类。

7.3.1 beforeInitialize

重写 #beforeInitialize() 方法,代码如下:

// Slf4JLoggingSystem.java@Override
public void beforeInitialize() {// 父方法super.beforeInitialize();// <1> 配置 JUL 的桥接处理器configureJdkLoggingBridgeHandler();
}
  • 因为艿艿没有特别完整的了解过日志框架,所以下面的解释,更多凭的是“直觉”!如果有错误的地方,给艿艿星球留言哈~
  • <1> 处,调用 #configureJdkLoggingBridgeHandler() 方法,配置 JUL 的桥接处理器。详细解析,见 「7.3.1.1 configureJdkLoggingBridgeHandler」 。

7.3.1.1 configureJdkLoggingBridgeHandler

#configureJdkLoggingBridgeHandler() 方法,配置 JUL 的桥接处理器。代码如下:

// Slf4JLoggingSystem.javaprivate void configureJdkLoggingBridgeHandler() {try {// <1> 判断 JUL 是否桥接到 SLF4J 了if (isBridgeJulIntoSlf4j()) {// <2> 移除 JUL 桥接处理器removeJdkLoggingBridgeHandler();// <3> 重新安装 SLF4JBridgeHandlerSLF4JBridgeHandler.install();}} catch (Throwable ex) {// Ignore. No java.util.logging bridge is installed.}
}
  • <1> 处,调用 #isBridgeJulIntoSlf4j() 方法,判断 JUL 是否桥接到 SLF4J 了。代码如下:

    // Slf4JLoggingSystem.javaprivate static final String BRIDGE_HANDLER = "org.slf4j.bridge.SLF4JBridgeHandler";/*** Return whether bridging JUL into SLF4J or not.* @return whether bridging JUL into SLF4J or not* @since 2.0.4*/
    protected final boolean isBridgeJulIntoSlf4j() {return isBridgeHandlerAvailable() // 判断是否存在 SLF4JBridgeHandler 类&& isJulUsingASingleConsoleHandlerAtMost(); // 判断是否 JUL 只有 ConsoleHandler 处理器被创建了
    }protected final boolean isBridgeHandlerAvailable() {return ClassUtils.isPresent(BRIDGE_HANDLER, getClassLoader());
    }private boolean isJulUsingASingleConsoleHandlerAtMost() {Logger rootLogger = LogManager.getLogManager().getLogger("");Handler[] handlers = rootLogger.getHandlers();return handlers.length == 0|| (handlers.length == 1 && handlers[0] instanceof ConsoleHandler);
    }
    
    • 第一个方法,调用后面的两个方法判断~
  • <2> 处,调用 #removeJdkLoggingBridgeHandler() 方法,移除 JUL 桥接处理器。代码如下:

    // Slf4JLoggingSystem.javaprivate void removeJdkLoggingBridgeHandler() {try {// 移除 JUL 的 ConsoleHandlerremoveDefaultRootHandler();// 卸载 SLF4JBridgeHandlerSLF4JBridgeHandler.uninstall();} catch (Throwable ex) {// Ignore and continue}
    }private void removeDefaultRootHandler() {try {Logger rootLogger = LogManager.getLogManager().getLogger("");Handler[] handlers = rootLogger.getHandlers();if (handlers.length == 1 && handlers[0] instanceof ConsoleHandler) {rootLogger.removeHandler(handlers[0]);}} catch (Throwable ex) {// Ignore and continue}
    }
    
    • 移除 JUL 的 ConsoleHandler ,卸载 SLF4JBridgeHandler 。
  • <3> 处,会重新安装 SLF4JBridgeHandler。

7.3.2 cleanUp

重写 #cleanUp() 方法,代码如下:

// Slf4JLoggingSystem.java@Override
public void cleanUp() {// 判断 JUL 是否桥接到 SLF4J 了if (isBridgeHandlerAvailable()) {// 移除 JUL 桥接处理器removeJdkLoggingBridgeHandler();}
}

7.3.3 loadConfiguration

重写 #loadConfiguration(LoggingInitializationContext initializationContext, String location, LogFile logFile) 方法,代码如下:

// Slf4JLoggingSystem.java@Override
protected void loadConfiguration(LoggingInitializationContext initializationContext, String location, LogFile logFile) {Assert.notNull(location, "Location must not be null");if (initializationContext != null) {applySystemProperties(initializationContext.getEnvironment(), logFile);}
}
  • 调用 #applySystemProperties(Environment environment, LogFile logFile) 方法,应用 environment 和 logFile 的属性,到系统属性种。在 「4.2 apply」 中,已经详细解析。
  • 不过有一点,搞不懂,为什么这么实现。

7.4 LogbackLoggingSystem

org.springframework.boot.logging.logback.LogbackLoggingSystem ,继承 Slf4JLoggingSystem 抽象类,基于 Logback 的 LoggingSystem 实现类。

7.4.1 beforeInitialize

重写 #beforeInitialize() 方法,代码如下:

// LogbackLoggingSystem.java@Override
public void beforeInitialize() {// <1.1> 获得 LoggerContext 对象LoggerContext loggerContext = getLoggerContext();// <1.2> 如果已经初始化过,则直接返回if (isAlreadyInitialized(loggerContext)) {return;}// <2> 调用父方法super.beforeInitialize();// <3> 添加 FILTER 到其中loggerContext.getTurboFilterList().add(FILTER);
}
  • <1.1> 处,调用 #getLoggerContext() 方法,获得 LoggerContext 对象。代码如下:
    // LogbackLoggingSystem.javaprivate 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;
    }
    
  • <1.2> 处,调用 #isAlreadyInitialized(LoggerContext loggerContext) 方法,判断如果已经初始化过,则直接返回。代码如下:
    // LogbackLoggingSystem.javaprivate boolean isAlreadyInitialized(LoggerContext loggerContext) {return loggerContext.getObject(LoggingSystem.class.getName()) != null;
    }
    
  • <2> 处,调用父方法。
  • <3> 处,添加 FILTER 到 loggerContext 其中。代码如下:

    // LogbackLoggingSystem.javaprivate static final TurboFilter FILTER = new TurboFilter() {@Overridepublic FilterReply decide(Marker marker, ch.qos.logback.classic.Logger logger,Level level, String format, Object[] params, Throwable t) {return FilterReply.DENY;}};
    
    • 因为此时,Logback 并未初始化好,所以全部返回 FilterReply.DENY 。即,先不打印日志。

7.4.2 getStandardConfigLocations

实现 #getStandardConfigLocations() 方法,获得约定的配置文件的数组。代码如下:

// LogbackLoggingSystem.java@Override
protected String[] getStandardConfigLocations() {return new String[] { "logback-test.groovy", "logback-test.xml", "logback.groovy","logback.xml" };
}

7.4.3 initialize

重写 #initialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) 方法,代码如下:

// LogbackLoggingSystem.javaprivate static final String CONFIGURATION_FILE_PROPERTY = "logback.configurationFile";@Override
public void initialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) {// <1> 如果已经初始化,则返回LoggerContext loggerContext = getLoggerContext();if (isAlreadyInitialized(loggerContext)) {return;}// <2> 调用父方法super.initialize(initializationContext, configLocation, logFile);// <3> 移除 FILTERloggerContext.getTurboFilterList().remove(FILTER);// <4> 标记已经初始化markAsInitialized(loggerContext);// <5> 如果配置了 logback.configurationFile ,则打印日志if (StringUtils.hasText(System.getProperty(CONFIGURATION_FILE_PROPERTY))) {getLogger(LogbackLoggingSystem.class.getName()).warn("Ignoring '" + CONFIGURATION_FILE_PROPERTY + "' system property. " + "Please use 'logging.config' instead.");}
}
  • <1> 处,如果已经初始化,则返回。
  • <2> 处,调用父方法,进行初始化。
  • <3> 处,从 loggerContext 中,移除 FILTER 。😈 如果不移除,就一直打印不出日志列。
  • <4> 处,调用 #markAsInitialized(LoggerContext loggerContext) 方法,标记已经初始化。代码如下:

    // LogbackLoggingSystem.javaprivate void markAsInitialized(LoggerContext loggerContext) {loggerContext.putObject(LoggingSystem.class.getName(), new Object());
    }
    
  • <5> 处,如果配置了 "logback.configurationFile" ,则打印日志。

7.4.3.1 loadConfiguration

实现 #loadConfiguration(LoggingInitializationContext initializationContext, String location, LogFile logFile) 方法,代码如下:

// LogbackLoggingSystem.java@Override
protected void loadConfiguration(LoggingInitializationContext initializationContext, String location, LogFile logFile) {// <1> 调用父方法super.loadConfiguration(initializationContext, location, logFile);// <2> 重置LoggerContext loggerContext = getLoggerContext();stopAndReset(loggerContext);// <3> 读取配置文件,并进行配置try {configureByResourceUrl(initializationContext, loggerContext, ResourceUtils.getURL(location));} catch (Exception ex) {throw new IllegalStateException("Could not initialize Logback logging from " + location, ex);}// <4> 判断是否发生错误。如果有,则抛出 IllegalStateException 异常List<Status> statuses = loggerContext.getStatusManager().getCopyOfStatusList();StringBuilder errors = new StringBuilder();for (Status status : statuses) {if (status.getLevel() == Status.ERROR) {errors.append((errors.length() > 0) ? String.format("%n") : "");errors.append(status.toString());}}if (errors.length() > 0) {throw new IllegalStateException(String.format("Logback configuration error detected: %n%s", errors));}
}
  • <1> 处,调用父方法。
  • <2> 处,调用 #stopAndReset(LoggerContext loggerContext) 方法,重置。代码如下:

    // LogbackLoggingSystem.javaprivate void stopAndReset(LoggerContext loggerContext) {// 停止loggerContext.stop();// 重置loggerContext.reset();// 如果是 SLF4J 桥接if (isBridgeHandlerInstalled()) {// 添加 LevelChangePropagatoraddLevelChangePropagator(loggerContext);}
    }private boolean isBridgeHandlerInstalled() {if (!isBridgeHandlerAvailable()) {return false;}java.util.logging.Logger rootLogger = LogManager.getLogManager().getLogger("");Handler[] handlers = rootLogger.getHandlers();// 判断有 SLF4JBridgeHandler 唯一元素return handlers.length == 1 && handlers[0] instanceof SLF4JBridgeHandler;
    }private void addLevelChangePropagator(LoggerContext loggerContext) {// 创建 LevelChangePropagator 对象(见 https://cloud.tencent.com/developer/ask/174323 说明)LevelChangePropagator levelChangePropagator = new LevelChangePropagator();// 设置属性levelChangePropagator.setResetJUL(true);levelChangePropagator.setContext(loggerContext);// 添加 LevelChangePropagator 到 loggerContext 中loggerContext.addListener(levelChangePropagator);
    }
    
    • 通过阅读 https://cloud.tencent.com/developer/ask/174323 文章,我们能弄懂这里为什么要使用 LevelChangePropagator ,以及 「7.3.1.1 configureJdkLoggingBridgeHandler」 处的原因。
  • <3> 处,调用 #configureByResourceUrl(LoggingInitializationContext initializationContext, LoggerContext loggerContext, URL url) 方法,读取配置文件,并进行配置。代码如下:

    // LogbackLoggingSystem.javaprivate void configureByResourceUrl(LoggingInitializationContext initializationContext, LoggerContext loggerContext, URL url) throws JoranException {// <X> 如果是 xml 配置格式,则使用 SpringBootJoranConfiguratorif (url.toString().endsWith("xml")) {JoranConfigurator configurator = new SpringBootJoranConfigurator(initializationContext);configurator.setContext(loggerContext);configurator.doConfigure(url);// 如果是其它格式,则使用 ContextInitializer} else {new ContextInitializer(loggerContext).configureByResource(url);}
    }
    
    • <X> 处,如果是 Logback xml 配置格式,则使用 SpringBootJoranConfigurator 类。
    • 至此,Logback 配置文件,就已经被读完落。
  • <4> 处,判断是否发生错误。如果有,则抛出 IllegalStateException 异常。

7.4.3.1.1 SpringBootJoranConfigurator

org.springframework.boot.logging.logback.SpringBootJoranConfigurator ,继承 JoranConfigurator 类,增加 Spring Boot 自定义的标签。代码如下:

// SpringBootJoranConfigurator.javaclass SpringBootJoranConfigurator extends JoranConfigurator {private LoggingInitializationContext initializationContext;SpringBootJoranConfigurator(LoggingInitializationContext initializationContext) {this.initializationContext = initializationContext;}@Overridepublic void addInstanceRules(RuleStore rs) {// 调用父方法super.addInstanceRules(rs);Environment environment = this.initializationContext.getEnvironment();rs.addRule(new ElementSelector("configuration/springProperty"), new SpringPropertyAction(environment));rs.addRule(new ElementSelector("*/springProfile"), new SpringProfileAction(environment));rs.addRule(new ElementSelector("*/springProfile/*"), new NOPAction());}}
  • 不了解的胖友,可以先看看 《SpringBoot 中 logback.xml 使用 application.yml 中属性》 文章。
  • org.springframework.boot.logging.logback.SpringProfileAction ,处理 <springProfile /> 标签。
  • org.springframework.boot.logging.logback.SpringPropertyAction ,处理 <springProperty /> 标签。

7.4.3.2 reinitialize

实现 #reinitialize(LoggingInitializationContext initializationContext) 方法,代码如下:

// LogbackLoggingSystem.java@Override
protected void reinitialize(LoggingInitializationContext initializationContext) {// <1> 重置getLoggerContext().reset();// <2> 清空 StatusManagergetLoggerContext().getStatusManager().clear();// <3> 加载配置loadConfiguration(initializationContext, getSelfInitializationConfig(), null);
}
  • <1> 处,重置。
  • <2> 处,清空 StatusManager 。
  • <3> 处,调用 #loadConfiguration(LoggingInitializationContext initializationContext, String location, LogFile logFile) 方法,加载配置。此时,使用的是约定的 Logback 配置文件。

7.4.3.3 loadDefaults

实现 #loadDefaults(LoggingInitializationContext initializationContext, LogFile logFile) 方法,代码如下:

// LogbackLoggingSystem.java@Override
protected void loadDefaults(LoggingInitializationContext initializationContext, LogFile logFile) {// <1> 重置LoggerContext context = getLoggerContext();stopAndReset(context);// <2> 创建 LogbackConfigurator 对象LogbackConfigurator configurator = new LogbackConfigurator(context);// <3> 从 environment 读取变量,设置到 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}}"));// <4> 创建 DefaultLogbackConfiguration 对象,设置到 configurator 中new DefaultLogbackConfiguration(initializationContext, logFile).apply(configurator);// <5> 设置日志文件,按天滚动context.setPackagingDataEnabled(true);
}
  • <1> 处,调用 #stopAndReset(LoggerContext loggerContext) 方法,重置。
  • <2> 处,创建 LogbackConfigurator 对象。详细解析,见 「7.4.3.3.1 LogbackConfigurator」 。
  • <3> 处,从 environment 读取变量,设置到 context 中。
  • <4> 处,创建 DefaultLogbackConfiguration 对象,后调用 DefaultLogbackConfiguration#apply(LogbackConfigurator) 方法,设置到 configurator 中。详细解析,见 「7.4.3.3.2 DefaultLogbackConfiguration」 。
  • <5> 处,调用 LoggerContext#setPackagingDataEnabled(boolean packagingDataEnabled) 方法,设置日志文件,按天滚动。

7.4.3.3.1 LogbackConfigurator

org.springframework.boot.logging.logback.LogbackConfigurator ,Logback 配置器,提供一些工具方法,方便配置 Logback 。

因为 LogbackConfigurator 提供的方法,都是被 DefaultLogbackConfiguration 所调用。所以我们先跳到 「7.4.3.3.2 DefaultLogbackConfiguration」 中。

7.4.3.3.1.1 conversionRule

#conversionRule(String conversionWord, Class<? extends Converter> converterClass) 方法,添加转换规则。代码如下:

// LogbackConfigurator.javapublic void conversionRule(String conversionWord, Class<? extends Converter> converterClass) {Assert.hasLength(conversionWord, "Conversion word must not be empty");Assert.notNull(converterClass, "Converter class must not be null");// 获得注册表Map<String, String> registry = (Map<String, String>) this.context.getObject(CoreConstants.PATTERN_RULE_REGISTRY);// 如果注册表为空,则进行注册if (registry == null) {registry = new HashMap<>();this.context.putObject(CoreConstants.PATTERN_RULE_REGISTRY, registry);}// 添加转换规则,到注册表中registry.put(conversionWord, converterClass.getName());
}
  • 比较简单,胖友自己瞅瞅。
  • 目前有三个转换规则,分别是:
    • org.springframework.boot.logging.logback.ColorConverter ,实现 ANSI 颜色转换器。
    • org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter ,在异常堆栈的打印过程中添加一些空格。
    • org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter ,在异常堆栈的打印过程中添加一些空格。
    • org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter ,在异常堆栈的打印过程中添加一些空格。【同上】
    • 就不详细解析啦,胖友自己瞅瞅就明白列。

7.4.3.3.1.2 logger

#logger(String name, Level level) 方法,添加 Logger 。代码如下:

// LogbackConfigurator.javapublic void logger(String name, Level level) {logger(name, level, true);
}public void logger(String name, Level level, boolean additive) {logger(name, level, additive, null);
}public void logger(String name, Level level, boolean additive, Appender<ILoggingEvent> appender) {// 获得 Logger 对象Logger logger = this.context.getLogger(name);// 设置 levelif (level != null) {logger.setLevel(level);}// 设置 additivelogger.setAdditive(additive);// 设置 appenderif (appender != null) {logger.addAppender(appender);}
}

7.4.3.3.1.3 appender

#appender(String name, Appender<?> appender) 方法,启动 Appender 。代码如下:

// LogbackConfigurator.javapublic void appender(String name, Appender<?> appender) {// 设置 nameappender.setName(name);// 启动 Appenderstart(appender);
}public void start(LifeCycle lifeCycle) {// 设置 contextif (lifeCycle instanceof ContextAware) {((ContextAware) lifeCycle).setContext(this.context);}// 启动lifeCycle.start();
}

7.4.3.3.1.4 root

#root(Level level, Appender<ILoggingEvent>... appenders) 方法,设置 appender 到 ROOT Logger 。代码如下:

// LogbackConfigurator.javapublic final void root(Level level, Appender<ILoggingEvent>... appenders) {// 获得 Root Logger 对象Logger logger = this.context.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);// 设置 levelif (level != null) {logger.setLevel(level);}// 添加 appender 到 logger 中for (Appender<ILoggingEvent> appender : appenders) {logger.addAppender(appender);}
}

7.4.3.3.2 DefaultLogbackConfiguration

org.springframework.boot.logging.logback.DefaultLogbackConfiguration ,默认的 Logback 配置类。代码如下:

相当于代码生成 logback.xml 的效果。

7.4.3.3.2.1 构造方法

// DefaultLogbackConfiguration.java/*** PropertyResolver 对象。提供从 environment 解析配置*/
private final PropertyResolver patterns;private final LogFile logFile;DefaultLogbackConfiguration(LoggingInitializationContext initializationContext, LogFile logFile) {this.patterns = getPatternsResolver(initializationContext.getEnvironment());this.logFile = logFile;
}private PropertyResolver getPatternsResolver(Environment environment) {// 创建 PropertySourcesPropertyResolver 对象,无 environmentif (environment == null) {return new PropertySourcesPropertyResolver(null);}// 创建 PropertySourcesPropertyResolver 对象,有 environmentif (environment instanceof ConfigurableEnvironment) {PropertySourcesPropertyResolver resolver = new PropertySourcesPropertyResolver(((ConfigurableEnvironment) environment).getPropertySources());resolver.setIgnoreUnresolvableNestedPlaceholders(true);return resolver;}// 直接返回 environmentreturn environment;
}

7.4.3.3.2.2 apply

#apply(LogbackConfigurator config) 方法,应用配置。代码如下:

// DefaultLogbackConfiguration.javapublic void apply(LogbackConfigurator config) {// <1> 锁synchronized (config.getConfigurationLock()) {// <2> 设置基础属性base(config);// <3> 创建 console AppenderAppender<ILoggingEvent> consoleAppender = consoleAppender(config);// <4> 如果 logFile 非空,则创建 file Appenderif (this.logFile != null) {Appender<ILoggingEvent> fileAppender = fileAppender(config, this.logFile.toString());// <5> 设置 appender 到 ROOT Loggerconfig.root(Level.INFO, consoleAppender, fileAppender);} else {// <5> 设置 appender 到 ROOT Loggerconfig.root(Level.INFO, consoleAppender);}}
}
  • <1> 处,锁。代码如下:

    // LogbackConfigurator.javaprivate LoggerContext context;public Object getConfigurationLock() {return this.context.getConfigurationLock();
    }
    
  • <2> 处,调用 #base(LogbackConfigurator config) 方法,设置基础属性。代码如下:

    // LogbackConfigurator.javaprivate void base(LogbackConfigurator config) {// <2.1> Converterconfig.conversionRule("clr", ColorConverter.class);config.conversionRule("wex", WhitespaceThrowableProxyConverter.class);config.conversionRule("wEx", ExtendedWhitespaceThrowableProxyConverter.class);// <2.2> 默认的 loggerconfig.logger("org.apache.catalina.startup.DigesterFactory", Level.ERROR);config.logger("org.apache.catalina.util.LifecycleBase", Level.ERROR);config.logger("org.apache.coyote.http11.Http11NioProtocol", Level.WARN);config.logger("org.apache.sshd.common.util.SecurityUtils", Level.WARN);config.logger("org.apache.tomcat.util.net.NioSelectorPool", Level.WARN);config.logger("org.eclipse.jetty.util.component.AbstractLifeCycle", Level.ERROR);config.logger("org.hibernate.validator.internal.util.Version", Level.WARN);
    }
    
    • <2.1> 处,调用 LogbackConfigurator#conversionRule(String conversionWord, Class<? extends Converter> converterClass) 方法,添加转换规则。详细解析,见 「 7.4.3.3.1.1 conversionRule」 。
    • <2.2> 处,调用 LogbackConfigurator#logger(String name, Level level) 方法,默认的 logger 。详细解析,见 「7.4.3.3.1.2 logger」 。
  • <3> 处,调用 #consoleAppender(LogbackConfigurator config) 方法,创建 console Appender 对象。代码如下:

    // LogbackConfigurator.javaprivate static final String CONSOLE_LOG_PATTERN = "%clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}}){faint} "+ "%clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} "+ "%clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} "+ "%clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}";private Appender<ILoggingEvent> consoleAppender(LogbackConfigurator config) {ConsoleAppender<ILoggingEvent> appender = new ConsoleAppender<>();PatternLayoutEncoder encoder = new PatternLayoutEncoder();String logPattern = this.patterns.getProperty("logging.pattern.console", CONSOLE_LOG_PATTERN); // <X>encoder.setPattern(OptionHelper.substVars(logPattern, config.getContext()));config.start(encoder);appender.setEncoder(encoder);config.appender("CONSOLE", appender); // <Y>return appender;
    }
    
    • <X> 处,从 environment 中,读取 "logging.pattern.console" 作为格式。如果找不到,使用 CONSOLE_LOG_PATTERN 。
    • <Y> 处,调用 LogbackConfigurator#appender(String name, Appender<?> appender) 方法,启动 Appender 。详细解析,见 「7.4.3.3.1.3 appender」 。
  • <4> 处,如果 logFile 非空,则调用 #fileAppender(LogbackConfigurator config, String logFile) 方法,创建 file Appender。代码如下:

     // LogbackConfigurator.javaprivate static final String FILE_LOG_PATTERN = "%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}} "
    + "${LOG_LEVEL_PATTERN:-%5p} ${PID:- } --- [%t] %-40.40logger{39} : %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}";private static final String MAX_FILE_SIZE = "10MB";private Appender<ILoggingEvent> fileAppender(LogbackConfigurator config, String logFile) {RollingFileAppender<ILoggingEvent> appender = new RollingFileAppender<>();PatternLayoutEncoder encoder = new PatternLayoutEncoder();String logPattern = this.patterns.getProperty("logging.pattern.file", FILE_LOG_PATTERN);encoder.setPattern(OptionHelper.substVars(logPattern, config.getContext()));appender.setEncoder(encoder);config.start(encoder);appender.setFile(logFile);// 滚动策略setRollingPolicy(appender, config, logFile);config.appender("FILE", appender);return appender;}private void setRollingPolicy(RollingFileAppender<ILoggingEvent> appender, LogbackConfigurator config, String logFile) {SizeAndTimeBasedRollingPolicy<ILoggingEvent> rollingPolicy = new SizeAndTimeBasedRollingPolicy<>();rollingPolicy.setFileNamePattern(logFile + ".%d{yyyy-MM-dd}.%i.gz");// 单文件最大值setMaxFileSize(rollingPolicy, this.patterns.getProperty("logging.file.max-size", MAX_FILE_SIZE));rollingPolicy.setMaxHistory(this.patterns.getProperty("logging.file.max-history", Integer.class, CoreConstants.UNBOUND_HISTORY));appender.setRollingPolicy(rollingPolicy);rollingPolicy.setParent(appender);config.start(rollingPolicy);}private void setMaxFileSize(SizeAndTimeBasedRollingPolicy<ILoggingEvent> rollingPolicy, String maxFileSize) {try {rollingPolicy.setMaxFileSize(FileSize.valueOf(maxFileSize));} catch (NoSuchMethodError ex) {// Logback < 1.1.8 used String configurationMethod method = ReflectionUtils.findMethod(SizeAndTimeBasedRollingPolicy.class, "setMaxFileSize", String.class);ReflectionUtils.invokeMethod(method, rollingPolicy, maxFileSize);}}
    
  • <5> 处,调用 LogbackConfigurator#root(Level level, Appender<ILoggingEvent>... appenders) 方法,设置 appender 到 ROOT Logger 。详细解析,见 「7.4.3.3.1.4 root」 。

7.4.4 setLogLevel

实现 #setLogLevel(String loggerName, LogLevel level) 方法,代码如下:

// LogbackLoggingSystem.javaprivate static final LogLevels<Level> LEVELS = new LogLevels<>();static {LEVELS.map(LogLevel.TRACE, Level.TRACE);LEVELS.map(LogLevel.TRACE, Level.ALL);LEVELS.map(LogLevel.DEBUG, Level.DEBUG);LEVELS.map(LogLevel.INFO, Level.INFO);LEVELS.map(LogLevel.WARN, Level.WARN);LEVELS.map(LogLevel.ERROR, Level.ERROR);LEVELS.map(LogLevel.FATAL, Level.ERROR);LEVELS.map(LogLevel.OFF, Level.OFF);
}@Override
public void setLogLevel(String loggerName, LogLevel level) {// <1> 获得 Logger 对象ch.qos.logback.classic.Logger logger = getLogger(loggerName);// <2> 设置日志级别if (logger != null) {logger.setLevel(LEVELS.convertSystemToNative(level));}
}
  • <1> 处,调用 #getLogger(String name) 方法,获得 Logger 对象。代码如下:
    // LogbackLoggingSystem.javaprivate ch.qos.logback.classic.Logger getLogger(String name) {LoggerContext factory = getLoggerContext();if (StringUtils.isEmpty(name) || ROOT_LOGGER_NAME.equals(name)) {name = Logger.ROOT_LOGGER_NAME;}return factory.getLogger(name);}
    
  • <2> 处,设置日志级别。

7.4.4 cleanUp

重写 #cleanUp() 方法,代码如下:

// LogbackLoggingSystem.java@Override
public void cleanUp() {// 标记为未初始化LoggerContext context = getLoggerContext();markAsUninitialized(context);// 调用父方法super.cleanUp();// 清空 StatusManagercontext.getStatusManager().clear();// 移除 FILTERcontext.getTurboFilterList().remove(FILTER);
}private void markAsUninitialized(LoggerContext loggerContext) {loggerContext.removeObject(LoggingSystem.class.getName());
}

7.4.5 getShutdownHandler

实现 #getShutdownHandler() 方法,代码如下:

// LogbackLoggingSystem.java@Override
public Runnable getShutdownHandler() {return new ShutdownHandler();
}private final class ShutdownHandler implements Runnable {@Overridepublic void run() {getLoggerContext().stop(); // 停止}}

7.5 Log4J2LoggingSystem

org.springframework.boot.logging.log4j2.Log4J2LoggingSystem ,继承 Slf4JLoggingSystem 抽象类,基于 Log4J2 的 LoggingSystem 实现类。

就暂时不解析了,基本类似。感兴趣的胖友,可以看看 《spring boot 源码解析28-Log4J2LoggingSystem》 。

7.6 JavaLoggingSystem

org.springframework.boot.logging.java.JavaLoggingSystem ,继承 AbstractLoggingSystem 抽象类,基于 JUL 的 LoggingSystem 实现类。

就暂时不解析了,基本类似。感兴趣的胖友,可以看看 《spring boot 源码解析27-JavaLoggingSystem及LoggingSystem生命周期详解》 的 「LoggingSystem」 部分。

666. 彩蛋

Spring Boot 的文章,基本都短不了~咋说呢?虽然长了一些吧,总体还是比较简单和顺畅的。

参考和推荐如下文章:

  • 一个努力的码农 《spring boot 源码解析29-LogbackLoggingSystem》
  • oldflame-Jm 《Spring boot源码分析-log日志系统(6)》
Yestar123456
发布了23 篇原创文章 · 获赞 0 · 访问量 83
私信关注
查看全文
如若内容造成侵权/违法违规/事实不符,请联系编程学习网邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!

相关文章

  1. 架构设计029 画图 实战四

    pass点赞收藏分享文章举报ailinyingai发布了295 篇原创文章 获赞 6 访问量 4万+他的留言板关注...

    2024/4/18 0:51:37
  2. Java中各种锁机制的介绍

    本文内容 在读很多并发文章中,会提及各种各样锁如公平锁,乐观锁等等,这篇文章介绍各种锁的分类。介绍的内容如下: 1.公平锁 / 非公平锁 2.可重入锁 / 不可重入锁 3.独享锁 / 共享锁 4.互斥锁 / 读写锁 5.乐观锁 / 悲观锁 6.分段锁 7.偏向锁 / 轻量级锁 / 重量级锁 8.自旋锁…...

    2024/3/29 10:16:45
  3. 精尽 Spring Boot 源码分析 —— AutoConfigurationMetadataLoader

    精尽 Spring Boot 源码分析 —— AutoConfigurationMetadataLoader1. 概述本文,我们来补充 《精尽 Spring Boot 源码分析 —— 自动配置》 文章,并未详细解析的 AutoConfigurationMetadataLoader 。在 SpringApplication 中,我们可以看到 AutoConfigurationImportSelector.A…...

    2024/4/16 22:53:50
  4. 精尽 Spring Boot 源码分析 —— SpringFactoriesLoader

    精尽 Spring Boot 源码分析 —— SpringFactoriesLoader1. 概述本文,我们来补充 《精尽 Spring Boot 源码分析 —— SpringApplication》 文章,并未详细解析的 SpringFactoriesLoader 。spring.factories 配置文件,我们可以认为是 Spring 自己的一套 SPI 机制的配置文件,在…...

    2024/4/18 4:11:55
  5. 图像处理工程师的基本要求有哪些

    既然学了人工智能这个专业,研究生期间主要方向是机器学习,计算机视觉,图像处理。所以很想了解现在这个领域的就业方向及相关要求。 今天在“增强视觉 | 计算机视觉 增强现实”上看到一则招聘智能图像/视频处理工程师的广告,岗位要求如下: 动手能力强,熟练掌握C/C++/Matla…...

    2024/4/17 2:39:20
  6. GIT commit问题 No errors and 30 warnings found. Would you like to review them?

    点赞收藏分享文章举报胖带鱼发布了21 篇原创文章 获赞 0 访问量 193私信关注...

    2024/4/16 8:29:12
  7. 转载之Java代码优化细节

    可以提高千倍效率的Java代码小技巧来源:www.cnblogs.com/Qian123/p/6046096.html前言代码优化 ,一个很重要的课题。可能有些人觉得没用,一些细小的地方有什么好修改的,改与不改对于代码的运行效率有什么影响呢?这个问题我是这么考虑的,就像大海里面的鲸鱼一样,它吃一条小…...

    2024/4/17 17:51:33
  8. Babybluetooth框架分析

    点赞收藏分享文章举报humiaor发布了34 篇原创文章 获赞 8 访问量 4万+私信关注...

    2024/4/14 6:59:07
  9. Bash - style

    dd点赞收藏分享文章举报wojiarucs发布了5 篇原创文章 获赞 0 访问量 22私信关注...

    2024/4/12 12:23:17
  10. JAVA专业术语面试100问

    前言: 面试技巧另外开篇再说,先上面试干货吧。 Redis、 消息队列、 SQL 不要走开,关注后更精彩! 1、面向对象的特点有哪些? 抽象、继承、封装、多态。 2、接口和抽象类有什么联系和区别? 3、重载和重写有什么区别? 4、java有哪些基本数据类型? 5、数组有没有length()方…...

    2024/4/19 12:58:26
  11. TortoiseSVN提示Error: Unable to connect to a repository at URL 的解决方法

    右键->SVN,设置里将缓存清空,一般情况下是可以正常连接了点赞收藏分享文章举报MakeGreatEffort发布了127 篇原创文章 获赞 174 访问量 108万+他的留言板关注...

    2024/4/19 12:55:45
  12. java序列化详解

    遇到这个 Java Serializable 序列化这个接口,我们可能会有如下的问题 a,什么叫序列化和反序列化 b,作用。为啥要实现这个 Serializable 接口,也就是为啥要序列化 c,serialVersionUID 这个的值到底是在怎么设置的,有什么用。有的是1L,有的是一长串数字,迷惑ing。 我刚刚…...

    2024/4/18 6:18:55
  13. Android使用https

    注:所有转载文章只作为学习记录,无其他想法。 前言 HI,欢迎来到裴智飞的《每周一博》。今天是九月第三周,我给大家介绍一下安卓如何使用https。 HTTP协议被用于在Web浏览器和网站服务器之间传递信息,HTTP协议以明文方式发送内容,不提供任何方式的数据加密,如果攻击者截取…...

    2024/3/31 2:51:26
  14. 【转载】基于redis的分布式锁

    前言分布式锁一般有三种实现方式:1. 数据库乐观锁;2. 基于Redis的分布式锁;3. 基于ZooKeeper的分布式锁。本篇博客将介绍第二种方式,基于Redis实现分布式锁。虽然网上已经有各种介绍Redis分布式锁实现的博客,然而他们的实现却有着各种各样的问题,为了避免误人子弟,本篇博…...

    2024/4/6 19:01:57
  15. linux安装oracle时出现乱码已解决

    llinux安装oracle出现乱码解决 原因分析:oracle安装包提供的jdk内缺少中文字体 解决办法:往oracle安装包提供的jdk内拷贝中文字体zysong.ttf 一、操作系统:centos7,oracle版本:11gr2 在解压出oracle安装包后,找到database/stage/Components/oracle.jdk/1.5.0.17.0/1/Data…...

    2024/4/7 3:05:29
  16. Epoll模型详解

    1. 内核中提高I/O性能的新方法epollepoll是什么?按照man手册的说法:是为处理大批量句柄而作了改进的poll。要使用epoll只需要这三个系统调 用:epoll_create(2), epoll_ctl(2), epoll_wait(2)。当然,这不是2.6内核才有的,它是在 2.5.44内核中被引进的(epoll(4) is a new …...

    2024/4/18 8:57:45
  17. js vue获取本地时间

    js vue 获取本地时间地方法及计算 let yy = new Date().getFullYear(); let mm = new Date().getMonth()+1; let dd = new Date().getDate(); let hh = new Date().getHours(); let mf = new Date().getMinutes()<10 ? ‘0’+new Date().getMinutes() : new Date().getMinu…...

    2024/4/19 7:09:58
  18. el表达式

    EL 全名为Expression Language EL 语法很简单,它最大的特点就是使用上很方便。接下来介绍EL主要的语法结构: ${sessionScope.user.sex} 所有EL都是以${为起始、以}为结尾的。上述EL范例的意思是:从Session的范围中,取得用户的性别。假若依照之前JSP Scriptlet的写法如下: …...

    2024/4/18 18:56:20
  19. 突然,我想恋爱了!!

    惜别过去,把握现在,憧憬未来 ——题记 你,还好吗?不知道你会不会看到这篇文章,即使看到了,估计你也不会有什么感觉,感谢你的不娶之恩… 不知不觉,咱们分手半年了吧,我是一个比较感性的女孩,每一段感情,我总会在过后的某个时间段回忆,唯独这份感情,一直都是黑暗的,…...

    2024/4/8 0:21:21
  20. Python 导入自己写的模块

    Python中import导入上一级目录模块及循环import问题的解决转载:https://www.cnblogs.com/sjy18039225956/p/9265461.html使用python进行程序编写时,经常会使用第三方模块包。这种包我们可以通过python setup install 进行安装后,通过import XXX或from XXX import yyy 进行导…...

    2024/4/8 0:21:25

最新文章

  1. C++|list的模拟实现

    C|list的模拟实现 前言ListNodelistlist的尾插操作list的尾删 list的迭代器代码实现 list的其他操作inserterase const迭代器 前言 我们模拟实现的list是带头双向循环链表&#xff0c;list的成员变量有指向头节点的指针。所以在实现list之前我们应该实现节点这个类型 ListNod…...

    2024/4/19 21:43:11
  2. 梯度消失和梯度爆炸的一些处理方法

    在这里是记录一下梯度消失或梯度爆炸的一些处理技巧。全当学习总结了如有错误还请留言&#xff0c;在此感激不尽。 权重和梯度的更新公式如下&#xff1a; w w − η ⋅ ∇ w w w - \eta \cdot \nabla w ww−η⋅∇w 个人通俗的理解梯度消失就是网络模型在反向求导的时候出…...

    2024/3/20 10:50:27
  3. promise.all方式使用

    romise.all( ).then( ) 处理多个异步任务&#xff0c;且所有的异步任务都得到结果时的情况。 比如&#xff1a;用户点击按钮&#xff0c;会弹出一个弹出对话框&#xff0c;对话框中有两部分数据呈现&#xff0c;这两部分数据分别是不同的后端接口获取的数据。 弹框弹出后的初…...

    2024/4/19 8:11:55
  4. 【图论】知识点集合

    边的类型 neighbors(邻居)&#xff1a;两个顶点有一条共同边 loop&#xff1a;链接自身 link&#xff1a;两个顶点有一条边 parallel edges&#xff1a;两个顶点有两条及以上条边 无向图 必要条件&#xff1a;删掉顶点数一定大于等于剩下的顶点数 设无向图G<V,E>是…...

    2024/4/18 20:00:28
  5. 【外汇早评】美通胀数据走低,美元调整

    原标题:【外汇早评】美通胀数据走低,美元调整昨日美国方面公布了新一期的核心PCE物价指数数据,同比增长1.6%,低于前值和预期值的1.7%,距离美联储的通胀目标2%继续走低,通胀压力较低,且此前美国一季度GDP初值中的消费部分下滑明显,因此市场对美联储后续更可能降息的政策…...

    2024/4/19 14:24:02
  6. 【原油贵金属周评】原油多头拥挤,价格调整

    原标题:【原油贵金属周评】原油多头拥挤,价格调整本周国际劳动节,我们喜迎四天假期,但是整个金融市场确实流动性充沛,大事频发,各个商品波动剧烈。美国方面,在本周四凌晨公布5月份的利率决议和新闻发布会,维持联邦基金利率在2.25%-2.50%不变,符合市场预期。同时美联储…...

    2024/4/19 18:20:22
  7. 【外汇周评】靓丽非农不及疲软通胀影响

    原标题:【外汇周评】靓丽非农不及疲软通胀影响在刚结束的周五,美国方面公布了新一期的非农就业数据,大幅好于前值和预期,新增就业重新回到20万以上。具体数据: 美国4月非农就业人口变动 26.3万人,预期 19万人,前值 19.6万人。 美国4月失业率 3.6%,预期 3.8%,前值 3…...

    2024/4/19 11:57:31
  8. 【原油贵金属早评】库存继续增加,油价收跌

    原标题:【原油贵金属早评】库存继续增加,油价收跌周三清晨公布美国当周API原油库存数据,上周原油库存增加281万桶至4.692亿桶,增幅超过预期的74.4万桶。且有消息人士称,沙特阿美据悉将于6月向亚洲炼油厂额外出售更多原油,印度炼油商预计将每日获得至多20万桶的额外原油供…...

    2024/4/19 11:57:31
  9. 【外汇早评】日本央行会议纪要不改日元强势

    原标题:【外汇早评】日本央行会议纪要不改日元强势近两日日元大幅走强与近期市场风险情绪上升,避险资金回流日元有关,也与前一段时间的美日贸易谈判给日本缓冲期,日本方面对汇率问题也避免继续贬值有关。虽然今日早间日本央行公布的利率会议纪要仍然是支持宽松政策,但这符…...

    2024/4/19 11:57:52
  10. 【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响

    原标题:【原油贵金属早评】欧佩克稳定市场,填补伊朗问题的影响近日伊朗局势升温,导致市场担忧影响原油供给,油价试图反弹。此时OPEC表态稳定市场。据消息人士透露,沙特6月石油出口料将低于700万桶/日,沙特已经收到石油消费国提出的6月份扩大出口的“适度要求”,沙特将满…...

    2024/4/19 11:57:53
  11. 【外汇早评】美欲与伊朗重谈协议

    原标题:【外汇早评】美欲与伊朗重谈协议美国对伊朗的制裁遭到伊朗的抗议,昨日伊朗方面提出将部分退出伊核协议。而此行为又遭到欧洲方面对伊朗的谴责和警告,伊朗外长昨日回应称,欧洲国家履行它们的义务,伊核协议就能保证存续。据传闻伊朗的导弹已经对准了以色列和美国的航…...

    2024/4/19 11:58:14
  12. 【原油贵金属早评】波动率飙升,市场情绪动荡

    原标题:【原油贵金属早评】波动率飙升,市场情绪动荡因中美贸易谈判不安情绪影响,金融市场各资产品种出现明显的波动。随着美国与中方开启第十一轮谈判之际,美国按照既定计划向中国2000亿商品征收25%的关税,市场情绪有所平复,已经开始接受这一事实。虽然波动率-恐慌指数VI…...

    2024/4/19 11:58:20
  13. 【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试

    原标题:【原油贵金属周评】伊朗局势升温,黄金多头跃跃欲试美国和伊朗的局势继续升温,市场风险情绪上升,避险黄金有向上突破阻力的迹象。原油方面稍显平稳,近期美国和OPEC加大供给及市场需求回落的影响,伊朗局势并未推升油价走强。近期中美贸易谈判摩擦再度升级,美国对中…...

    2024/4/19 11:58:32
  14. 【原油贵金属早评】市场情绪继续恶化,黄金上破

    原标题:【原油贵金属早评】市场情绪继续恶化,黄金上破周初中国针对于美国加征关税的进行的反制措施引发市场情绪的大幅波动,人民币汇率出现大幅的贬值动能,金融市场受到非常明显的冲击。尤其是波动率起来之后,对于股市的表现尤其不安。隔夜美国股市出现明显的下行走势,这…...

    2024/4/19 11:58:39
  15. 【外汇早评】美伊僵持,风险情绪继续升温

    原标题:【外汇早评】美伊僵持,风险情绪继续升温昨日沙特两艘油轮再次发生爆炸事件,导致波斯湾局势进一步恶化,市场担忧美伊可能会出现摩擦生火,避险品种获得支撑,黄金和日元大幅走强。美指受中美贸易问题影响而在低位震荡。继5月12日,四艘商船在阿联酋领海附近的阿曼湾、…...

    2024/4/19 11:58:51
  16. 【原油贵金属早评】贸易冲突导致需求低迷,油价弱势

    原标题:【原油贵金属早评】贸易冲突导致需求低迷,油价弱势近日虽然伊朗局势升温,中东地区几起油船被袭击事件影响,但油价并未走高,而是出于调整结构中。由于市场预期局势失控的可能性较低,而中美贸易问题导致的全球经济衰退风险更大,需求会持续低迷,因此油价调整压力较…...

    2024/4/19 18:09:34
  17. 氧生福地 玩美北湖(上)——为时光守候两千年

    原标题:氧生福地 玩美北湖(上)——为时光守候两千年一次说走就走的旅行,只有一张高铁票的距离~ 所以,湖南郴州,我来了~ 从广州南站出发,一个半小时就到达郴州西站了。在动车上,同时改票的南风兄和我居然被分到了一个车厢,所以一路非常愉快地聊了过来。 挺好,最起…...

    2024/4/19 11:59:15
  18. 氧生福地 玩美北湖(中)——永春梯田里的美与鲜

    原标题:氧生福地 玩美北湖(中)——永春梯田里的美与鲜一觉醒来,因为大家太爱“美”照,在柳毅山庄去寻找龙女而错过了早餐时间。近十点,向导坏坏还是带着饥肠辘辘的我们去吃郴州最富有盛名的“鱼头粉”。说这是“十二分推荐”,到郴州必吃的美食之一。 哇塞!那个味美香甜…...

    2024/4/19 11:59:23
  19. 氧生福地 玩美北湖(下)——奔跑吧骚年!

    原标题:氧生福地 玩美北湖(下)——奔跑吧骚年!让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 让我们红尘做伴 活得潇潇洒洒 策马奔腾共享人世繁华 对酒当歌唱出心中喜悦 轰轰烈烈把握青春年华 啊……啊……啊 两…...

    2024/4/19 11:59:44
  20. 扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!

    原标题:扒开伪装医用面膜,翻六倍价格宰客,小姐姐注意了!扒开伪装医用面膜,翻六倍价格宰客!当行业里的某一品项火爆了,就会有很多商家蹭热度,装逼忽悠,最近火爆朋友圈的医用面膜,被沾上了污点,到底怎么回事呢? “比普通面膜安全、效果好!痘痘、痘印、敏感肌都能用…...

    2024/4/19 11:59:48
  21. 「发现」铁皮石斛仙草之神奇功效用于医用面膜

    原标题:「发现」铁皮石斛仙草之神奇功效用于医用面膜丽彦妆铁皮石斛医用面膜|石斛多糖无菌修护补水贴19大优势: 1、铁皮石斛:自唐宋以来,一直被列为皇室贡品,铁皮石斛生于海拔1600米的悬崖峭壁之上,繁殖力差,产量极低,所以古代仅供皇室、贵族享用 2、铁皮石斛自古民间…...

    2024/4/19 12:00:06
  22. 丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者

    原标题:丽彦妆\医用面膜\冷敷贴轻奢医学护肤引导者【公司简介】 广州华彬企业隶属香港华彬集团有限公司,专注美业21年,其旗下品牌: 「圣茵美」私密荷尔蒙抗衰,产后修复 「圣仪轩」私密荷尔蒙抗衰,产后修复 「花茵莳」私密荷尔蒙抗衰,产后修复 「丽彦妆」专注医学护…...

    2024/4/19 16:57:22
  23. 广州械字号面膜生产厂家OEM/ODM4项须知!

    原标题:广州械字号面膜生产厂家OEM/ODM4项须知!广州械字号面膜生产厂家OEM/ODM流程及注意事项解读: 械字号医用面膜,其实在我国并没有严格的定义,通常我们说的医美面膜指的应该是一种「医用敷料」,也就是说,医用面膜其实算作「医疗器械」的一种,又称「医用冷敷贴」。 …...

    2024/4/19 12:00:25
  24. 械字号医用眼膜缓解用眼过度到底有无作用?

    原标题:械字号医用眼膜缓解用眼过度到底有无作用?医用眼膜/械字号眼膜/医用冷敷眼贴 凝胶层为亲水高分子材料,含70%以上的水分。体表皮肤温度传导到本产品的凝胶层,热量被凝胶内水分子吸收,通过水分的蒸发带走大量的热量,可迅速地降低体表皮肤局部温度,减轻局部皮肤的灼…...

    2024/4/19 12:00:40
  25. 配置失败还原请勿关闭计算机,电脑开机屏幕上面显示,配置失败还原更改 请勿关闭计算机 开不了机 这个问题怎么办...

    解析如下&#xff1a;1、长按电脑电源键直至关机&#xff0c;然后再按一次电源健重启电脑&#xff0c;按F8健进入安全模式2、安全模式下进入Windows系统桌面后&#xff0c;按住“winR”打开运行窗口&#xff0c;输入“services.msc”打开服务设置3、在服务界面&#xff0c;选中…...

    2022/11/19 21:17:18
  26. 错误使用 reshape要执行 RESHAPE,请勿更改元素数目。

    %读入6幅图像&#xff08;每一幅图像的大小是564*564&#xff09; f1 imread(WashingtonDC_Band1_564.tif); subplot(3,2,1),imshow(f1); f2 imread(WashingtonDC_Band2_564.tif); subplot(3,2,2),imshow(f2); f3 imread(WashingtonDC_Band3_564.tif); subplot(3,2,3),imsho…...

    2022/11/19 21:17:16
  27. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机...

    win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”问题的解决方法在win7系统关机时如果有升级系统的或者其他需要会直接进入一个 等待界面&#xff0c;在等待界面中我们需要等待操作结束才能关机&#xff0c;虽然这比较麻烦&#xff0c;但是对系统进行配置和升级…...

    2022/11/19 21:17:15
  28. 台式电脑显示配置100%请勿关闭计算机,“准备配置windows 请勿关闭计算机”的解决方法...

    有不少用户在重装Win7系统或更新系统后会遇到“准备配置windows&#xff0c;请勿关闭计算机”的提示&#xff0c;要过很久才能进入系统&#xff0c;有的用户甚至几个小时也无法进入&#xff0c;下面就教大家这个问题的解决方法。第一种方法&#xff1a;我们首先在左下角的“开始…...

    2022/11/19 21:17:14
  29. win7 正在配置 请勿关闭计算机,怎么办Win7开机显示正在配置Windows Update请勿关机...

    置信有很多用户都跟小编一样遇到过这样的问题&#xff0c;电脑时发现开机屏幕显现“正在配置Windows Update&#xff0c;请勿关机”(如下图所示)&#xff0c;而且还需求等大约5分钟才干进入系统。这是怎样回事呢&#xff1f;一切都是正常操作的&#xff0c;为什么开时机呈现“正…...

    2022/11/19 21:17:13
  30. 准备配置windows 请勿关闭计算机 蓝屏,Win7开机总是出现提示“配置Windows请勿关机”...

    Win7系统开机启动时总是出现“配置Windows请勿关机”的提示&#xff0c;没过几秒后电脑自动重启&#xff0c;每次开机都这样无法进入系统&#xff0c;此时碰到这种现象的用户就可以使用以下5种方法解决问题。方法一&#xff1a;开机按下F8&#xff0c;在出现的Windows高级启动选…...

    2022/11/19 21:17:12
  31. 准备windows请勿关闭计算机要多久,windows10系统提示正在准备windows请勿关闭计算机怎么办...

    有不少windows10系统用户反映说碰到这样一个情况&#xff0c;就是电脑提示正在准备windows请勿关闭计算机&#xff0c;碰到这样的问题该怎么解决呢&#xff0c;现在小编就给大家分享一下windows10系统提示正在准备windows请勿关闭计算机的具体第一种方法&#xff1a;1、2、依次…...

    2022/11/19 21:17:11
  32. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”的解决方法...

    今天和大家分享一下win7系统重装了Win7旗舰版系统后&#xff0c;每次关机的时候桌面上都会显示一个“配置Windows Update的界面&#xff0c;提示请勿关闭计算机”&#xff0c;每次停留好几分钟才能正常关机&#xff0c;导致什么情况引起的呢&#xff1f;出现配置Windows Update…...

    2022/11/19 21:17:10
  33. 电脑桌面一直是清理请关闭计算机,windows7一直卡在清理 请勿关闭计算机-win7清理请勿关机,win7配置更新35%不动...

    只能是等着&#xff0c;别无他法。说是卡着如果你看硬盘灯应该在读写。如果从 Win 10 无法正常回滚&#xff0c;只能是考虑备份数据后重装系统了。解决来方案一&#xff1a;管理员运行cmd&#xff1a;net stop WuAuServcd %windir%ren SoftwareDistribution SDoldnet start WuA…...

    2022/11/19 21:17:09
  34. 计算机配置更新不起,电脑提示“配置Windows Update请勿关闭计算机”怎么办?

    原标题&#xff1a;电脑提示“配置Windows Update请勿关闭计算机”怎么办&#xff1f;win7系统中在开机与关闭的时候总是显示“配置windows update请勿关闭计算机”相信有不少朋友都曾遇到过一次两次还能忍但经常遇到就叫人感到心烦了遇到这种问题怎么办呢&#xff1f;一般的方…...

    2022/11/19 21:17:08
  35. 计算机正在配置无法关机,关机提示 windows7 正在配置windows 请勿关闭计算机 ,然后等了一晚上也没有关掉。现在电脑无法正常关机...

    关机提示 windows7 正在配置windows 请勿关闭计算机 &#xff0c;然后等了一晚上也没有关掉。现在电脑无法正常关机以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;关机提示 windows7 正在配…...

    2022/11/19 21:17:05
  36. 钉钉提示请勿通过开发者调试模式_钉钉请勿通过开发者调试模式是真的吗好不好用...

    钉钉请勿通过开发者调试模式是真的吗好不好用 更新时间:2020-04-20 22:24:19 浏览次数:729次 区域: 南阳 > 卧龙 列举网提醒您:为保障您的权益,请不要提前支付任何费用! 虚拟位置外设器!!轨迹模拟&虚拟位置外设神器 专业用于:钉钉,外勤365,红圈通,企业微信和…...

    2022/11/19 21:17:05
  37. 配置失败还原请勿关闭计算机怎么办,win7系统出现“配置windows update失败 还原更改 请勿关闭计算机”,长时间没反应,无法进入系统的解决方案...

    前几天班里有位学生电脑(windows 7系统)出问题了&#xff0c;具体表现是开机时一直停留在“配置windows update失败 还原更改 请勿关闭计算机”这个界面&#xff0c;长时间没反应&#xff0c;无法进入系统。这个问题原来帮其他同学也解决过&#xff0c;网上搜了不少资料&#x…...

    2022/11/19 21:17:04
  38. 一个电脑无法关闭计算机你应该怎么办,电脑显示“清理请勿关闭计算机”怎么办?...

    本文为你提供了3个有效解决电脑显示“清理请勿关闭计算机”问题的方法&#xff0c;并在最后教给你1种保护系统安全的好方法&#xff0c;一起来看看&#xff01;电脑出现“清理请勿关闭计算机”在Windows 7(SP1)和Windows Server 2008 R2 SP1中&#xff0c;添加了1个新功能在“磁…...

    2022/11/19 21:17:03
  39. 请勿关闭计算机还原更改要多久,电脑显示:配置windows更新失败,正在还原更改,请勿关闭计算机怎么办...

    许多用户在长期不使用电脑的时候&#xff0c;开启电脑发现电脑显示&#xff1a;配置windows更新失败&#xff0c;正在还原更改&#xff0c;请勿关闭计算机。。.这要怎么办呢&#xff1f;下面小编就带着大家一起看看吧&#xff01;如果能够正常进入系统&#xff0c;建议您暂时移…...

    2022/11/19 21:17:02
  40. 还原更改请勿关闭计算机 要多久,配置windows update失败 还原更改 请勿关闭计算机,电脑开机后一直显示以...

    配置windows update失败 还原更改 请勿关闭计算机&#xff0c;电脑开机后一直显示以以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;配置windows update失败 还原更改 请勿关闭计算机&#x…...

    2022/11/19 21:17:01
  41. 电脑配置中请勿关闭计算机怎么办,准备配置windows请勿关闭计算机一直显示怎么办【图解】...

    不知道大家有没有遇到过这样的一个问题&#xff0c;就是我们的win7系统在关机的时候&#xff0c;总是喜欢显示“准备配置windows&#xff0c;请勿关机”这样的一个页面&#xff0c;没有什么大碍&#xff0c;但是如果一直等着的话就要两个小时甚至更久都关不了机&#xff0c;非常…...

    2022/11/19 21:17:00
  42. 正在准备配置请勿关闭计算机,正在准备配置windows请勿关闭计算机时间长了解决教程...

    当电脑出现正在准备配置windows请勿关闭计算机时&#xff0c;一般是您正对windows进行升级&#xff0c;但是这个要是长时间没有反应&#xff0c;我们不能再傻等下去了。可能是电脑出了别的问题了&#xff0c;来看看教程的说法。正在准备配置windows请勿关闭计算机时间长了方法一…...

    2022/11/19 21:16:59
  43. 配置失败还原请勿关闭计算机,配置Windows Update失败,还原更改请勿关闭计算机...

    我们使用电脑的过程中有时会遇到这种情况&#xff0c;当我们打开电脑之后&#xff0c;发现一直停留在一个界面&#xff1a;“配置Windows Update失败&#xff0c;还原更改请勿关闭计算机”&#xff0c;等了许久还是无法进入系统。如果我们遇到此类问题应该如何解决呢&#xff0…...

    2022/11/19 21:16:58
  44. 如何在iPhone上关闭“请勿打扰”

    Apple’s “Do Not Disturb While Driving” is a potentially lifesaving iPhone feature, but it doesn’t always turn on automatically at the appropriate time. For example, you might be a passenger in a moving car, but your iPhone may think you’re the one dri…...

    2022/11/19 21:16:57