小架构step系列07:查找日志配置文件

1 概述

日志这里采用logback,其为springboot默认的日志工具。其整体已经被springboot封装得比较好了,扔个配置文件到classpath里就能够使用。

但在实际使用中,日志配置文件有可能需要进行改动,比如日志的打印级别,平时可能定的是WARN或者ERROR级别,如果出点问题,可能希望临时能够调整为INFO或者DEBUG,方便产生更加丰富的日志以定位问题。如果配置文件是放到classpath里,也就会被打包到jar包里,修改配置文件就需要重新打包、部署、启动等,很可能做不到只修改配置文件并生效。要想改变配置文件的位置,就有必要了解一下这个配置文件是如何加载的。

2 原理

2.1 logback是如何被依赖的

前面看对spring-boot-starter的依赖的时候,有个不起眼的依赖:spring-boot-starter-logging

https://repo.maven.apache.org/maven2/org/springframework/boot/spring-boot-starter/2.7.18/spring-boot-starter-2.7.18.pom

XML 复制代码
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot</artifactId>
        <version>2.7.18</version>
        <scope>compile</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-logging</artifactId>
        <version>2.7.18</version>
        <scope>compile</scope>
    </dependency>
    <!--  省略其它依赖 -->
<dependencies>

查看spring-boot-starter-logging的依赖:https://repo.maven.apache.org/maven2/org/springframework/boot/spring-boot-starter-logging/2.7.18/spring-boot-starter-logging-2.7.18.pom

XML 复制代码
<dependencies>
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <version>1.2.12</version>
        <scope>compile</scope>
    </dependency>
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-to-slf4j</artifactId>
        <version>2.17.2</version>
        <scope>compile</scope>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>jul-to-slf4j</artifactId>
        <version>1.7.36</version>
        <scope>compile</scope>
    </dependency>
</dependencies>

logback-classic包提供了logback打印日志功能。

2.2 查找logback.xml配置文件

2.2.1 触发查找的Listener

spring-boot-2.7.18.jar包里提供了META-INF/spring.factories配置文件,里面配置了LoggingApplicationListener:

java 复制代码
# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.ClearCachesApplicationListener,\
org.springframework.boot.builder.ParentContextCloserApplicationListener,\
org.springframework.boot.context.FileEncodingApplicationListener,\
org.springframework.boot.context.config.AnsiOutputApplicationListener,\
org.springframework.boot.context.config.DelegatingApplicationListener,\
org.springframework.boot.context.logging.LoggingApplicationListener,\
org.springframework.boot.env.EnvironmentPostProcessorApplicationListener

2.2.2 查找过程

Springboot提供了一个LoggingApplicationListener,其继实现了GenericApplicationListener接口,该接口最终继承了ApplicationListener。

按Springboot的规则,实现了ApplicationListener接口的都会被Springboot统一调用。这个类就是响应springboot的准备环境对象事件来初始化日志对象LogbackLoggingSystem的:

java 复制代码
// 源码位置:org.springframework.boot.context.logging.LoggingApplicationListener
public void onApplicationEvent(ApplicationEvent event) {
    if (event instanceof ApplicationStartingEvent) {
        onApplicationStartingEvent((ApplicationStartingEvent) event);
    }
    else if (event instanceof ApplicationEnvironmentPreparedEvent) {
        // 1. 在PreparedEvent的时候加载日志配置文件
        onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event);
    }
    else if (event instanceof ApplicationPreparedEvent) {
        onApplicationPreparedEvent((ApplicationPreparedEvent) event);
    }
    else if (event instanceof ContextClosedEvent && ((ContextClosedEvent) event).getApplicationContext().getParent() == null) {
        onContextClosedEvent();
    }
    else if (event instanceof ApplicationFailedEvent) {
        onApplicationFailedEvent();
    }
}
private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
    SpringApplication springApplication = event.getSpringApplication();
    if (this.loggingSystem == null) {
        this.loggingSystem = LoggingSystem.get(springApplication.getClassLoader());
    }
    // 2. 调用日志初始化接口
    initialize(event.getEnvironment(), springApplication.getClassLoader());
}
protected void initialize(ConfigurableEnvironment environment, ClassLoader classLoader) {
    getLoggingSystemProperties(environment).apply();
    // 3. 获取环境变量里配置的日志文件,在环境变量里配置了才有
    this.logFile = LogFile.get(environment);
    if (this.logFile != null) {
        this.logFile.applyToSystemProperties();
    }
    this.loggerGroups = new LoggerGroups(DEFAULT_GROUP_LOGGERS);
    initializeEarlyLoggingLevel(environment);
    initializeSystem(environment, this.loggingSystem, this.logFile);
    initializeFinalLoggingLevels(environment, this.loggingSystem);
    registerShutdownHookIfNecessary(environment, this.loggingSystem);
}

// 源码位置:org.springframework.boot.logging.LogFile
public static LogFile get(PropertyResolver propertyResolver) {
    // 4. 如果配置了logging.file.name、logging.file.path环境变量,则它们组成一个log文件的路径,用此路径初始化一个日志文件对象
    //    FILE_NAME_PROPERTY = "logging.file.name"
    //    FILE_PATH_PROPERTY = "logging.file.path"
    String file = propertyResolver.getProperty(FILE_NAME_PROPERTY);
    String path = propertyResolver.getProperty(FILE_PATH_PROPERTY);
    if (StringUtils.hasLength(file) || StringUtils.hasLength(path)) {
        return new LogFile(file, path);
    }
    return null;
}

// 回到LoggingApplicationListener继续处理环境变量里配置的日志文件
// 源码位置:org.springframework.boot.context.logging.LoggingApplicationListener
protected void initialize(ConfigurableEnvironment environment, ClassLoader classLoader) {
    getLoggingSystemProperties(environment).apply();
    // 3. 获取环境变量里配置的日志文件,在环境变量里配置了才有
    this.logFile = LogFile.get(environment);
    if (this.logFile != null) {
        // 5. 把环境变量里的配置日志文件路径和文件名设置到系统属性里
        this.logFile.applyToSystemProperties();
    }
    this.loggerGroups = new LoggerGroups(DEFAULT_GROUP_LOGGERS);
    initializeEarlyLoggingLevel(environment);
    initializeSystem(environment, this.loggingSystem, this.logFile);
    initializeFinalLoggingLevels(environment, this.loggingSystem);
    registerShutdownHookIfNecessary(environment, this.loggingSystem);
}

// 源码位置:org.springframework.boot.logging.LogFile
public void applyToSystemProperties() {
    // 6. 提供系统属性为参数
    applyTo(System.getProperties());
}
public void applyTo(Properties properties) {
    // 7. 把配置文件的路径和文件名设置到系统属性里,可以在logback.xml里作为变量引用,如${LOG_PATH}
    //    LoggingSystemProperties.LOG_PATH = "LOG_PATH"
    //    LoggingSystemProperties.LOG_FILE = "LOG_FILE"
    put(properties, LoggingSystemProperties.LOG_PATH, this.path);
    put(properties, LoggingSystemProperties.LOG_FILE, toString());
}
private void put(Properties properties, String key, String value) {
    if (StringUtils.hasLength(value)) {
        properties.put(key, value);
    }
}

// 回到LoggingApplicationListener继续处理环境变量里配置的日志文件
// 源码位置:org.springframework.boot.context.logging.LoggingApplicationListener
protected void initialize(ConfigurableEnvironment environment, ClassLoader classLoader) {
    getLoggingSystemProperties(environment).apply();
    // 3. 获取环境变量里配置的日志文件,在环境变量里配置了才有
    this.logFile = LogFile.get(environment);
    if (this.logFile != null) {
        // 5. 把环境变量里的配置日志文件路径和文件名设置到系统属性里
        this.logFile.applyToSystemProperties();
    }
    this.loggerGroups = new LoggerGroups(DEFAULT_GROUP_LOGGERS);
    initializeEarlyLoggingLevel(environment);
    
    // 8. 初始化LogbackLoggingSystem对象
    initializeSystem(environment, this.loggingSystem, this.logFile);
    initializeFinalLoggingLevels(environment, this.loggingSystem);
    registerShutdownHookIfNecessary(environment, this.loggingSystem);
}
private void initializeSystem(ConfigurableEnvironment environment, LoggingSystem system, LogFile logFile) {
    // 9. 读取logging.config配置(在命令行配置),CONFIG_PROPERTY = "logging.config"
    String logConfig = StringUtils.trimWhitespace(environment.getProperty(CONFIG_PROPERTY));
    try {
        LoggingInitializationContext initializationContext = new LoggingInitializationContext(environment);
        // 10. 调用LogbackLoggingSystem对象的初始化方法,
        //     如果配置了logging.config,则把配置的文件作为logConfig参数传入,否则logConfig参数为null
        if (ignoreLogConfig(logConfig)) {
            system.initialize(initializationContext, null, logFile);
        }
        else {
            system.initialize(initializationContext, logConfig, logFile);
        }
    }
    // 省略其它代码
}


// 源码位置:org.springframework.boot.logging.logback.LogbackLoggingSystem
// 注意:configLocation有null和非null两种情况
public void initialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) {
    LoggerContext loggerContext = getLoggerContext();
    if (isAlreadyInitialized(loggerContext)) {
        return;
    }
    // 11. 调用父类的初始化方法,父类为AbstractLoggingSystem
    super.initialize(initializationContext, configLocation, logFile);
    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.");
    }
}

// 源码位置:org.springframework.boot.logging.AbstractLoggingSystem
public void initialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) {
    // 12. 如果logging.config配置的值不为空,加载配置指定的日志配置文件
    if (StringUtils.hasLength(configLocation)) {
        initializeWithSpecificConfig(initializationContext, configLocation, logFile);
        return;
    }
    // 13. 当没有配置logging.config时,则加载默认的配置文件,这里重点关注默认的
    initializeWithConventions(initializationContext, logFile);
}
private void initializeWithSpecificConfig(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) {
    // 配置了logging.config的时候,主要是先把里面可能出现${}占位符替换为实际值,
    // 然后得到一个正常的日志配置文件路径,按正常流程处理,参考下面对loadConfiguration()的说明
    configLocation = SystemPropertyUtils.resolvePlaceholders(configLocation);
    loadConfiguration(initializationContext, configLocation, logFile);
}
private void initializeWithConventions(LoggingInitializationContext initializationContext, LogFile logFile) {
    // 14. 获取可能的默认的日志配置文件路径
    String config = getSelfInitializationConfig();
    if (config != null && logFile == null) {
        reinitialize(initializationContext);
        return;
    }
    if (config == null) {
        config = getSpringInitializationConfig();
    }
    if (config != null) {
        loadConfiguration(initializationContext, config, logFile);
        return;
    }
    loadDefaults(initializationContext, logFile);
}
protected String getSelfInitializationConfig() {
    // 15. getStandardConfigLocations()获取默认的日志配置文件
    return findConfig(getStandardConfigLocations());
}

// 源码位置:org.springframework.boot.logging.logback.LogbackLoggingSystem
protected String[] getStandardConfigLocations() {
    // 16. 默认支持四种配置文件名称,注意其顺序,带test的在前面,logback.xml是最后一种
    //     如果开发环境了放logback-test.xml和logback.xml,生产环境只放logback.xml,
    //     则可以开发环境用的是带test的,不影响生产文件的修改,会比较便利
    return new String[] { "logback-test.groovy", "logback-test.xml", "logback.groovy", "logback.xml" };
}

// 回到AbstractLoggingSystem的getSelfInitializationConfig(),调用findConfig()
// 源码位置:org.springframework.boot.logging.AbstractLoggingSystem
protected String getSelfInitializationConfig() {
    // 17. 调findConfig()查找配置文件的路径
    return findConfig(getStandardConfigLocations());
}
private String findConfig(String[] locations) {
    // 18. 遍历每个可能的配置文件名,调用Spring提供的ClassPathResource,从classpath中检查文件是否存在,即上面指定的4中文件需要放到classpath中
    //     如果存在,则在文件名的前面加上classpath:路径前缀,注意这里体现顺序,只要找到第一个就返回
    for (String location : locations) {
        ClassPathResource resource = new ClassPathResource(location, this.classLoader);
        if (resource.exists()) {
            return "classpath:" + location;
        }
    }
    return null;
}

// 回到AbstractLoggingSystem的initializeWithConventions(),继续处理获取到的日志文件路径
// 源码位置:org.springframework.boot.logging.AbstractLoggingSystem
private void initializeWithConventions(LoggingInitializationContext initializationContext, LogFile logFile) {
    // 14. 获取可能的默认的日志配置文件路径
    String config = getSelfInitializationConfig();
    if (config != null && logFile == null) {
        // 15. 重新初始化,这里并没有传入找到的文件,因为里面还会再找一次
        reinitialize(initializationContext);
        return;
    }
    if (config == null) {
        config = getSpringInitializationConfig();
    }
    if (config != null) {
        loadConfiguration(initializationContext, config, logFile);
        return;
    }
    loadDefaults(initializationContext, logFile);
}

// 源码位置:org.springframework.boot.logging.logback.LogbackLoggingSystem
protected void reinitialize(LoggingInitializationContext initializationContext) {
    getLoggerContext().reset();
    getLoggerContext().getStatusManager().clear();
    // 16. 加载配置文件,这里重新调了getSelfInitializationConfig(),从classpath找配置文件路径,参考上面步骤17
    loadConfiguration(initializationContext, getSelfInitializationConfig(), null);
}
protected void loadConfiguration(LoggingInitializationContext initializationContext, String location, LogFile logFile) {
    // 如果logFile有值,则调父类的方法加载配置文件,没有用到location,这里先忽略
    // logFile是前面从环境变量里获取的日志配置文件路径和文件名
    super.loadConfiguration(initializationContext, location, logFile);
    LoggerContext loggerContext = getLoggerContext();
    stopAndReset(loggerContext);
    try {
        // 17. 用url的方式加载文件,ResourceUtils.getURL(location)把以classpath前缀的路径转为文件绝对路径,比如classpath:logback.xml
        configureByResourceUrl(initializationContext, loggerContext, ResourceUtils.getURL(location));
    }
    catch (Exception ex) {
        throw new IllegalStateException("Could not initialize Logback logging from " + location, ex);
    }
    reportConfigurationErrorsIfNecessary(loggerContext);
}

// 源码位置:org.springframework.boot.logging.logback.LogbackLoggingSystem
private void configureByResourceUrl(LoggingInitializationContext initializationContext, LoggerContext loggerContext, URL url) throws JoranException {
    if (XML_ENABLED && url.toString().endsWith("xml")) {
        JoranConfigurator configurator = new SpringBootJoranConfigurator(initializationContext);
        configurator.setContext(loggerContext);
        // 18. SpringBootJoranConfigurator继承了logback-classic包的类GenericConfigurator,从而转到logback-classic包进行日志文件处理了
        //     GenericConfigurator提供了doConfigure()方法实际加载配置文件,由logback包完成日志对象的初始化
        configurator.doConfigure(url);
    }
    else {
        new ContextInitializer(loggerContext).configureByResource(url);
    }
}

// 上面看的是如果配置了日志文件(如logback.xml的情况),回到AbstractLoggingSystem.initialize()看看没有配置日志文件的情况
// 源码位置:org.springframework.boot.logging.AbstractLoggingSystem
private void initializeWithConventions(LoggingInitializationContext initializationContext, LogFile logFile) {
    // 14. 获取可能的默认的日志配置文件路径
    String config = getSelfInitializationConfig();
    if (config != null && logFile == null) {
        reinitialize(initializationContext);
        return;
    }
    
    // 19. 如果没有配置日志文件,可找spring提供的日志配置文件
    if (config == null) {
        config = getSpringInitializationConfig();
    }
    if (config != null) {
        loadConfiguration(initializationContext, config, logFile);
        return;
    }
    loadDefaults(initializationContext, logFile);
}


// 源码位置:org.springframework.boot.logging.AbstractLoggingSystem
protected String getSpringInitializationConfig() {
    // 20. 找配置文件
    return findConfig(getSpringConfigLocations());
}
protected String[] getSpringConfigLocations() {
    // 21. 先找标准文件,参考步骤16可得logback-test.groovy、logback-test.xml、logback.groovy、logback.xml
    String[] locations = getStandardConfigLocations();
    // 22. 给每个文件名加上-spring后缀,作为新的文件名,如logback.xml转为logback-spring.xml
    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;
}

// 回到AbstractLoggingSystem.initialize()继续加载带-spring后缀的日志配置文件
// 源码位置:org.springframework.boot.logging.AbstractLoggingSystem
private void initializeWithConventions(LoggingInitializationContext initializationContext, LogFile logFile) {
    // 14. 获取可能的默认的日志配置文件路径
    String config = getSelfInitializationConfig();
    if (config != null && logFile == null) {
        reinitialize(initializationContext);
        return;
    }
    
    // 19. 如果没有配置日志文件,可找spring提供的日志配置文件
    if (config == null) {
        config = getSpringInitializationConfig();
    }
    
    // 23. 加载日志配置文件,方式同步骤17
    if (config != null) {
        loadConfiguration(initializationContext, config, logFile);
        return;
    }
    
    // 24. 如果spring的配置文件也没有,则加载默认的,保证一定可以打印日志
    loadDefaults(initializationContext, logFile);
}

// 默认的日志是把日志格式等硬编码在代码里的,一般也不使用,大概参考一下即可
// 源码位置:org.springframework.boot.logging.logback.DefaultLogbackConfiguration
private void defaults(LogbackConfigurator config) {
    config.conversionRule("clr", ColorConverter.class);
    config.conversionRule("wex", WhitespaceThrowableProxyConverter.class);
    config.conversionRule("wEx", ExtendedWhitespaceThrowableProxyConverter.class);
    config.getContext().putProperty("CONSOLE_LOG_PATTERN", resolve(config, "${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}}"));
    String defaultCharset = Charset.defaultCharset().name();
    config.getContext().putProperty("CONSOLE_LOG_CHARSET", resolve(config, "${CONSOLE_LOG_CHARSET:-" + defaultCharset + "}"));
    config.getContext().putProperty("FILE_LOG_PATTERN", resolve(config, "${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}}"));
    config.getContext().putProperty("FILE_LOG_CHARSET", resolve(config, "${FILE_LOG_CHARSET:-" + defaultCharset + "}"));
    config.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);
    config.logger("org.springframework.boot.actuate.endpoint.jmx", Level.WARN);
}

2.2.3 小结

SpringBoot加载日志配置文件的核心过程:

  • 如果在启动参数里通过-Dlogging.config指定了日志配置文件,则直接加载此日志配置文件;这种方法指定的配置文件,可以使用${}占位符引用系统属性或者系统环境变量。
  • 如果没有手工指定,则从classpath目录下按顺序加载四种日志配置文件(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 使用

了解原理之后,可以来考虑一下,如何使用。Springboot提供的日志配置文件名有4种:logback-test.groovy、logback-test.xml、logback.groovy、logback.xml,一般情况已经够用了,看看如何用好就可以。

3.1 不需要修改配置文件的场景

如果日志配置文件放到服务器环境上就不需要改动了,那么最简单的方式就是直接放到classpath中,在idea里就是放到resources目录下,打包时则是放到classes目录下打包。按上面原理就可以直接加载使用。

3.2 需要修改配置文件的场景

有下面几种场景需要更改日志配置文件:

  • 在生产环境中,一般日志级别只会开到WARN甚至ERROR级别,如果想看INFO甚至DEBUG级别的日志就有可能看不到,在定位棘手问题时可能需要更详细的日志信息。此时如果想修改一下日志级别,那么就希望配置文件能够改动一下。
  • 日志配置的一些参数配置不理想,想调整一下。比如日志文件过大或者过小,不利于日志文件维护。
  • 增加一些场景的日志打印,比如原来没有加spring相关的日志控制,比较影响问题定位,希望加上spring相关日志只有ERROR才打印的控制等。
    如果是在生产环境中,若因为日志配置文件的改动,就需要重新打包并重启,那代价有点大。所以偏向不把配置文件放到jar中,此时可以在启动参数指定-Dlogging.config=logback.xml,则会加载jar外同级目录的配置文件;如果配置文件在别的目录,也可以指定绝对路径。

3.3 测试和生产分开的场景

如果配置文件是打包到jar里的,那么生产环境使用的配置文件和平时开发测试用的配置文件有可能是不一样的。如果只维护一个文件,那么很容易把测试的配置带到了生产环境中,这是不允许的。按照上面的加载文件顺序的原理,带test的是放前面的,比如logback-test.xml是在logback.xml前面的,那就可以同时维护这两个文件,开发测试的时候使用带test的,打包到生产环境的时候只打包不带test的配置文件,由于打包工具是工具化的,就不容易出现漏掉的情况。

4 架构一小步

增加日志配置文件:

  • 开发测试环境,在代码src/main/resources目录下放一个带test的配置文件(如logback-test.xml),springboot优先加载带test是日志配置文件;一般也放一个不带test的配置文件,作为代码版本管理的一部分。
  • 在部署环境的时候,不把logback.xml文件打包到jar中,而是放到和jar包同级的config/logback.xml中,使用启动参数-Dlogging.config手工指定配置文件。
相关推荐
love530love6 小时前
LiveTalking 数字人项目 Windows 部署完全指南(EPGF 架构)
人工智能·windows·python·架构·livetalking·epgf
星辰徐哥6 小时前
Spring Boot 微服务架构设计与实现
spring boot·后端·微服务
星辰徐哥6 小时前
Spring Boot 数据导入导出与报表生成
spring boot·后端·ui
明夜之约6 小时前
Spring Boot 自动装配源码
java·spring boot·后端
Leaton Lee6 小时前
Spring Boot分层架构详解:从Controller到Service再到Mapper的完整流程
java·spring boot·后端·架构
Micro麦可乐6 小时前
Spring Boot 实战:从零设计一个短链系统(含完整代码与数据库设计)
数据库·spring boot·后端·哈希算法·雪花算法·短链系统
Jinkxs6 小时前
Resilience4j- 与 Spring Boot 快速集成:自动配置与基础注解使用
java·spring boot·后端
毕设源码_郑学姐6 小时前
计算机毕业设计springboot网络相册设计与实现 基于Spring Boot框架的在线相册管理系统开发与应用 Spring Boot驱动的网络影集设计与实践
spring boot·后端·课程设计
辣机小司6 小时前
【踩坑记录:Spring Boot 配置文件读取值不一致?警惕 YAML 的“八进制陷阱”与 SnakeYAML 版本之谜】
java·spring boot·后端·yaml·踩坑记录
一条小锦吕*6 小时前
基于Spring Boot + 数据可视化 + 协同过滤算法的推荐系统设计与实现(源码+论文+部署全讲解)
spring boot·算法·信息可视化