Spring 自動配置與起始依賴

📢 本文由 gemini-2.5-flash 翻譯

設定

YAML、YML、Properties 檔案都可以進行設定,也可以透過 Java 系統屬性及命令列參數。

優先順序:命令列參數 > Java 系統屬性 > Properties 檔案 > YML 檔案 > YAML 檔案

命令列使用時,需要先執行 Maven 的包裝指令,然後在命令列執行。

1
2
3
4
5
java -jar path_to_jar.jar
# Java 系統屬性,以埠號為例
java -Dserver.port=9000 -jar path_to_jar.jar
# 命令列參數,以埠號為例
java -jar path_to_jar.jar --server.port=9000

SpringBoot 專案進行包裝時需要引入 spring-boot-maven-plugin 外掛程式(如果基於官方樣板建立專案,會自動加入該外掛程式)。

Bean 管理

取得 Bean

對於預設的單例非延遲載入 Bean 而言,Spring 專案啟動時,會把 Bean 都建立好並放置在 IOC 容器中(例如加上 @Lazy 註解後,將會在第一次被使用時實例化)。

如果想主動取得這些 Bean,可以透過以下方式。

  1. 根據名稱取得
1
Object getBean(String name)
  1. 根據類型取得
1
<T> T getBean(Class<T> requiredType)
  1. 根據名稱及類型取得(類型轉換)
1
<T> T getBean(String name, Class<T> requiredType)

為了使用此方法,需要先取得 IOC 容器物件。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@Autowired
private ApplicationContext applicationContext; //IOC 容器物件

public void testGetBean(){
    // 根據 Bean 的名稱取得
    DeptController beanl = (DeptController) applicationContext.getBean("deptController");
    
    // 根據 Bean 的類型取得
    DeptController bean2 = applicationContext.getBean(DeptController.class);
    
    // 根據 Bean 的名稱及類型取得
    DeptController bean3 = applicationContext.getBean("deptController", DeptController.class);
}

Bean 作用域

Spring 支援五種作用域,其中後三種只在 Web 環境中生效。

作用域描述
singleton容器內同名稱的 Bean 只有一個實例(單例)
prototype每次使用該 Bean 時會建立新的實例(非單例)
request每個請求範圍內會建立新的實例
session每個會話範圍內會建立新的實例
application每個應用程式範圍內會建立新的實例

使用註解 @Scope 設定作用域

1
2
3
4
5
6
// 設定為非單例
@Scope("prototype")
@RestController
public class xxxController{
    
}

在實際開發中,絕大部分的 Bean 都是單例的,也就是說絕大部分的 Bean 不需要設定 scope 屬性。

第三方 Bean

如果要管理的 Bean 物件來自於第三方(並非自訂),就無法使用 @Component 及衍生註解來宣告 Bean,這時就需要用到 @Bean 註解。例如解析 XML 檔案的 dom4j。

1
2
3
4
5
<dependency>
    <groupId>org.dom4j</groupId>
    <artifactId>dom4j</artifactId>
    <version>2.1.3</version>
</dependency>

依賴如上所示。

1
2
3
4
5
6
7
@SpringBootApplication
public class xxxApplication{
    @Bean // 將方法回傳值交給 IOC 容器管理,使其成為 IOC 容器的 Bean 物件
    public SAXReader saxReader(){
        return new SAXReader();
    }
}

不過,若要管理第三方 Bean 物件,建議對這些 Bean 進行集中分類設定,可以透過 @Configuration 註解宣告一個設定類。

1
2
3
4
5
6
7
@Configuration
public class CommonConfig {
    @Bean
    public SAXReader saxReader(){
        return new SAXReader();
    }
}

透過 @Bean 註解的 namevalue 屬性可以宣告 Bean 的名稱,如果不指定,預設 Bean 的名稱就是方法名。如果第三方 Bean 需要依賴其他 Bean 物件,直接在 Bean 定義方法中設定形參即可,容器會根據類型自動組裝。

1
2
3
4
5
6
7
@Configuration
public class CommonConfig {
    @Bean
    public SAXReader saxReader(XxService xxService){
        return new SAXReader();
    }
}

起始依賴

在開發中,若直接使用 Spring 需要引入相關依賴,並且保證版本匹配;而使用 SpringBoot 則只需要引入起始依賴即可。其原理是 Maven 的傳遞依賴,其他的依賴都會自動透過 Maven 的依賴機制傳遞進來。

自動配置

SpringBoot 的自動配置就是當 Spring 容器啟動後,一些設定類、Bean 物件會自動儲存到 IOC 容器中,不需要我們手動去宣告,從而簡化了開發,省去了繁瑣的設定操作。

設定類 @Configuration 的底層是 @Component,也是容器中的一個 Bean 物件。

在引入依賴之後,是如何將依賴 Jar 檔案中所定義的設定類以及 Bean 載入到 Spring IOC 容器中的呢?

@ComponentScan

使用 @ComponentScan 可以指定要掃描的套件,例如依賴匯入了 com.example 套件。

1
2
@SpringBootApplication
@ComponentScan({"net.yexca","com.example"})

不過,當需要引入大量的第三方依賴時,上方需要設定大量的套件,而大面積的掃描效能也比較低。

@Import

可以匯入普通類別、設定類以及 ImportSelector 介面實作類。

普通類別

1
2
@Import(TokenParser.class) //匯入普通類別
@SpringBootApplication

設定類

設定類內容

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@Configuration
public class HeaderConfig {
    @Bean
    public HeaderParser headerParser(){
        return new HeaderParser();
    }

    @Bean
    public HeaderGenerator headerGenerator(){
        return new HeaderGenerator();
    }
}

啟動類

1
2
@Import(HeaderConfig.class) //匯入設定類
@SpringBootApplication

ImportSelector 介面實作類

ImportSelector 介面實作類內容

1
2
3
4
5
6
public class MyImportSelector implements ImportSelector {
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        //回傳值為字串陣列(陣列中封裝了具完整限定名稱的類別)
        return new String[]{"com.example.HeaderConfig"};
    }
}

啟動類

1
2
@Import(MyImportSelector.class) //匯入 `ImportSelector` 介面實作類
@SpringBootApplication

@EnableXxxxx

上述 @Import 需要首先知道第三方依賴中有哪些設定類或 Bean 才行;而第三方依賴可以提供 @EnableXxxxx 註解,封裝 @Import 註解以提供一些常用 Bean,使用時只需要 @EnableXxxxx 註解即可。

例如上述 @Import 的設定類,可以封裝一個 @EnableHeaderConfig 註解。

1
2
3
4
5
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import(MyImportSelector.class)//指定要匯入哪些 Bean 物件或設定類
public @interface EnableHeaderConfig { 
}

然後只需要在啟動類加上 @EnableHeaderConfig 註解即可匯入相應的 Bean。

1
2
@EnableHeaderConfig  //使用第三方依賴提供的以 Enable 開頭的註解
@SpringBootApplication

此方法也是 SpringBoot 所採用的方式。

SpringBoot 的自動配置

在 @SpringBootApplication 註解裡有 @EnableAutoConfiguration,其中 @Import({AutoConfigurationImportSelector.class}) 匯入了 ImportSelector 介面的實作類 AutoConfigurationImportSelector.class

在該實作類中覆寫了 selectImports() 方法。

1
2
3
4
5
6
7
8
9
public String[] selectImports(AnnotationMetadata annotationMetadata) {
    if (!this.isEnabled(annotationMetadata)) {
        return NO_IMPORTS;
    } else {
        // 取得自動配置的設定類資訊集合
        AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(annotationMetadata);
        return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
    }
}

呼叫 getAutoConfigurationEntry() 方法取得了自動配置的設定類資訊集合。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
    if (!this.isEnabled(annotationMetadata)) {
        return EMPTY_ENTRY;
    } else {
        AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
        // 取得在設定檔中設定的所有自動配置類別的集合
        List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
        configurations = this.removeDuplicates(configurations);
        Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
        this.checkExcludedClasses(configurations, exclusions);
        configurations.removeAll(exclusions);
        configurations = this.getConfigurationClassFilter().filter(configurations);
        this.fireAutoConfigurationImportEvents(configurations, exclusions);
        return new AutoConfigurationEntry(configurations, exclusions);
    }
}

其中 getCandidateConfigurations(annotationMetadata, attributes) 方法會取得在設定檔中設定的所有自動配置類別的集合。

1
2
3
4
5
6
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
    List<String> configurations = new ArrayList(SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader()));
    ImportCandidates.load(AutoConfiguration.class, this.getBeanClassLoader()).forEach(configurations::add);
    Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories nor 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;
}

可以看到,它是取得 META-INF/spring.factoriesMETA-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 檔案中設定類別的集合。

上述兩個檔案通常在引入的起始依賴中。

也就是說,當 SpringBoot 程式啟動時,就會載入設定檔當中所定義的設定類,並將這些設定類資訊(類別的完整限定名稱)封裝到 String 類型的陣列中,最終透過 @Import 註解將這些設定類全部載入到 Spring 的 IOC 容器中,交由 IOC 容器管理。

@Conditional

但是檔案中的設定類那麼多,每個 Bean 都會註冊到 IOC 容器中嗎?並非如此,使用 @Conditional 註解可以讓 Bean 物件依照條件進行組裝。

@Conditional 是一個父註解,底下有許多子註解。

@ConditionalOnClass

判斷環境中是否存在對應的位元碼檔案,才會將 Bean 註冊到 IOC 容器。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@Configuration
public class HeaderConfig {

    @Bean
    //環境中存在指定的這個類別時,才會將該 Bean 加入 IOC 容器
    @ConditionalOnClass(name="io.jsonwebtoken.Jwts")
    public HeaderParser headerParser(){
        return new HeaderParser();
    }
    
}

上述 Bean 需要引入 JWT 權杖的依賴才會注入到 IOC 容器中。

1
2
3
4
5
6
<!--JWT 權杖-->
<dependency>
     <groupId>io.jsonwebtoken</groupId>
     <artifactId>jjwt</artifactId>
     <version>0.9.1</version>
</dependency>

測試

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@SpringBootTest
public class AutoConfigurationTests {
    @Autowired
    private ApplicationContext applicationContext;

    @Test
    public void testHeaderParser(){
        System.out.println(applicationContext.getBean(HeaderParser.class));
    }

}

@ConditionalOnMissingBean

判斷環境中沒有對應的 Bean(類型或名稱),才會將 Bean 註冊到 IOC 容器。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@Configuration
public class HeaderConfig {

    @Bean
    //不存在該類型的 Bean 時,才會將該 Bean 加入 IOC 容器
    @ConditionalOnMissingBean
    public HeaderParser headerParser(){
        return new HeaderParser();
    }
    
}

上述當 IOC 中沒有 HeaderConfig 類型的 Bean 時才會建立。

也可以在註解中指定其他 Bean 名稱。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@Configuration
public class HeaderConfig {

    @Bean
    //不存在指定名稱的 Bean 時,才會將該 Bean 加入 IOC 容器
    @ConditionalOnMissingBean(name="deptController2")
    public HeaderParser headerParser(){
        return new HeaderParser();
    }
    
}

上例在不存在名稱為 deptController2 的 Bean 物件時,才會建立 HeaderConfig 物件並註冊到 IOC。

還可以指定類型。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@Configuration
public class HeaderConfig {

    @Bean
    //不存在指定類型的 Bean 時,才會將 Bean 加入 IOC 容器
    @ConditionalOnMissingBean(HeaderConfig.class)
    public HeaderParser headerParser(){
        return new HeaderParser();
    }

}

上例執行時呼叫該 Bean 會引發 NoSuchBeanDefinitionException 異常,因為 @Configuration 中有 @Component,所以會自動建立 HeaderConfig 的 Bean,因此不會建立 HeaderParser 的 Bean。

@ConditionalOnProperty

判斷設定檔中存在對應屬性與值時,才會將 Bean 註冊到 IOC 容器。

設定檔

1
name: header

設定類

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@Configuration
public class HeaderConfig {

    @Bean
    //設定檔中存在指定屬性名與值時,才會將 Bean 加入 IOC 容器
    @ConditionalOnProperty(name ="name",havingValue = "header")
    public HeaderParser headerParser(){
        return new HeaderParser();
    }

}

自訂起始依賴

例如自訂一個阿里雲 OSS 的起始依賴。

首先是命名,SpringBoot 官方 Starter 命名為 spring-boot-starter-xxx,而第三方組織提供的則為 xxx-spring-boot-starter

然後是模組,需要按照規範定義兩個模組:

  1. Starter 模組,進行依賴管理,將程式開發所需的依賴都定義在 Starter 起始依賴中。
  2. Autoconfigure 模組,用於自動配置。

定義好這兩個模組後,其他專案只需要引入起始依賴即可,自動配置模組會透過依賴傳遞引入。

模組 POM 檔案

aliyun-oss-spring-boot-starter 模組

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.5</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>com.aliyun.oss</groupId>
    <artifactId>aliyun-oss-spring-boot-starter</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <properties>
        <java.version>11</java.version>
    </properties>
    
    <dependencies>
        <!--引入autoconfigure模組-->
        <dependency>
            <groupId>com.aliyun.oss</groupId>
            <artifactId>aliyun-oss-spring-boot-autoconfigure</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
    </dependencies>

</project>

aliyun-oss-spring-boot-autoconfigure 模組

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.5</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    
    <groupId>com.aliyun.oss</groupId>
    <artifactId>aliyun-oss-spring-boot-autoconfigure</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <properties>
        <java.version>11</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        
        <!--引入Web起始依賴-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--Lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <!--阿里雲OSS-->
        <dependency>
            <groupId>com.aliyun.oss</groupId>
            <artifactId>aliyun-sdk-oss</artifactId>
            <version>3.15.1</version>
        </dependency>

        <dependency>
            <groupId>javax.xml.bind</groupId>
            <artifactId>jaxb-api</artifactId>
            <version>2.3.1</version>
        </dependency>
        <dependency>
            <groupId>javax.activation</groupId>
            <artifactId>activation</artifactId>
            <version>1.1.1</version>
        </dependency>
        <!-- no more than 2.3.3-->
        <dependency>
            <groupId>org.glassfish.jaxb</groupId>
            <artifactId>jaxb-runtime</artifactId>
            <version>2.3.3</version>
        </dependency>
    </dependencies>

</project>

自動配置

AliOSSAutoConfiguration 類別

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@Configuration
//匯入 AliOSSProperties 類別,並交給 Spring IOC 管理
@EnableConfigurationProperties(AliOSSProperties.class)
public class AliOSSAutoConfiguration {

    //建立 AliOSSUtils 物件,並交給 Spring IOC 容器
    @Bean
    public AliOSSUtils aliOSSUtils(AliOSSProperties aliOSSProperties){
        AliOSSUtils aliOSSUtils = new AliOSSUtils();
        aliOSSUtils.setAliOSSProperties(aliOSSProperties);
        return aliOSSUtils;
    }
}

AliOSSProperties 類別

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
/*阿里雲 OSS 相關設定*/
@Data
@ConfigurationProperties(prefix = "aliyun.oss")
public class AliOSSProperties {
    //區域
    private String endpoint;
    //身份 ID
    private String accessKeyId ;
    //身份密鑰
    private String accessKeySecret ;
    //儲存空間
    private String bucketName;
}

AliOSSUtils 類別

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Data 
public class AliOSSUtils {
    private AliOSSProperties aliOSSProperties;

    /**
     * 實作上傳圖片到 OSS
     */
    public String upload(MultipartFile multipartFile) throws IOException {
        // 取得上傳檔案的輸入流
        InputStream inputStream = multipartFile.getInputStream();

        // 避免檔案覆蓋
        String originalFilename = multipartFile.getOriginalFilename();
        String fileName = UUID.randomUUID().toString() + originalFilename.substring(originalFilename.lastIndexOf("."));

        //上傳檔案到 OSS
        OSS ossClient = new OSSClientBuilder().build(aliOSSProperties.getEndpoint(),
                aliOSSProperties.getAccessKeyId(), aliOSSProperties.getAccessKeySecret());
        ossClient.putObject(aliOSSProperties.getBucketName(), fileName, inputStream);

        //檔案存取路徑
        String url =aliOSSProperties.getEndpoint().split("//")[0] + "//" + aliOSSProperties.getBucketName() + "." + aliOSSProperties.getEndpoint().split("//")[1] + "/" + fileName;

        // 關閉 ossClient
        ossClient.shutdown();
        return url;// 回傳上傳到 OSS 的路徑
    }
}

新建自動設定檔 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

1
com.aliyun.oss.AliOSSAutoConfiguration

使用

引入依賴

1
2
3
4
5
6
<!--引入阿里雲 OSS 起始依賴-->
<dependency>
    <groupId>com.aliyun.oss</groupId>
    <artifactId>aliyun-oss-spring-boot-starter</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

上例阿里雲 OSS 相關設定需要從設定檔讀取。

1
2
3
4
5
6
7
#設定阿里雲 OSS 參數
aliyun:
  oss:
    endpoint: your_oss_region
    accessKeyId: your_key_id
    accessKeySecret: your_key_secret
    bucketName: your_bucker_name

測試

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@RestController
public class UploadController {

    @Autowired
    private AliOSSUtils aliOSSUtils;

    @PostMapping("/upload")
    public String upload(MultipartFile image) throws Exception {
        //上傳檔案到阿里雲 OSS
        String url = aliOSSUtils.upload(image);
        return url;
    }

}