深入剖析Spring Boot3.0自动配置原理,核心概念以及Tomcat自动启动原理

导言

现在许多项目都广泛采用了Spring Boot,你只需要引入相应的starter,例如spring-boot-starter-web,然后启动应用程序,就会自动启动Tomcat Web服务器并开始接收HTTP请求。那么,这是如何实现的呢?它是如何知道要启动Tomcat而不是Undertow? 另外,如果我希望使用Undertow吗,要如何切换?本文将深入剖析背后的原理。

简单的说,就是Spring Boot提供了一种自动配置(auto-configuration)机制:当项目引入一个包含自动配置的jar包时,根据特定的条件和规则,它会注册不同的Bean到Spring容器中,从而启动不同的功能特性。

那么,具体什么是自动配置,它是如何工作的?有哪些条件和规则?这些条件和规则又是如何匹配和应用的?本文将分三个部分帮你全面了解自动配置的工作原理:

  • 核心概念:@AutoConfiguration(自动配置类)和@Conditional注解(条件匹配)
  • 案例分析:Spring Boot是怎么自动启动Tomcat服务器的?
  • 常见问题和FAQ

本文基于Spring Boot 3.0.x版本,同时也适用于Spring Boot 2.7.x版本。

核心概念 - 自动配置类@AutoConfiguration

什么是自动配置类

使用过Spring框架的开发者应该对@Configuration注解非常熟悉了。在项目中,我们经常使用它来进行自定义的Bean配置。

@AutoConfiguration是专门用于自动配置类的注解,而这些加了AutoConfiguration注解的自动配置类就是自动配置的入口。@AutoConfiguration本身也使用了@Configuration注解,表明自动配置类也是一个标准的配置类。

与标准的配置类相同,自动配置类的核心内容也是配置Bean,但是它会在此基础上,添加各种条件和规则,只有满足特定的条件和规则,这些Bean才会生效。另外,这些条件规则也可以应用到自动配置类本身,控制整个自动配置类的开启与否。

通常一个特定的自包含特性功能会对应一个自动配置类,但是配置本身不一定要都要全写在这一个类里,可以分解为多个普通的@Configuration配置类,然后通过@Import引入。

例如,Servlet Web服务器相关功能的自动配置,入口就是一个自动配置类ServletWebServerFactoryAutoConfiguration,它将每个可选的Web服务器配置都拆分到各自的@Configuration配置类中,部分代码如下所示:

Java 复制代码
@AutoConfiguration(after = SslAutoConfiguration.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)  
@ConditionalOnClass(ServletRequest.class)  
@ConditionalOnWebApplication(type = Type.SERVLET)  
@EnableConfigurationProperties(ServerProperties.class)  
@Import({
ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class,
ServletWebServerFactoryConfiguration.EmbeddedTomcat.class,  
ServletWebServerFactoryConfiguration.EmbeddedJetty.class,  
ServletWebServerFactoryConfiguration.EmbeddedUndertow.class })  
public class ServletWebServerFactoryAutoConfiguration {
    // ... 其它Bean配置 ...
}

我们来详细分析下这个自动配置类上的注解。

  • @AutoConfiguration(after = SslAutoConfiguration.class):告诉Spring框架这个类是用于自动配置的。有些自动配置的初始化是有先后依赖关系,可以通过afterbefore来声明这种依赖关系。
  • @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE):设置自动配置类加载的顺序。
  • @ConditionalOnClass@ConditionalOnWebApplication:这两个注解就是自动配置生效的条件和规则,后面会详细说明。
  • @EnableConfigurationProperties(ServerProperties.class):自动配置提供的自定义参数,比如server.port等。
  • @Import({...}):自动配置类一般是作为入口,简单的配置可以直接写在自动配置类里。而复杂的配置建议按功能或范围拆分成子配置,然后通过@Import引入。注意,引入的顺序会影响条件的匹配,尤其是选项类的配置(比如选择Tomcat,Jetty还是Undertow)。

查找自动配置类

我们现在有了自动配置类,那么Spring Boot是如何知道要加载这个自动配置类的呢?要知道,我们只是单纯引入了一个jar包而已,并没有做任何设置。

答案是,Spring定义了一套自动配置专用的发现机制,就是jar包里的META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件。该文件的每一行就是一个自动配置类的完全限定名,比如下面是spring-boot-autoconfigure包里该文件的部分内容:

shell 复制代码
## 其它自动配置类
org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration  
org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration  
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration
## 其它自动配置类

可以看到ServletWebServerFactoryAutoConfiguration就在这个文件里。具体的加载逻辑可以查看源码AutoConfigurationImportSelector#getCandidateConfigurations

imports文件是Spring 2.7新引入的发现机制,之前版本使用的是spring.factories文件。实际上,Spring 2.7.x版本两种方式都支持,而在Spring 3.0中完全删除了对spring.factories文件的兼容支持。
【自动配置的模块组织】一般简单的自动配置模块,只有一个starter模块。而复杂的配置,都会拆成两个模块:starter和autoconfigure。例如,Spring Boot将其内置的所有自动配置类都放在了spring-boot-autoconfigure包里,包括imports文件。而为每个单独的功能特性提供了独立的starter包,比如spring-boot-starter-webspring-boot-starter-jdbc等。这些starter没有任何Java代码,唯一作用是引入所有需要的依赖。

核心概念 - 条件@Conditional

自动配置的核心是条件匹配,不同的条件加载不同的Bean,从而启用不同的功能特性。在Spring Boot中,使用了一系列的@ConditionalXXX注解来定义条件。其中,最常用的包括:

  • 类条件@ConditionalOnClass@ConditionalOnMissingClass,用于检测类的存在与否。简单的说就是,应用程序有没有直接或者间接的引用了包含这个类的jar包。比如,你要开启Undertow服务器的自动配置,就要引入Undertow相关的jar包。
  • Bean条件@ConditionalOnBean@ConditionalOnMissingBean,用于检测Spring容器中是否已经注册了指定的Bean。通过使用这些条件注解,开发者可以根据需要注册自定义的Bean,以覆盖默认的配置。比如Spring提供了多种DataSource,不过不包含Druid,你就可以自定义一个基于DruidDataSource Bean,覆盖Spring默认提供的DataSource实现。
  • 属性条件@ConditionalOnProperty,用于检测当前的Environment中是否配置了指定的属性,这些属性可以来自配置文件,JVM系统属性,操作系统的环境变量等。比如Hikari的其中一个条件是@ConfigurationProperties(prefix = "spring.datasource.hikari")
  • 资源条件@ConditionalOnResource,用于检查是否存在特定的资源,比如是否存在某个配置文件,这种条件用到的很少。
  • Web特定条件@ConditionalOnWebApplication@ConditionalOnNotWebApplication,用于检测应用类型是否为Web应用。@ConditionalOnWarDeployment@ConditionalOnNotWarDeployment注解用于判断是否是一个部署在Servlet容器上的传统WAR应用,而使用内嵌的web服务器的应用就不符合此条件。
  • SpEL表达式条件@ConditionalOnExpression可以用SpEL表达式指定条件规则。要注意, 如果在表达式中引入了其它bean,会导致提早初始化这些bean。此时,这些Bean的状态可能是不完整的,因为它还没有经过Post Processor(比如属性绑定)的处理。建议先用上面的几种条件,无法满足再考虑这种。

我们分析一个实际案例,ServletWebServerFactoryAutoConfiguration条件注解如下:

Java 复制代码
@AutoConfiguration(after = SslAutoConfiguration.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)  
@ConditionalOnClass(ServletRequest.class) // (1)
@ConditionalOnWebApplication(type = Type.SERVLET) // (2)
@EnableConfigurationProperties(ServerProperties.class)  
@Import({ ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class,  
ServletWebServerFactoryConfiguration.EmbeddedTomcat.class,  
ServletWebServerFactoryConfiguration.EmbeddedJetty.class,  
ServletWebServerFactoryConfiguration.EmbeddedUndertow.class })  
public class ServletWebServerFactoryAutoConfiguration {
    // ... 其它Bean配置 ...
}
  1. @ConditionalOnClass(ServletRequest.class):要求类路径下必须存在ServletRequest类,这个很好理解,如果都没用到Servlet相关的类和库,说明你不需要Servlet Web服务相关的功能,也就没必要启动相关配置了。
  2. @ConditionalOnWebApplication(type = Type.SERVLET):只是引入了Servlet相关类和库,也不能表明这就是一个Servlet Web服务应用。这个条件就能确保当前启动的应用是一个Servlet Web服务。

这两个条件注解是应用在自动配置类上的,是一种总开关,如果不满足,这个自动配置类就会被完全禁用。

如果满足了类级别上的条件,就会继续加载具体的配置,包括自动配置类里定义的@Bean方法和@Import的配置类。

假设自动配置类的开关条件满足了,我们看下Tomcat的具体配置,也就是@Import里的ServletWebServerFactoryConfiguration.EmbeddedTomcat配置类,它的核心代码如下:

Java 复制代码
@Configuration(proxyBeanMethods = false) // (1)
@ConditionalOnClass({ Servlet.class, Tomcat.class, UpgradeProtocol.class }) // (2) 
@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT) // (3)
static class EmbeddedTomcat {
    @Bean // (4)
    TomcatServletWebServerFactory tomcatServletWebServerFactory(...) {  
        TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();  
        // 其它初始化代码
        return factory;  
    }
}

我们详细分析下这个配置类:

  1. @Configuration(proxyBeanMethods = false):表明它是一个配置类,proxyBeanMethods=false表示这个配置类不需要用CGLIB增强@Bean方法,CGLIB增强后,可以以直接调用@Bean方法的方式,定义Bean之间的依赖关系。
  2. @ConditionalOnClass({ Servlet.class, Tomcat.class, UpgradeProtocol.class }) :这些都是Tomcat的核心类。简单的说,就是要求你引入Tomcat相关的jar包。同理,EmbeddedUndertow的条件就要求引入Jetty的核心类。
  3. @ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT) :字面意思是,只有当前Spring容器中没有ServletWebServerFactory类型的Bean,才会注册这个Bean。换种说法就是,目前还没有加载其它Web服务器。其它可选的服务器配置类,比如EmbeddedJettyEmbeddedUndertow也是这个条件。你也可以注册自定义的ServletWebServerFactory,覆盖Spring Boot自带的Web服务器。
  4. tomcatServletWebServerFactory:这个配置类只有这一个@Bean方法,返回的是一个工厂类Bean,它的作用是实例化,初始化一个Tomcat Web Server。只有EmbeddedTomcat类上的条件注解都满足之后,这个@Bean方法才会生效。

案例分析:Spring Boot是怎么自动启动Tomcat服务器的?

上面讲述了自动配置的基本原理和概念,接下来我们来回答文章开头提出的问题:"我们只是引入了spring-boot-starter-web包,Spring Boot是怎么知道要自动启动Tomcat服务器的?具体是如何启动的呢?"

第一个问题其实简单,因为spring-boot-starter-web引入了spring-boot-starter-tomcat

而关于其中的决策和启动过程,上面讲原理的时候其实已经提到了核心部分,无非就是条件匹配,不过前面部分侧重原理,知识点比较分散,这里通过案例分析的方式,把整个过程串起来,再详细说明下Spring Boot的整个决策过程。

第一步:扫描和注册用户自定义的Bean配置

这是所有Spring Boot启动的标准步骤,这里没有什么特殊的地方。只需要知道自动配置类的解析和加载是在用户自定义的Bean配置之后的。只有这样,自动配置才能根据用户的自定义配置做调整。

第二步:查找自动配置类

在这一步,Spring会扫描类路径下的所有jar包,查找自动配置类的注册文件META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports,然后加载文件里的自动配置类。

我们的应用只引入了spring-boot-starter-web包,但是这个包引入了spring-boot-starter,继而引入了spring-boot-autoconfigure,我们可以从spring-boot-autoconfigure包下找到这个imports文件,该文件配置了Spring Boot内置的大量自动配置类,这里我们只关心Servlet Web服务器相关的自动配置类org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration

我们再回顾下这个类的源码,后续会解析具体的条件匹配过程。

Java 复制代码
@AutoConfiguration(after = SslAutoConfiguration.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)  
@ConditionalOnClass(ServletRequest.class)
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties(ServerProperties.class)  
@Import({ ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class,  
ServletWebServerFactoryConfiguration.EmbeddedTomcat.class,  
ServletWebServerFactoryConfiguration.EmbeddedJetty.class,  
ServletWebServerFactoryConfiguration.EmbeddedUndertow.class })  
public class ServletWebServerFactoryAutoConfiguration {
    // ... 其它Bean配置 ...
}

第三步:条件匹配@ConditionalOnClass(ServletRequest.class)

spring-boot-starter-web包引入了spring-boot-starter-tomcat,继而引入了tomcat-embed-core,这个包打包了JavaEE(Spring Boot 3.x之后是Jakarta EE)的类,其中就包含了ServletRequest类,这样就满足了该条件。

第四步:条件匹配@ConditionalOnWebApplication(type = Type.SERVLET)

SpringApplication类的构造函数会调用下面这段代码,判断Web应用的类型。

Java 复制代码
static WebApplicationType deduceFromClasspath() {  
    if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)  
    && !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {  
        return WebApplicationType.REACTIVE;  
    }  
    for (String className : SERVLET_INDICATOR_CLASSES) {
        if (!ClassUtils.isPresent(className, null)) {  
            return WebApplicationType.NONE;  
        }  
    }  
    return WebApplicationType.SERVLET;  
}

从这段代码可以看出,当前应用是Servlet Web服务的前提,是存在相关的类SERVLET_INDICATOR_CLASSES,这个值在Spring Boot 3.x和之前的版本有些微差别,具体如下:

Java 复制代码
// Spring Boot 3.x
String[] SERVLET_INDICATOR_CLASSES = { "jakarta.servlet.Servlet",  
"org.springframework.web.context.ConfigurableWebApplicationContext" };

// Spring Boot 2.7.x
String[] SERVLET_INDICATOR_CLASSES = { "javax.servlet.Servlet",  
"org.springframework.web.context.ConfigurableWebApplicationContext" };

差异就是Servlet类的包名改了,因为Spring 3.x从JavaEE升级到了Jakarta EE,Servlet跟第三步要求的ServletRequest类在同一个包下,因此这个条件也满足了。剩下的就是org.springframework.web.context.ConfigurableWebApplicationContext类。从包名可以看出,它是spring-web中的一个类,我们分析下包的依赖关系,发现spring-web包是由spring-boot-starter-web包引入的。它其实是个接口,具体的实现类是ServletWebServerApplicationContext

至此,ServletWebServerFactoryAutoConfiguration的两个条件注解都满足了。Spring Boot就会开始加载这个配置类以及它@Import的配置类。

第五步:加载@Import的EmbeddedTomcat

我们先看下EmbeddedTomcat的源码:

Java 复制代码
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ Servlet.class, Tomcat.class, UpgradeProtocol.class })
@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)
static class EmbeddedTomcat {
    @Bean
    TomcatServletWebServerFactory tomcatServletWebServerFactory(...) {  
        // ...
    }
}

spring-boot-starter-web引入了spring-boot-starter-tomcat,继而引入了Tomcat相关的依赖包,因此满足了第一个条件@ConditionalOnClass({ Servlet.class, Tomcat.class, UpgradeProtocol.class })

由于我们只是引入了spring-boot-starter-web包,没有做任何配置,此时容器肯定没有ServletWebServerFactory类型的Bean,因此满足了第二个条件@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)

自此,EmbeddedTomcat配置的所有条件满足,配置生效,@Bean方法tomcatServletWebServerFactory会被注册到Spring容器中,在合适的阶段用于创建TomcatServletWebServerFactory类型的Bean实例。

此外,剩下两个被@ImportEmbeddedJettyEmbeddedUndertow也还是会被处理的,但是由于我们没有引入相应的Jetty或Undertow的包,因此条件都不满足,它们的配置也就不会生效。其实,就算引入了需要的jar包,由于EmbeddedTomcat已经注册了ServletWebServerFactory,这两个配置类也不会生效,它们的源码如下:

Java 复制代码
@Configuration(proxyBeanMethods = false)  
@ConditionalOnClass({ Servlet.class, Server.class, Loader.class, WebAppContext.class })  // 这些类都是Jetty核心包下的类
@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT) // EmbeddedTomcat已经注册了,这个条件无法满足
static class EmbeddedJetty {
    @Bean  
    JettyServletWebServerFactory jettyServletWebServerFactory(...) {  
        // ...
    }
}

@Configuration(proxyBeanMethods = false)  
@ConditionalOnClass({ Servlet.class, Undertow.class, SslClientAuthMode.class })  // 这些类都是Undertow核心包下的类
@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT) // EmbeddedTomcat已经注册了,这个条件无法满足
static class EmbeddedUndertow {
    @Bean  
    UndertowServletWebServerFactory undertowServletWebServerFactory(...) {
        // ...
    }
}

到目前位置,Web服务器的选择决策部分已经结束了,剩下的就是其它依赖Bean的配置,这里就不再详细展开了。

第六步:启动内嵌的Tomcat服务器

在容器初始化完毕后,会调用AbstractApplicationContext#onRefresh方法,而ServletWebServerApplicationContext会重写该方法,在重写的方法中调用createWebServer方法来创建一个WebServer实例。而具体要创建哪个WebServer实例,就是看容器中注册的ServletWebServerFactory类型Bean。具体代码如下:

scss 复制代码
protected ServletWebServerFactory getWebServerFactory() {
    String[] beanNames = getBeanFactory().getBeanNamesForType(ServletWebServerFactory.class);
    return getBeanFactory().getBean(beanNames[0], ServletWebServerFactory.class);
    }

从实际效果来看,就是调用了第五步注册的tomcatServletWebServerFactory创建的工厂Bean,然后用这个工厂Bean创建了真正的Tomcat实例。

需要提一下,此时还只是创建和初始化Tomcat实例,并没有真正启动服务。在SpringApplication启动的最后一步,会触发WebServerStartStopLifecyclestart()回调,这个回调触发WebServer.start()方法,从而真正启动一个Web服务器,开始接收请求。

FAQ

1. 如何排除特定的自动配置类?

我们以排除自动数据源配置类为例,第一种方法是通过@SpringBootApplicationexclude字段:

Java 复制代码
// 第一种方法,用exclude字段。
// 如果不想对类有依赖,可以用excludeName字段。
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class)
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(PayPalApplication.class, args);
    }
}

第二种方法是在配置文件中排除:

Java 复制代码
spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration

2. 不想用Tomcat,如何换成Undertow?

只需要排除spring-boot-starter-tomcat,并引入spring-boot-starter-undertow

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <!-- 排除Tomcat的依赖 -->
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<!-- 替换成Undertow的依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-undertow</artifactId>
</dependency>

3. 如果同时直接或间接地引入了Tomcat,Jetty和Undertow的依赖包,最终启动的是哪个?

实际测试发现,三个都引入的话,最终启动的是Tomcat。而如果只有Jetty和Undertow,实际启动的是Jetty。没有找到官方的优先级文档,我猜测这跟@Import的顺序有关,@Import就是按照Tomcat,Jetty和Undertow的顺序引用的,Spring先看到了import的EmbeddedTomcat配置类,发现满足条件,于是注册了ServletWebServerFactory类型的Bean TomcatServletWebServerFactory,然后继续检查Jetty和Undertown,此时由于已经注册了TomcatServletWebServerFactory,就不满足条件@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)了。

4. 某个配置类为什么没生效?要怎么排查?

启动应用的时候,加上-Ddebug参数,Spring就会打印出每个配置类的条件匹配的细节。

作为案例,我们看下没有exclude掉tomcat,同时又引入undertow的情况下,看看为什么undertow没有生效。从下面这个输出可以看出,虽然匹配了@ConditionalOnClass条件,但是没有匹配到@ConditionalOnMissingBean条件,具体原因是已经存在了tomcatServletWebServerFactory

Java 复制代码
ServletWebServerFactoryConfiguration.EmbeddedUndertow:
      Did not match:
         - @ConditionalOnMissingBean (types: org.springframework.boot.web.servlet.server.ServletWebServerFactory; SearchStrategy: current) found beans of type 'org.springframework.boot.web.servlet.server.ServletWebServerFactory' tomcatServletWebServerFactory (OnBeanCondition)
      Matched:
         - @ConditionalOnClass found required classes 'jakarta.servlet.Servlet', 'io.undertow.Undertow', 'org.xnio.SslClientAuthMode' (OnClassCondition)

总结

Spring Boot自动配置的核心思想就是将自动配置类和条件匹配相结合,使得我们能够快速集成各种功能和组件,而无需手动进行繁琐的配置。

而从使用者的角度看,就是通过引入或者排除特定jar包的依赖,配置特定属性和Bean,来影响条件的匹配,从而灵活地配置和定制特定功能的开关和选项。

Spring Boot支持的所有自动配置类都配置在了spring-boot-autoconfigure包的META-INF\spring\org.springframework.boot.autoconfigure.AutoConfiguration.imports,引入一个新的Starter包的时候,强烈建议去看下相关的自动配置类。

相关推荐
ChinaRainbowSea1 分钟前
补充:问题:CORS ,前后端访问跨域问题
java·spring boot·后端·spring
KiddoStone11 分钟前
多实例schedule job同步数据流的数据一致性设计和实现方案
java
岁忧32 分钟前
(LeetCode 每日一题) 1865. 找出和为指定值的下标对 (哈希表)
java·c++·算法·leetcode·go·散列表
YuTaoShao35 分钟前
【LeetCode 热题 100】240. 搜索二维矩阵 II——排除法
java·算法·leetcode
考虑考虑1 小时前
JDK9中的dropWhile
java·后端·java ee
想躺平的咸鱼干2 小时前
Volatile解决指令重排和单例模式
java·开发语言·单例模式·线程·并发编程
hqxstudying2 小时前
java依赖注入方法
java·spring·log4j·ioc·依赖
·云扬·2 小时前
【Java源码阅读系列37】深度解读Java BufferedReader 源码
java·开发语言
Bug退退退1233 小时前
RabbitMQ 高级特性之重试机制
java·分布式·spring·rabbitmq
小皮侠3 小时前
nginx的使用
java·运维·服务器·前端·git·nginx·github