Spring Boot 自動配置原理

ACatSmiling發表於2024-07-23

Author: ACatSmiling

Since: 2024-07-23

核心場景啟動器

Spring Boot 的每個場景啟動器都引入了一個spring-boot-starter,這是 Spring Boot 的核心場景啟動器。

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter</artifactId>
</dependency>

本文程式碼基於 Spring Boot 3.2.4 版本。

在 spring-boot-starter 中,又引入了spring-boot-autoconfigure包,spring-boot-autoconfigure 裡面事先定義了所有場景的所有配置,只要這個包下的所有類都能生效,那麼相當於 Spring Boot 官方寫好的整合功能就生效了。

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-autoconfigure</artifactId>
</dependency>

但是 Spring Boot 預設只掃描主程式所在的包,因此掃描不到 spring-boot-autoconfigure 下定義好的所有配置類,這部分的工作,是由@EnableAutoConfiguration註解完成的。

引導載入自動配置類

主程式:

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

@SpringBootApplication

@SpringBootApplication

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
}

@SpringBootConfiguration

@SpringBootConfiguration:是@Configuration的派生註解,表明當前主類實際上也是一個配置類。

@ComponentScan

@ComponentScan:指定掃描的包,預設為當前主類所在包及其子包。

@EnableAutoConfiguration

@EnableAutoConfiguration

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
}
@AutoConfigurationPackage

@AutoConfigurationPackage

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {
}
@Import(AutoConfigurationPackages.Registrar.class)

向容器中註冊了一個 AutoConfigurationPackages.Registrar.class 元件:

/**
 * {@link ImportBeanDefinitionRegistrar} to store the base package from the importing
 * configuration.
 */
static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
       register(registry, new PackageImports(metadata).getPackageNames().toArray(new String[0]));
    }

    @Override
    public Set<Object> determineImports(AnnotationMetadata metadata) {
       return Collections.singleton(new PackageImports(metadata));
    }

}
  • new PackageImports(metadata).getPackageNames():拿到元註解所包含的包資訊,實際上就是主類所在的包,如 cn.zero.cloud.business。
  • register()的功能,也就是將主類所在包下的所有元件,批次註冊到容器中,這也就是預設包路徑為主類所在包的原因。
@Import(AutoConfigurationImportSelector.class)

向容器中註冊了一個 AutoConfigurationImportSelector.class 元件,執行如下方法:

@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
    if (!isEnabled(annotationMetadata)) {
       return NO_IMPORTS;
    }
    AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
    return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}

執行getAutoConfigurationEntry(annotationMetadata)方法,向容器中批次註冊一些元件:

/**
 * Return the {@link AutoConfigurationEntry} based on the {@link AnnotationMetadata}
 * of the importing {@link Configuration @Configuration} class.
 * @param annotationMetadata the annotation metadata of the configuration class
 * @return the auto-configurations that should be imported
 */
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
    if (!isEnabled(annotationMetadata)) {
       return EMPTY_ENTRY;
    }
    AnnotationAttributes attributes = getAttributes(annotationMetadata);
    List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
    configurations = removeDuplicates(configurations);
    Set<String> exclusions = getExclusions(annotationMetadata, attributes);
    checkExcludedClasses(configurations, exclusions);
    configurations.removeAll(exclusions);
    configurations = getConfigurationClassFilter().filter(configurations);
    fireAutoConfigurationImportEvents(configurations, exclusions);
    return new AutoConfigurationEntry(configurations, exclusions);
}

執行getCandidateConfigurations(annotationMetadata, attributes)方法:

/**
 * Return the auto-configuration class names that should be considered. By default,
 * this method will load candidates using {@link ImportCandidates}.
 * @param metadata the source metadata
 * @param attributes the {@link #getAttributes(AnnotationMetadata) annotation
 * attributes}
 * @return a list of candidate configurations
 */
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
    List<String> configurations = ImportCandidates.load(AutoConfiguration.class, getBeanClassLoader())
       .getCandidates();
    // 從類路徑下獲取預先定義的元件,Spring Boot 2 中此檔名為 spring.factories
    Assert.notEmpty(configurations,
          "No auto configuration classes found in "
                + "META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. If you "
                + "are using a custom packaging, make sure that file is correct.");
    return configurations;
}

Spring Boot 3 中,事先定義的元件位於 spring-boot-autoconfigure 包類路徑下的META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports檔案,在 Spring Boot 2 中,對應的檔名是 spring.factories。

protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
  List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
        getBeanClassLoader());
  Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
        + "are using a custom packaging, make sure that file is correct.");
  return configurations;
}

執行ImportCandidates.load(AutoConfiguration.class, getBeanClassLoader())方法,獲取類路徑下配置檔案中事先定義的,所有待批次註冊的元件(配置類):

/**
 * Loads the names of import candidates from the classpath.
 *
 * The names of the import candidates are stored in files named
 * {@code META-INF/spring/full-qualified-annotation-name.imports} on the classpath.
 * Every line contains the full qualified name of the candidate class. Comments are
 * supported using the # character.
 * @param annotation annotation to load
 * @param classLoader class loader to use for loading
 * @return list of names of annotated classes
 */
public static ImportCandidates load(Class<?> annotation, ClassLoader classLoader) {
    Assert.notNull(annotation, "'annotation' must not be null");
    ClassLoader classLoaderToUse = decideClassloader(classLoader);
    String location = String.format(LOCATION, annotation.getName());
    Enumeration<URL> urls = findUrlsInClasspath(classLoaderToUse, location);
    List<String> importCandidates = new ArrayList<>();
    while (urls.hasMoreElements()) {
       URL url = urls.nextElement();
       // 獲取配置檔案中的預定義元件的全路徑
       importCandidates.addAll(readCandidateConfigurations(url));
    }
    return new ImportCandidates(importCandidates);
}
image-20240723221554851

按需開啟自動配置項

在上面的分析中,Spring Boot 在啟動時,預設會載入 152 個自動配置的元件。但在實際啟動時,各 xxxxAutoConfiguration 元件,會根據@Conditional註解,即按照條件裝配規則,實現按需配置。我們選取如下幾個元件進行分析。

AopAutoConfiguration

org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,其裝配規則如下:

package org.springframework.boot.autoconfigure.aop;

import org.aspectj.weaver.Advice;

import org.springframework.aop.config.AopConfigUtils;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

/**
 * {@link org.springframework.boot.autoconfigure.EnableAutoConfiguration
 * Auto-configuration} for Spring's AOP support. Equivalent to enabling
 * {@link EnableAspectJAutoProxy @EnableAspectJAutoProxy} in your configuration.
 * <p>
 * The configuration will not be activated if {@literal spring.aop.auto=false}. The
 * {@literal proxyTargetClass} attribute will be {@literal true}, by default, but can be
 * overridden by specifying {@literal spring.aop.proxy-target-class=false}.
 *
 * @author Dave Syer
 * @author Josh Long
 * @since 1.0.0
 * @see EnableAspectJAutoProxy
 */
@AutoConfiguration
// 當配置檔案中配置了 spring.aop.auto 屬性,且值為 true 時,AopAutoConfiguration 生效,預設為 true
@ConditionalOnProperty(prefix = "spring.aop", name = "auto", havingValue = "true", matchIfMissing = true)
public class AopAutoConfiguration {

    // 當 org.aspectj.weaver.Advice.class 檔案存在時,AspectJAutoProxyingConfiguration 才生效
    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass(Advice.class)
    static class AspectJAutoProxyingConfiguration {

       @Configuration(proxyBeanMethods = false)
       @EnableAspectJAutoProxy(proxyTargetClass = false)
       @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false")
       static class JdkDynamicAutoProxyConfiguration {

       }

       @Configuration(proxyBeanMethods = false)
       @EnableAspectJAutoProxy(proxyTargetClass = true)
       @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true",
             matchIfMissing = true)
       static class CglibAutoProxyConfiguration {

       }

    }

    // 當 org.aspectj.weaver.Advice.class 檔案不存在,且配置檔案中 spring.aop.proxy-target-class 屬性值為 true (預設為 true)時,ClassProxyingConfiguration 生效
    @Configuration(proxyBeanMethods = false)
    @ConditionalOnMissingClass("org.aspectj.weaver.Advice")
    @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true",
          matchIfMissing = true)
    static class ClassProxyingConfiguration {

       @Bean
       static BeanFactoryPostProcessor forceAutoProxyCreatorToUseClassProxying() {
          return (beanFactory) -> {
             if (beanFactory instanceof BeanDefinitionRegistry registry) {
                AopConfigUtils.registerAutoProxyCreatorIfNecessary(registry);
                AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
             }
          };
       }

    }

}

可以看出,當匯入 spring-boot-starter-aop 依賴時,會註冊 AspectJAutoProxyingConfiguration 配置類,否則,註冊 ClassProxyingConfiguration 配置類,後者是 Spring Boot 預設的的 AOP 功能。

DispatcherServletAutoConfiguration

org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration,其裝配規則如下:

package org.springframework.boot.autoconfigure.web.servlet;

import java.util.Arrays;
import java.util.List;

import jakarta.servlet.MultipartConfigElement;
import jakarta.servlet.ServletRegistration;

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigureOrder;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionMessage;
import org.springframework.boot.autoconfigure.condition.ConditionMessage.Style;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.web.multipart.MultipartResolver;
import org.springframework.web.servlet.DispatcherServlet;

/**
 * {@link EnableAutoConfiguration Auto-configuration} for the Spring
 * {@link DispatcherServlet}. Should work for a standalone application where an embedded
 * web server is already present and also for a deployable application using
 * {@link SpringBootServletInitializer}.
 *
 * @author Phillip Webb
 * @author Dave Syer
 * @author Stephane Nicoll
 * @author Brian Clozel
 * @since 2.0.0
 */
// 當前配置類的配置順序
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
// 在 ServletWebServerFactoryAutoConfiguration 後配置
@AutoConfiguration(after = ServletWebServerFactoryAutoConfiguration.class)
// 當專案是一個原生的 Web Servlet 應用時
@ConditionalOnWebApplication(type = Type.SERVLET)
// 當容器中存在 DispatcherServlet 時
@ConditionalOnClass(DispatcherServlet.class)
public class DispatcherServletAutoConfiguration {

    /**
     * The bean name for a DispatcherServlet that will be mapped to the root URL "/".
     */
    public static final String DEFAULT_DISPATCHER_SERVLET_BEAN_NAME = "dispatcherServlet";

    /**
     * The bean name for a ServletRegistrationBean for the DispatcherServlet "/".
     */
    public static final String DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME = "dispatcherServletRegistration";

    @Configuration(proxyBeanMethods = false)
    @Conditional(DefaultDispatcherServletCondition.class)
    // 當容器中存在 ServletRegistration 時
    @ConditionalOnClass(ServletRegistration.class)
    // 開啟 WebMvcProperties 類的配置繫結功能,並註冊到容器中
    @EnableConfigurationProperties(WebMvcProperties.class)
    protected static class DispatcherServletConfiguration {

       // 註冊 DispatcherServlet 元件到容器中,名字為 dispatcherServlet
       @Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
       public DispatcherServlet dispatcherServlet(WebMvcProperties webMvcProperties) {
          // 新建了一個 DispatcherServlet 物件
          DispatcherServlet dispatcherServlet = new DispatcherServlet();
          dispatcherServlet.setDispatchOptionsRequest(webMvcProperties.isDispatchOptionsRequest());
          dispatcherServlet.setDispatchTraceRequest(webMvcProperties.isDispatchTraceRequest());
          configureThrowExceptionIfNoHandlerFound(webMvcProperties, dispatcherServlet);
          dispatcherServlet.setPublishEvents(webMvcProperties.isPublishRequestHandledEvents());
          dispatcherServlet.setEnableLoggingRequestDetails(webMvcProperties.isLogRequestDetails());
          return dispatcherServlet;
       }

       @SuppressWarnings({ "deprecation", "removal" })
       private void configureThrowExceptionIfNoHandlerFound(WebMvcProperties webMvcProperties,
             DispatcherServlet dispatcherServlet) {
          dispatcherServlet.setThrowExceptionIfNoHandlerFound(webMvcProperties.isThrowExceptionIfNoHandlerFound());
       }

       // 註冊 MultipartResolver 元件到容器中,即檔案上傳解析器
       @Bean
       // 當容器中存在 MultipartResolver 時
       @ConditionalOnBean(MultipartResolver.class)
       // 當容器中沒有 name 為 multipartResolver 的 MultipartResolver 物件時
       @ConditionalOnMissingBean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME)
       public MultipartResolver multipartResolver(MultipartResolver resolver) {
          // 當容器中有 MultipartResolver 的物件,resolver 引數會自動繫結該物件
          // 此方法的作用是,防止有些使用者配置的檔案上傳解析器不符合規範:
          // 將使用者自己配置的檔案上傳解析器重新註冊給容器,並重新命名為 multipartResolver (方法名)
          // (Spring Boot 中的檔案上傳解析器的名字,就叫 multipartResolver)
          // Detect if the user has created a MultipartResolver but named it incorrectly
          return resolver;
       }

    }
    
    ...
}
  • @ConditionalOnWebApplication(type = Type.SERVLET):Spring Boot 支援兩種型別的 Web 應用開發,一種是響應式,一種是原生 Servlet。響應式 Web 開發需要匯入spring-boot-starter-webflux依賴,原生 Servlet Web 開發需要匯入spring-boot-starter-web依賴。

  • @ConditionalOnClass(DispatcherServlet.class):在主類中可以驗證專案中存在 DispatcherServlet 類。

    @SpringBootApplication
    public class MainApplication {
        public static void main(String[] args) {
            ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args);
    
            String[] beanNamesForType = run.getBeanNamesForType(DispatcherServlet.class);
            System.out.println(beanNamesForType.length);// 1
        }
    }
    

HttpEncodingAutoConfiguration

org.springframework.boot.autoconfigure.web.servlet.HttpEncodingAutoConfiguration,其裝配規則如下:

package org.springframework.boot.autoconfigure.web.servlet;

import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.boot.web.servlet.filter.OrderedCharacterEncodingFilter;
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
import org.springframework.boot.web.servlet.server.Encoding;
import org.springframework.context.annotation.Bean;
import org.springframework.core.Ordered;
import org.springframework.web.filter.CharacterEncodingFilter;

/**
 * {@link EnableAutoConfiguration Auto-configuration} for configuring the encoding to use
 * in web applications.
 *
 * @author Stephane Nicoll
 * @author Brian Clozel
 * @since 2.0.0
 */
@AutoConfiguration
// 開啟 ServerProperties 類的配置繫結功能,並註冊到容器中
@EnableConfigurationProperties(ServerProperties.class)
// 當專案是一個原生的 Web Servlet 應用時
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
// 當容器中存在 CharacterEncodingFilter 時
@ConditionalOnClass(CharacterEncodingFilter.class)
// 當配置檔案中 server.servlet.encoding 屬性值為 enabled (預設為 true) 時
@ConditionalOnProperty(prefix = "server.servlet.encoding", value = "enabled", matchIfMissing = true)
public class HttpEncodingAutoConfiguration {

    private final Encoding properties;

    public HttpEncodingAutoConfiguration(ServerProperties properties) {
       this.properties = properties.getServlet().getEncoding();
    }

    // 向容器中註冊一個 CharacterEncodingFilter 元件,此元件就是解決 Spring Boot 收到的請求出現亂碼的問題
    @Bean
    // 當容器中沒有這個 Bean 時才配置,即使用者未配置時,Spring Boot 才主動配置一個
    @ConditionalOnMissingBean
    public CharacterEncodingFilter characterEncodingFilter() {
       CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
       filter.setEncoding(this.properties.getCharset().name());
       filter.setForceRequestEncoding(this.properties.shouldForce(Encoding.Type.REQUEST));
       filter.setForceResponseEncoding(this.properties.shouldForce(Encoding.Type.RESPONSE));
       return filter;
    }

    @Bean
    public LocaleCharsetMappingsCustomizer localeCharsetMappingsCustomizer() {
       return new LocaleCharsetMappingsCustomizer(this.properties);
    }

    static class LocaleCharsetMappingsCustomizer
          implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory>, Ordered {

       private final Encoding properties;

       LocaleCharsetMappingsCustomizer(Encoding properties) {
          this.properties = properties;
       }

       @Override
       public void customize(ConfigurableServletWebServerFactory factory) {
          if (this.properties.getMapping() != null) {
             factory.setLocaleCharsetMappings(this.properties.getMapping());
          }
       }

       @Override
       public int getOrder() {
          return 0;
       }

    }

}

HttpEncodingAutoConfiguration 配置類,可以防止 Spring Boot 亂碼。

修改預設配置

一般來說,Spring Boot 預設會在底層配好所有需要的元件,但是如果使用者自己配置了,就會以使用者配置的優先。

以 CharacterEncodingFilter 為例,如果使用者希望按自己的需求進行配置,可以在配置類中自行新增:

@Configuration
public class MyConfig {
    @Bean
    public CharacterEncodingFilter characterEncodingFilter() {
        // filter的實現程式碼
    }
}

從前面對 HttpEncodingAutoConfiguration 的分析可以看出,當使用者自己配置了 CharacterEncodingFilter 的例項時,Spring Boot 就不會再配置。

當然,也可以根據元件 @ConfigurationProperties 註解繫結的屬性,按需做相應的修改。

總結

Spring Boot 先載入所有預設的自動配置類,即 xxxxxAutoConfiguration.class,每個自動配置類按照條件進行生效。xxxxxAutoConfiguration.class 在配置時,會從對應的 xxxxxProperties.class 中取值,而 xxxxxProperties.class 會和配置檔案中對應的值進行繫結(@EnableConfigurationProperties 註解)。

  • 生效的配置類,會給容器中裝配很多不同功能的元件。
  • 這些元件裝配到容器中後,專案就具有了該元件所具有的功能。
  • 如果使用者自行配置了某一個元件,則以使用者配置的優先。

若想實現定製化配置,有兩種方法:

  • 方法一:使用者自行配置元件,新增@Bean註解,用以替換 Spring Boot 底層的預設元件。
  • 方法二:使用者檢視該元件從配置檔案種獲取的是什麼屬性的值,然後按需求自行修改對應的屬性值。比如 HttpEncodingAutoConfiguration 對應的就是配置檔案中的server.servlet.encoding屬性。

更多配置項取值,參考:https://docs.spring.io/spring-boot/docs/current/reference/html/application-properties.html#application-properties

自動配置過程:xxxxxAutoConfiguration.class ---> 註冊元件 ---> 從 xxxxxProperties.class 裡面拿值 ----> 繫結 application.properties 檔案。可以看出,一般透過修改 application.properties 檔案中相應的配置,就可完成 Spring Boot 功能的修改。

原文連結

https://github.com/ACatSmiling/zero-to-zero/blob/main/SpringEcosystem/spring-boot.md

相關文章