8.spring对logback的支持

文章目录

  • 一、入口
  • 二、源码解析
    • LoggingApplicationListener
  • 三、其它支持
  • 四、总结

本节以logback为背景介绍的

一、入口

gav: org.springframework.boot:spring-boot:3.3.4

spring.factories文件中有如下两个配置

org.springframework.boot.logging.LoggingSystemFactory=\
org.springframework.boot.logging.logback.LogbackLoggingSystem.Factory,\
org.springframework.boot.logging.log4j2.Log4J2LoggingSystem.Factory,\
org.springframework.boot.logging.java.JavaLoggingSystem.Factoryorg.springframework.context.ApplicationListener=\
org.springframework.boot.context.logging.LoggingApplicationListener,\
// 省略其它的...

这里定义了一个ApplicationListener的监听器, 以及三种不同日志实现的工厂

说明: 本节只分析使用logback作为slf4j实现的场景

二、源码解析

LoggingApplicationListener

继承链: GenericApplicationListener -> GenericApplicationListener -> SmartApplicationListener -> ApplicationListener

监听的事件为ApplicationEvent

public class LoggingApplicationListener implements GenericApplicationListener {// 触发事件public void onApplicationEvent(ApplicationEvent event) {// 启动初期触发if (event instanceof ApplicationStartingEvent startingEvent) {onApplicationStartingEvent(startingEvent);}// 环境准备之后触发else if (event instanceof ApplicationEnvironmentPreparedEvent environmentPreparedEvent) {onApplicationEnvironmentPreparedEvent(environmentPreparedEvent);}// 容器启动完成触发else if (event instanceof ApplicationPreparedEvent preparedEvent) {onApplicationPreparedEvent(preparedEvent);}// 容器关闭触发else if (event instanceof ContextClosedEvent contextClosedEvent) {onContextClosedEvent(contextClosedEvent);}// 容器启动失败触发else if (event instanceof ApplicationFailedEvent) {onApplicationFailedEvent();}}
}

容器启动事件

private void onApplicationStartingEvent(ApplicationStartingEvent event) {// 实例化LoggingSystem对象this.loggingSystem = LoggingSystem.get(event.getSpringApplication().getClassLoader());// 初始化前置处理this.loggingSystem.beforeInitialize();
}// LoggingSystem.get
public static LoggingSystem get(ClassLoader classLoader) {// 系统配置的LoggingSystem; key:org.springframework.boot.logging.LoggingSystemString loggingSystemClassName = System.getProperty(SYSTEM_PROPERTY);if (StringUtils.hasLength(loggingSystemClassName)) {if (NONE.equals(loggingSystemClassName)) {return new NoOpLoggingSystem();}return get(classLoader, loggingSystemClassName);}// SPI获取LoggingSystem, 顺序是LogbackLoggingSystem->Log4J2LoggingSystem->JavaLoggingSystemLoggingSystem loggingSystem = SYSTEM_FACTORY.getLoggingSystem(classLoader);Assert.state(loggingSystem != null, "No suitable logging system located");return loggingSystem;
}// LogbackLoggingSystem#beforeInitialize
@Override
public void beforeInitialize() {// 获取logContext日志上下文LoggerContext loggerContext = getLoggerContext();if (isAlreadyInitialized(loggerContext)) {return;}super.beforeInitialize();configureJdkLoggingBridgeHandler();loggerContext.getTurboFilterList().add(FILTER);
}
// 获取logContext日志上下文
private LoggerContext getLoggerContext() {ILoggerFactory factory = getLoggerFactory();// ....return (LoggerContext) factory;
}
// 获取logContext日志上下文
private ILoggerFactory getLoggerFactory() {// slf4j获取LoggerContextILoggerFactory factory = LoggerFactory.getILoggerFactory();while (factory instanceof SubstituteLoggerFactory) {try {Thread.sleep(50);}catch (InterruptedException ex) {// 设置当前线程的中断标志位,表示该线程已被请求中断,但并不会立即停止线程的执行。Thread.currentThread().interrupt();throw new IllegalStateException("Interrupted while waiting for non-substitute logger factory", ex);}factory = LoggerFactory.getILoggerFactory();}return factory;
}

方法小结

  1. 容器在启动时通过ApplicationStartingEvent事件创建日志上下文
  2. 可以通过系统属性配置LoggingSystem对象, key为org.springframework.boot.logging.LoggingSystem
  3. 如果没有指定使用的LoggingSystem, 那么通过SPI获取, 由于在spring.factories中配置的LoggingSystemFactory里面LogbackLoggingSystem.Factory在第一个, 所以默认使用的LogbackLoggingSystem.Factory(如果有logback相关包的话)
  4. 执行LogbackLoggingSystem的beforeInitialize进行前置初始化
  5. beforeInitialize中使用SLF4J创建日志上下文; 这里就是SL4FJ和logback的内容了, 通过前面文章的介绍, 大家应该很熟悉了

在容器启动时创建了LoggingSystem, 一般是LogbackLoggingSystem, 同时创建了日志上下文LogContext

环境准备事件

private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {SpringApplication springApplication = event.getSpringApplication();// 容器启动事件中创建过了, 一般是LogbackLoggingSystemif (this.loggingSystem == null) {this.loggingSystem = LoggingSystem.get(springApplication.getClassLoader());}// 进行初始化initialize(event.getEnvironment(), springApplication.getClassLoader());
}protected void initialize(ConfigurableEnvironment environment, ClassLoader classLoader) {// 这里创建LogbackLoggingSystemProperties, 用于给日志上下文添加必要的属性getLoggingSystemProperties(environment).apply();// 从环境变量中获取logging.file.name和logging.file.path, 然后构建LogFilethis.logFile = LogFile.get(environment);if (this.logFile != null) {// 将logging.file.path的值添加到系统属性中, key为LOG_PATH// 将logging.file.path目录下spring.log文件的路径添加到系统属性中, key为LOG_FILEthis.logFile.applyToSystemProperties();}this.loggerGroups = new LoggerGroups(DEFAULT_GROUP_LOGGERS);// 设置spring启动时的日志等级, 如果环境变量中有debug, 那么是debug登记, // 如果环境变量中有trace, 那么是trace等级initializeEarlyLoggingLevel(environment);// 初始化LogbackLoggingSysteminitializeSystem(environment, this.loggingSystem, this.logFile);// 环境变量中获取logging.group的内容添加到loggerGroups中, 并设置spring启动时相关包的日志等级initializeFinalLoggingLevels(environment, this.loggingSystem);// 添加shutdown的回调registerShutdownHookIfNecessary(environment, this.loggingSystem);
}private void initializeSystem(ConfigurableEnvironment environment, LoggingSystem system, LogFile logFile) {// 环境变量中的logging.config, 指定的日志配置文件路径String logConfig = environment.getProperty(CONFIG_PROPERTY);if (StringUtils.hasLength(logConfig)) {// 去掉字符串两端的空格logConfig = logConfig.strip();}try {// 就封装了一个environment的getter方法LoggingInitializationContext initializationContext = new LoggingInitializationContext(environment);// 没有配置logging.config或者以-D开头if (ignoreLogConfig(logConfig)) {// LogbackLoggingSystem初始化system.initialize(initializationContext, null, logFile);}else {// LogbackLoggingSystem初始化system.initialize(initializationContext, logConfig, logFile);}}catch (Throwable ex) {// ...}
}

小结

  1. springboot在环境准备完成后发出ApplicationEnvironmentPreparedEvent事件, 然后开始对LogbackLoggingSystemProperties进行初始化
  2. 创建LogbackLoggingSystemProperties对象, 并添加系统变量值, 下面是添加的内容
  • LOGGED_APPLICATION_NAME:spring.application.name的值
  • PID: pid的值
  • CONSOLE_LOG_CHARSET: 环境变量中logging.charset.console的值, 默认是U8
  • FILE_LOG_CHARSET: 环境变量中logging.charset.file的值, 默认是U8
  • CONSOLE_LOG_THRESHOLD: 环境变量中logging.threshold.console的值, 可选true/false
  • LOG_EXCEPTION_CONVERSION_WORD: 环境变量中logging.exception-conversion-word的值
  • CONSOLE_LOG_PATTERN: 环境变量中logging.pattern.console的值
  • FILE_LOG_PATTERN: 环境变量中logging.pattern.file的值
  • LOG_LEVEL_PATTERN:环境变量中logging.pattern.level的值
  • LOG_DATEFORMAT_PATTERN: 环境变量中logging.pattern.dateformat的值
  • LOG_CORRELATION_PATTERN: 环境变量中logging.pattern.correlation的值
  • 如果环境变量中logging.file.name存在, 添加LOG_FILE: file的值 到系统变量中
  • 如果环境变量中logging.file.path存在, 添加LOG_PATH: path的值 到系统变量中
  1. 设置springboot的日志等级, 如果系统变量中有debug值, 设置为debug等级, 如果有trace值, 设置为trace等级
  2. 可以在系统变量中使用logging.config指定日志文件路径, 也可以不指定使用默认的logback.xml, 然后进行LogbackLoggingSystem的初始化
  3. 设置一些包/类的日志等级, 该等级由第3步即系统变量中有debug值或者trace值来设置, 可以配置的内置模块日志等级的有如下几个
  • 如果环境变量中有debug, 那么设置包sql相关的包org.springframework.jdbc.core, org.hibernate.SQL,org.jooq.tools.LoggerListener和web相关的包org.springframework.core.codec, org.springframework.http,org.springframework.web,org.springframework.boot.actuate.endpoint.web,org.springframework.boot.web.servlet.ServletContextInitializerBeans的日志级别为debug
  • 如果环境变量中有trace, 那么设置包org.springframeworkorg.apache.tomcat,org.apache.catalina,org.eclipse.jetty,org.hibernate.tool.hbm2ddl的日志级别为trace
  • 如果环境变量中有logging.level, 那么设置指定web或者sql的日志等级为配置的日志等级, logging.level可以这么配置
logging.level.web=info
logging.level.org.springframework.boot=info
## 等等与上面环境变量中可配置的包相同

当然处理默认的web和sql两种类型的包之外, 还可以使用环境变量logging.group来自定义springboot中包或者类的日志级别

这里环境变量中logging.level的优先级要高于debug的配置

LogbackLoggingSystem的初始化

public void initialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) {// 容器启动事件中创建的日志上下文LoggerContext loggerContext = getLoggerContext();if (isAlreadyInitialized(loggerContext)) {return;}// 非aot环境下直接返回false, 那么这里就是true, 这里对aot环境下不考虑if (!initializeFromAotGeneratedArtifactsIfPossible(initializationContext, logFile)) {// 初始化的核心super.initialize(initializationContext, configLocation, logFile);}// 环境上下文添加到日志上下文中loggerContext.putObject(Environment.class.getName(), initializationContext.getEnvironment());loggerContext.getTurboFilterList().remove(FILTER);// 标记为初始化完成markAsInitialized(loggerContext);if (StringUtils.hasText(System.getProperty(CONFIGURATION_FILE_PROPERTY))) {getLogger(LogbackLoggingSystem.class.getName()).warn("Ignoring '" + CONFIGURATION_FILE_PROPERTY+ "' system property. Please use 'logging.config' instead.");}
}

如果没有开启aot, 那么这个方法没有什么内容, 直接看AbstractLoggingSystem#initialize方法即可

AbstractLoggingSystem

public void initialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) {// 环境变量中没有使用logging.config指定日志文件路径的话走这里if (StringUtils.hasLength(configLocation)) {initializeWithSpecificConfig(initializationContext, configLocation, logFile);return;}// 指定日志文件路径的话走这里initializeWithConventions(initializationContext, logFile);
}// 使用logging.config指定日志文件路径的场景
private void initializeWithSpecificConfig(LoggingInitializationContext initializationContext, String configLocation,LogFile logFile) {// 使用系统属性中的值替换configLocation中的占位符configLocation = SystemPropertyUtils.resolvePlaceholders(configLocation);loadConfiguration(initializationContext, configLocation, logFile);
}// 没有指定日志文件路径的场景
private void initializeWithConventions(LoggingInitializationContext initializationContext, LogFile logFile) {// logback支持的文件名,只取一个, 顺序为:"logback-test.groovy", "logback-test.xml", "logback.groovy", "logback.xml" String config = getSelfInitializationConfig();// 存在上面这几种文件的话if (config != null && logFile == null) {// 重置容器状态, 并调用loadConfiguration方法开始解析配置reinitialize(initializationContext);return;}// 项目中没有配置默认的四个文件if (config == null) {// 这里获取spring扩展的四个文件名, 顺序为: "logback-test-spring.groovy", "logback-test-spring.xml", "logback-spring.groovy", "logback-spring.xml" config = getSpringInitializationConfig();}// 存在配置文件的话if (config != null) {// 解析配置文件loadConfiguration(initializationContext, config, logFile);return;}// 使用一套默认的配置, appender仅为ConsoleAppender, 这里不做解释loadDefaults(initializationContext, logFile);
}

方法小结

  1. 如果使用logging.config指定了日志文件的路径(路径支持使用占位符, 将从系统变量中获取变量值), 使用loadConfiguration方法进行日志文件解析
  2. 如果没有指定日志文件的路径, 那么先获取默认配置文件(“logback-test.groovy”, “logback-test.xml”, “logback.groovy”, “logback.xml” ), 如果没有默认的配置文件, 取带有spring后缀的日志文件(“logback-test-spring.groovy”, “logback-test-spring.xml”, “logback-spring.groovy”, “logback-spring.xml” )
  3. 如果配置文件存在, 使用loadConfiguration方法进行日志文件解析
  4. 如果没有配置文件, 那么使用logback默认的容器, 以及一个ConsoleAppender

解析配置

protected void loadConfiguration(LoggingInitializationContext initializationContext, String location,LogFile logFile) {// 日志上下文LoggerContext loggerContext = getLoggerContext();// 停止并重启; 如果你的springBoot启动类中有静态属性Logger使用LoggerFactory.getLogger获取的话,它会在spring启动之前执行, 这里就会存在一个loggerContext, 需要关闭stopAndReset(loggerContext);withLoggingSuppressed(() -> {// initializationContext对象仅仅是环境上下文的载体, 提供getEnvironment方法if (initializationContext != null) {// 创建LogbackLoggingSystemProperties对象, 并将一堆环境变量添加到系统变量中, 上面的环境准备事件中有介绍applySystemProperties(initializationContext.getEnvironment(), logFile);}try {// 配置的日志文件资源Resource resource = new ApplicationResourceLoader().getResource(location);// 解析日志配置文件configureByResourceUrl(initializationContext, loggerContext, resource.getURL());}catch (Exception ex) {throw new IllegalStateException("Could not initialize Logback logging from " + location, ex);}// 标识日志容器被启动loggerContext.start();});// 打印解析异常信息, 略过reportConfigurationErrorsIfNecessary(loggerContext);
}private void configureByResourceUrl(LoggingInitializationContext initializationContext, LoggerContext loggerContext,URL url) throws JoranException {// 只允许xml为后缀的文件if (url.getPath().endsWith(".xml")) {// 使用springboot视线的SpringBootJoranConfigurator来解析配置JoranConfigurator configurator = new SpringBootJoranConfigurator(initializationContext);configurator.setContext(loggerContext);// 开始解析配置configurator.doConfigure(url);}else {throw new IllegalArgumentException("Unsupported file extension in '" + url + "'. Only .xml is supported");}
}

方法小结

  1. 添加一些环境变量参数到系统变量中
  2. 配置文件仅支持xml结尾的文件, 然后使用SpringBootJoranConfigurator来解析日志配置文件, 这里是对JoranConfigurator的扩展

SpringBootJoranConfigurator

class SpringBootJoranConfigurator extends JoranConfigurator {private final LoggingInitializationContext initializationContext;SpringBootJoranConfigurator(LoggingInitializationContext initializationContext) {this.initializationContext = initializationContext;}@Overrideprotected void addModelHandlerAssociations(DefaultProcessor defaultProcessor) {// 添加处理configuration/springProperty的handlerdefaultProcessor.addHandler(SpringPropertyModel.class,(handlerContext, handlerMic) -> new SpringPropertyModelHandler(this.context,this.initializationContext.getEnvironment()));// 添加处理*/springProfile的handlerdefaultProcessor.addHandler(SpringProfileModel.class,(handlerContext, handlerMic) -> new SpringProfileModelHandler(this.context,this.initializationContext.getEnvironment()));super.addModelHandlerAssociations(defaultProcessor);}@Overridepublic void addElementSelectorAndActionAssociations(RuleStore ruleStore) {super.addElementSelectorAndActionAssociations(ruleStore);// 添加允许的标签configuration/springPropertyruleStore.addRule(new ElementSelector("configuration/springProperty"), SpringPropertyAction::new);// 添加允许的标签*/springProfileruleStore.addRule(new ElementSelector("*/springProfile"), SpringProfileAction::new);ruleStore.addTransparentPathPart("springProfile");}@Overridepublic void buildModelInterpretationContext() {super.buildModelInterpretationContext();// modelInterpretationContext中的JoranConfigurator替换成SpringBootJoranConfiguratorthis.modelInterpretationContext.setConfiguratorSupplier(() -> {SpringBootJoranConfigurator configurator = new SpringBootJoranConfigurator(this.initializationContext);configurator.setContext(this.context);return configurator;});}// 省略一些代码...
}

SpringBootJoranConfigurator类在不考虑aot的情况下, 添加了对configuration/springProperty*/springProfile标签的支持, 其中*/springProfile是一种后缀标签的形式, 也就是说它可以放在任意标签的后面; 下面看看这两个handler

SpringPropertyModelHandler

@Override
public void handle(ModelInterpretationContext intercon, Model model) throws ModelHandlerException {SpringPropertyModel propertyModel = (SpringPropertyModel) model;// 作用域, 支持LOCAL(model上下文), CONTEXT(日志上下文), SYSTEM(系统级别); 默认是LOCAL, 在解析配置文件时有效Scope scope = ActionUtil.stringToScope(propertyModel.getScope());// 默认值String defaultValue = propertyModel.getDefaultValue();// source就是属性的名称String source = propertyModel.getSource();// name和source都不能为空if (OptionHelper.isNullOrEmpty(propertyModel.getName()) || OptionHelper.isNullOrEmpty(source)) {addError("The \"name\" and \"source\" attributes of <springProperty> must be set");}// 将属性添加到指定的作用域中PropertyModelHandlerHelper.setProperty(intercon, propertyModel.getName(), getValue(source, defaultValue),scope);
}
// 从环境变量中获取source属性对应的值
private String getValue(String source, String defaultValue) {if (this.environment == null) {addWarn("No Spring Environment available to resolve " + source);return defaultValue;}return this.environment.getProperty(source, defaultValue);
}

方法小结

  1. configuration/springProperty标签支持name,source,scope, defaultValue四个属性
  • name: 标签名称
  • source: 属性名称; 从环境变量中获取值的那个key
  • scope: 属性存放的位置, LOCAL:logback配置文件解析期间, CONTEXT:日志上线文范文内, SYSTEM: 系统属性

总的来说就是: configuration/springProperty将从环境变量中获取的值添加到日志容器中, 供解析日志使用, 其中key为name属性的值, value为source属性在环境变量中对应的值

例如:

// application.properties
log.fileName=info.log// logback.xml
<configuration><springProperty name="fileName" source="log.fileName" scope="LOCAL" defaultValue="temp.log"/>
</configuration>

SpringProfileModelHandler

class SpringProfileModelHandler extends ModelHandlerBase {private final Environment environment;@Overridepublic void handle(ModelInterpretationContext intercon, Model model) throws ModelHandlerException {SpringProfileModel profileModel = (SpringProfileModel) model;// 如果当前spring的环境(spring.profiles.active)不是springProfile指定下的, 那么被springProfile标签包裹的子标签将不会生效if (!acceptsProfiles(intercon, profileModel)) {model.deepMarkAsSkipped();}}private boolean acceptsProfiles(ModelInterpretationContext ic, SpringProfileModel model) {if (this.environment == null) {return false;}// name根据逗号分割String[] profileNames = StringUtils.trimArrayElements(StringUtils.commaDelimitedListToStringArray(model.getName()));if (profileNames.length == 0) {return false;}for (int i = 0; i < profileNames.length; i++) {try {// 从LOCAL、CONTEXT、SYSTEM范围内获取值替换占位符; 没有占位符的话用原值profileNames[i] = OptionHelper.substVars(profileNames[i], ic, this.context);}catch (ScanException ex) {throw new RuntimeException(ex);}}// 判断是不是环境变量中配置的spring.profiles.active的值return this.environment.acceptsProfiles(Profiles.of(profileNames));}
}

方法小结

springProfile标签可以放在任意子标签下, 其中name属性用来指定当前的环境, 它可以指定什么环境下使用什么样的配置, 如果当前环境与springProfile配置的不同, 那么springProfile的子标签将不会生效; 例如

<root level="info"><springProfile name="dev,test"><appender-ref ref="CONSOLE" /></springProfile><springProfile name="prod"><appender-ref ref="ROLLER" /></springProfile>
</root>

这种配置下dev或者test环境 CONSOLE的appender将会生效, ROLLER的appender不会生效

三、其它支持

spring还提供了ColorConverter,ExtendedWhitespaceThrowableProxyConverter,WhitespaceThrowableProxyConverter转换器, 用来给控制台输出颜色日志的、异常等

四、总结

  1. spring自动装配了LoggingApplicationListener监听器, 监听ApplicationEvent事件, 在springboot启动周期中对日志做了一些扩展
  • 在springboot启动初期(ApplicationStartingEvent事件), 实例化了LogbackLoggingSystem对象
  • 在环境准备完成后(ApplicationEnvironmentPreparedEvent事件), 对logback容器做了初始化并启动
  1. springboot对日志slf4j的实现默认顺序为LogbackLoggingSystem->Log4J2LoggingSystem->JavaLoggingSystem, 确保其中有ch.qos.logback:logback-classic:版本号的包

  2. 关于环境变量中配置的logging.file.namelogging.file.path属性, 是用来给默认日志配置设置滚动文件的, 就像appender中的file属性一样, 但是如果你配置了日志文件(例如logback.xml), 它就没什么用了

  3. 可以在环境变量中配置 debug=true或者trace=true来设置springboot内置包的日志等级; 同样也可以在环境变量中设置指定包的日志级别, 就不限于debug或者trace了, 例如 logging.level.web=info; logging.level.org.springframework.boot=info 这种logging.level.web=info的方式优先级高于debug=true

  4. 可以使用环境变量logging.config配置日志文件的位置, 支持classpath的配置, 即放在项目的resources目录下即可;

  • 如过没有使用logging.config指定日志配置, 那么会默认读取"logback-test.groovy", "logback-test.xml", "logback.groovy", "logback.xml"中的一个
  • 如果这一步也没有指定, 那么读取springboot扩展的配置文件"logback-test-spring.groovy", "logback-test-spring.xml", "logback-spring.groovy", "logback-spring.xml"
  • 如果直接没有配置文件, 那么默认构建ConsoleAppender和root的logger对象, 如果配置了logging.file.namelogging.file.path属性, 那么就会多创建一个info级别的RollingFileAppender
  1. springboot使用SpringBootJoranConfigurator扩展了JoranConfigurator, 添加了如下的相关支持
  • configuration/springProperty标签, 用来从环境变量中获取属性, 作用到解析日志文件中
  • */springProfile标签, 该标签是后缀匹配型, 可以放在任意位置, 它用于指定哪些配置在不同的环境下生效
  1. springboot还提供了额外的转换器, 例如ColorConverter, 大家配置ConsoleAppender的时候可以借用它
  2. 另外, 在遇到Thread.sleep的时候, 可以用Thread.currentThread().interrupt();设置当前线程的中断标志位,表示该线程已被请求中断,但并不会立即停止线程的执行。也曾看到一些地方什么都不处理, 这种应该是标准做法, 在几个源码中见过了

个人公众号
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/23796.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

OpenHarmony分布式数据管理子系统

OpenHarmony分布式数据管理子系统 简介 目录 组件说明 分布式数据对象数据共享分布式数据服务Key-Value数据库首选项关系型数据库标准数据化通路 相关仓 简介 子系统介绍 分布式数据管理子系统支持单设备的各种结构化数据的持久化&#xff0c;以及跨设备之间数据的同步、…

智慧后勤的消防管理:豪越科技为安全护航

智慧后勤消防管理难题大揭秘&#xff01; 在智慧后勤发展得如火如荼的当下&#xff0c;消防管理却暗藏诸多难题。传统模式下&#xff0c;消防设施分布得那叫一个散&#xff0c;就像一盘散沙&#xff0c;管理起来超费劲。人工巡检不仅效率低&#xff0c;还容易遗漏&#xff0c;不…

python轻量级框架-flask

flask简述 Flask 是 Python 生态圈中一个基于 Python 的Web 框架。其轻量、模块化和易于扩展的特点导致其被广泛使用&#xff0c;适合快速开发 Web 应用以及构建小型到中型项目。它提供了开发 Web 应用最基础的工具和组件。之所以称为微框架&#xff0c;是因为它与一些大型 We…

政安晨【零基础玩转各类开源AI项目】DeepSeek 多模态大模型Janus-Pro-7B,本地部署!支持图像识别和图像生成

政安晨的个人主页&#xff1a;政安晨 欢迎 &#x1f44d;点赞✍评论⭐收藏 希望政安晨的博客能够对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出指正&#xff01; 目录 下载项目 创建虚拟环境 安装项目依赖 安装 Gradio&#xff08;UI&#xff09; 运…

在 Mac mini M2 上本地部署 DeepSeek-R1:14B:使用 Ollama 和 Chatbox 的完整指南

随着人工智能技术的飞速发展&#xff0c;本地部署大型语言模型&#xff08;LLM&#xff09;已成为许多技术爱好者的热门选择。本地部署不仅能够保护隐私&#xff0c;还能提供更灵活的使用体验。本文将详细介绍如何在 Mac mini M2&#xff08;24GB 内存&#xff09;上部署 DeepS…

【Godot4.3】基于绘图函数的矢量蒙版效果与UV换算

概述 在设计圆角容器时突发奇想&#xff1a; 将圆角矩形的每个顶点坐标除以对应圆角矩形所在Rect2的size&#xff0c;就得到了顶点对应的UV坐标。然后使用draw_colored_polygon&#xff0c;便可以做到用图片填充圆角矩形的效果。而且这种计算的效果就是图片随着其填充的图像缩…

51单片机-AT24CXX存储器工作原理

1、AT24CXX存储器工作原理 1.1、特点&#xff1a; 与400KHz&#xff0c;I2C总线兼容1.8到6.0伏工作电压范围低功耗CMOS技术写保护功能当WP为高电平时进入写保护状态页写缓冲器自定时擦写周期100万次编程/擦除周期可保存数据100年8脚DIP SOIC或TSSOP封装温度范围商业级和工业级…

Linux网络 网络层

IP 协议 协议头格式 4 位版本号(version): 指定 IP 协议的版本, 对于 IPv4 来说, 就是 4. 4 位头部长度(header length): IP 头部的长度是多少个 32bit, 也就是 4 字节&#xff0c;4bit 表示最大的数字是 15, 因此 IP 头部最大长度是 60 字节. 8 位服务类型(Type Of Service):…

Unity百游修炼(1)——FootBall详细制作全流程

一、引言 游玩测试&#xff1a; Football 游玩测试 1.项目背景与动机 背景&#xff1a;在学习 Unity 的过程中&#xff0c;希望通过实际项目来巩固所学知识&#xff0c;同时出于对休闲小游戏的喜爱&#xff0c;决定开发一款简单有趣的小游戏加深自己的所学知识点。 动机&#…

C语言(13)------------>do-while循环

1.do-while循环的语法 我们知道C语言有三大结构&#xff0c;顺序、选择、循环。我们可以使用while循环、for循环、do-while循环实现循环结构。之前的博客中提及到了前两者的技术实现。可以参考&#xff1a; C语言&#xff08;11&#xff09;-------------&#xff1e;while循…

【1】VS Code 新建上位机项目---C#基础语法

VS Code 新建上位机项目---C#基础语法 1 基本概念1.1 准备工具1.2 新建项目2 C#编程基础2.1 命名空间和类2.2 数据类型2.3 控制台输入输出2.3.1 输入输出: write 与 read2.3.2 格式化 : string.Foramt() 与 $2.3.3 赋值与运算2.4 类型转换2.4.1 数值类型之间的转换:(int)2.4…

SQL:DQL数据查询语言以及系统函数(oracle)

SQL Structured Query Language&#xff0c;结构化查询语言, 是一种用于管理和操作关系数据库的标准编程语言。 sql的分类 DQL&#xff08;Data Query Language&#xff09;&#xff1a;数据查询语言 DDL&#xff08;Data Definition Language&#xff09;&#xff1a;数据…

从单片机的启动说起一个单片机到点灯发生了什么下——使用GPIO点一个灯

目录 前言 HAL库对GPIO的抽象 核心分析&#xff1a;HAL_GPIO_Init 前言 我们终于到达了熟悉的地方&#xff0c;对GPIO的初始化。经过漫长的铺垫&#xff0c;我们终于历经千辛万苦&#xff0c;来到了这里。关于GPIO的八种模式等更加详细的细节&#xff0c;由于只是点个灯&am…

提效10倍:基于Paimon+Dolphin湖仓一体新架构在阿里妈妈品牌业务探索实践

1. 业务背景 阿里妈妈品牌广告数据包括投放引擎、下发、曝光、点击等日志&#xff0c;面向运筹调控、算法特征、分析报表、诊断监控等应用场景&#xff0c;进行了品牌数仓能力建设。随着业务发展&#xff0c;基于Lambda架构的数仓开发模式&#xff0c;缺陷日益突出&#xff1a;…

一文详解U盘启动UEFI/Legacy方式以及GPT/MBR关系

对于装系统的老手而说一直想研究一下装系统的原理&#xff0c;以及面对一些问题时的解决思路&#xff0c;故对以前的方法进行原理上的解释&#xff0c;主要想理解其底层原理。 引导模式 MBR分区可以同时支持UEFI和Legacy引导&#xff0c;我们可以看一下微pe制作的启动盘&#…

基于Docker的前端环境管理:从开发环境到生产部署的实现方案

# 基于Docker的前端环境管理&#xff1a;从开发环境到生产部署的实现方案 简介及前端开发环境挑战 简介 是一种容器化平台&#xff0c;可以将应用程序及其依赖项打包为一个容器&#xff0c;提供一种轻量级、可移植的环境。它能够简化开发、部署和运维的流程&#xff0c;提高…

连锁管理系统的五大核心设计及 PHP 源码分享

在当今竞争激烈的连锁商业领域&#xff0c;高效的管理系统是企业脱颖而出的关键。商淘云连锁管理系统凭借其卓越的功能和灵活的架构&#xff0c;为连锁企业提供了强大的运营支持。在这里详细介绍其五大核心设计&#xff0c;并分享相关的 PHP 源码示例。 一、总部进销存管理 &a…

C语言基本知识------指针(4)

1. 回调函数是什么&#xff1f; 回调函数就是⼀个通过函数指针调用的函数。 如果你把函数的指针&#xff08;地址&#xff09;作为参数传递给另⼀个函数&#xff0c;当这个指针被⽤来调⽤其所指向的函数 时&#xff0c;被调⽤的函数就是回调函数。 void qsort(void base,//指针…

MTK-Android13-包安装器PackageInstaller 静默安装实现

目的 我们最终是为了搞明白安装的整个流程。一方面通过安卓系统自带的包安装器来了解PMS 安装流程&#xff1b;另一方面熟悉框架层Framework 针对Android apk 安装流程。 前两篇文章分析了PackagerInstaller 安装流程。 Android13-包安装器PackageInstaller-之apk安装跳转 An…

qt-C++笔记之创建和初始化 `QGraphicsScene` 和 `QGraphicsView` 并关联视图和场景的方法

qt-C++笔记之创建和初始化 QGraphicsScene 和 QGraphicsView 并关联视图和场景的方法 code review! 参考笔记 1.qt-C++笔记之创建和初始化 QGraphicsScene 和 QGraphicsView 并关联视图和场景的方法 2.qt-C++笔记之QGraphicsScene和 QGraphicsView中setScene、通过scene得到vie…