DEV Community

Cyrus Tse
Cyrus Tse

Posted on

Spring Boot 4 入門教學 - Part 3 (@SpringBootApplication 深度解析)

第三章:@SpringBootApplication 深度解析

3.1 前言:揭開@SpringBootApplication的神秘面纱

在上一章中,我們使用了 @SpringBootApplication 註解來標記主應用程式類。這個註解是 Spring Boot 的核心之一,但很多初學者只是機械地使用它,而不理解它背後的原理。在這一章節中,我們將深入探索 @SpringBootApplication 的組成和工作原理,讓你能夠真正理解 Spring Boot 的「魔法」。

理解這個註解不僅是為了滿足好奇心,更是為了成為一個優秀的 Spring Boot 開發者。當你遇到問題時,了解這些基礎概念可以幫助你更快地定位和解決問題。同時,這些知識也會讓你在閱讀官方文檔和源碼時更加得心應手。

3.2 @SpringBootApplication 的三個組成註解

@SpringBootApplication 實際上是一個組合註解(或稱為元註解),它由三個重要的註解組成。讓我們逐一分析每個組成部分。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
@EnableAutoConfiguration
@ComponentScan
public @interface SpringBootApplication {
    // 屬性配置
}
Enter fullscreen mode Exit fullscreen mode

這段程式碼顯示 @SpringBootApplication 被標記了 @Configuration@EnableAutoConfiguration@ComponentScan 三個註解。這意味著當你使用 @SpringBootApplication 時,這三個註解的功能都會被啟用。

3.3 @Configuration:配置類的核心

@Configuration 是 Spring Framework 的核心註解之一,它標記一個類作為 Bean 定義的來源。讓我們詳細了解這個註解的作用和用法。

@Configuration 註解表明其標記的類是一個配置類,可以包含 @Bean 註解的方法。這些方法會被 Spring 容器調用,返回的對象會被註冊為 Spring 容器中的 Bean。

讓我們通過一個實際的範例來理解 @Configuration

package com.example.myfirstapp.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class AppConfig {

    /**
     * @Bean 註解標記的方法會被 Spring 容器調用,
     * 返回的對象會被註冊為容器中的 Bean。
     * 預設 Bean 的名稱就是方法名稱(這裡是 "restTemplate")。
     */
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

    /**
     * 自定義 Bean 名稱
     */
    @Bean(name = "customRestTemplate")
    public RestTemplate customRestTemplate() {
        RestTemplate restTemplate = new RestTemplate();
        // 自定義配置...
        return restTemplate;
    }
}
Enter fullscreen mode Exit fullscreen mode

在這個範例中,AppConfig 類被標記為 @Configuration,這意味著 Spring 會將其視為配置類。類中的 restTemplate() 方法被 @Bean 註解標記,當 Spring 容器啟動時,它會調用這個方法並將返回的 RestTemplate 對象註冊為 Bean。

你可能會問:為什麼需要 @Configuration?為什麼不能直接使用 @Component?這是一個很好的問題。讓我們通過一個對比來理解:

// 使用 @Configuration
@Configuration
public class AppConfig {
    @Bean
    public MyService myService() {
        return new MyService(myRepository());
    }

    @Bean
    public MyRepository myRepository() {
        return new MyRepository();
    }
}

// 使用 @Component(不推薦)
@Component
public class AppConfig {
    @Bean
    public MyService myService() {
        return new MyService(myRepository());
    }

    @Bean
    public MyRepository myRepository() {
        return new MyRepository();
    }
}
Enter fullscreen mode Exit fullscreen mode

這兩種寫法的關鍵區別在於:使用 @Configuration 時,Spring 會確保 Bean 之間的依賴關係被正確處理。如果你從同一個 @Configuration 類中的多個 @Bean 方法請求 Bean,Spring 會確保每次都返回同一個 Bean 實例(單例模式)。而使用 @Component 時,每次調用都會創建新的實例。

這是因為 @Configuration 類會被 CGLIB 代理,代理類會攔截所有 @Bean 方法的調用,確保它們返回的是容器中已存在的 Bean 實例,而不是每次都創建新的實例。

3.4 @EnableAutoConfiguration:自動配置的原理

@EnableAutoConfiguration 是 Spring Boot 特有的註解,它負責自動配置你的 Spring 應用程式。這個註解是 Spring Boot「約定優於配置」哲學的核心。

讓我們了解自動配置的工作原理。當 Spring Boot 啟動時,它會掃描類路徑中的各種條件,根據這些條件自動配置適合的 Bean。例如:

  • 如果類路徑中包含 spring-boot-starter-web,自動配置會設置 Tomcat 內嵌伺服器
  • 如果類路徑中包含 HibernateJpaAutoConfiguration,自動配置會設置 JPA 和 Hibernate
  • 如果類路徑中包含 DataSourceAutoConfiguration,自動配置會設置資料來源

這種自動配置的機制是通過 @Conditional 系列註解實現的。讓我們看看一些常用的條件註解:

// 條件:當類路徑中存在某個類時才配置
@ConditionalOnClass(name = "org.postgresql.Driver")
public class PostgreSqlDataSourceConfiguration {
    // 配置邏輯
}

// 條件:當配置文件中存在某個屬性時才配置
@ConditionalOnProperty(prefix = "spring.datasource", name = "url")
public class DataSourceConfiguration {
    // 配置邏輯
}

// 條件:當類路徑中缺少某個類時才配置
@ConditionalOnMissingClass("org.hibernate.Version")
public class AlternativeConfiguration {
    // 配置邏輯
}

// 條件:當應用程式是 Web 應用程式時才配置
@ConditionalOnWebApplication
public class WebMvcConfiguration {
    // 配置邏輯
}
Enter fullscreen mode Exit fullscreen mode

你可以創建自己的自動配置類。以下是一個自定義自動配置的範例:

package com.example.myfirstapp.autoconfigure;

import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@AutoConfiguration
@ConditionalOnClass(MyService.class)
public class MyAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    public MyService myService() {
        return new MyService();
    }
}
Enter fullscreen mode Exit fullscreen mode

要讓 Spring Boot 識別你的自動配置,需要在 src/main/resources/META-INF/spring.factories 文件中註冊:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.myfirstapp.autoconfigure.MyAutoConfiguration
Enter fullscreen mode Exit fullscreen mode

3.5 @ComponentScan:元件掃描的機制

@ComponentScan 負責掃描指定包及其子包中的類,將標記為 @Component@Service@Repository@Controller 等註解的類註冊為 Spring 容器中的 Bean。

讓我們通過一個詳細的範例來理解元件掃描的工作原理:

package com.example.myfirstapp;

// 主應用程式類(Spring Boot 3.2.0+,預設掃描 com.example.myfirstapp 及其子包)
@SpringBootApplication
public class MyFirstAppApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyFirstAppApplication.class, args);
    }
}
Enter fullscreen mode Exit fullscreen mode

在這個範例中,由於 @SpringBootApplication 包含 @ComponentScan,Spring 會自動掃描 com.example.myfirstapp 包及其所有子包下的類。

讓我們看看不同類型的元件:

// @Controller:表示這是一個 Web 控制器
package com.example.myfirstapp.controller;

@Controller
public class UserController {
    // 處理 HTTP 請求
}

// @Service:表示這是一個服務類
package com.example.myfirstapp.service;

@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    public List<User> getAllUsers() {
        return userRepository.findAll();
    }
}

// @Repository:表示這是一個資料存取類
package com.example.myfirstapp.repository;

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    // JPA 自動實現
}

// @Component:通用元件
package com.example.myfirstapp.util;

@Component
public class DateUtils {
    public String formatDate(LocalDateTime date) {
        return date.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
    }
}
Enter fullscreen mode Exit fullscreen mode

Spring 會自動掃描這些類並將它們註冊為 Bean。在上面的範例中:

  • UserController 會被註冊為 Bean,並且因為它標記了 @Controller,它還會處理 HTTP 請求
  • UserService 會被註冊為 Bean,Spring 會自動處理它的依賴注入
  • UserRepository 會被註冊為 Bean,Spring Data JPA 會自動創建實現類
  • DateUtils 會被註冊為 Bean,可以在其他地方注入使用

自定義元件掃描的範圍也是可以的:

// 自定義掃描範圍
@SpringBootApplication
@ComponentScan(
    basePackages = {"com.example.myfirstapp", "com.example.common"},
    excludeFilters = @ComponentScan.Filter(type = FilterType.REGEX, pattern = ".*Test")
)
public class MyFirstAppApplication {
    // ...
}
Enter fullscreen mode Exit fullscreen mode

3.6 理解 Spring 容器和 Bean 生命週期

要真正理解 @SpringBootApplication 的運作方式,我們需要了解 Spring 容器和 Bean 的生命週期。

Spring 容器(也稱為 ApplicationContext)是 Spring Framework 的核心。它負責創建、配置和管理 Bean。以下是容器的主要職責:

  1. Bean 的實例化
  2. 依賴注入
  3. Bean 的配置
  4. Bean 的生命週期管理

Bean 的生命週期包含以下階段:

package com.example.myfirstapp.service;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Service;

@Service
public class LifecycleDemoService implements BeanNameAware, InitializingBean, DisposableBean {

    private String beanName;

    // 構造函數
    public LifecycleDemoService() {
        System.out.println("1. 構造函數被調用");
    }

    // BeanNameAware 接口方法
    @Override
    public void setBeanName(String name) {
        this.beanName = name;
        System.out.println("2. Bean 名稱已設置: " + beanName);
    }

    // @PostConstruct 註解的方法
    @PostConstruct
    public void init() {
        System.out.println("3. @PostConstruct 初始化方法被調用");
    }

    // InitializingBean 接口方法
    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("4. afterPropertiesSet() 被調用");
    }

    // 業務方法
    public void doSomething() {
        System.out.println("5. 執行業務邏輯");
    }

    // @PreDestroy 註解的方法
    @PreDestroy
    public void cleanup() {
        System.out.println("6. @PreDestroy 清理方法被調用");
    }

    // DisposableBean 接口方法
    @Override
    public void destroy() throws Exception {
        System.out.println("7. destroy() 被調用");
    }
}
Enter fullscreen mode Exit fullscreen mode

這個範例展示了 Bean 生命週期的各個階段。當 Spring 容器啟動時,它會按照特定的順序調用這些方法。理解這個順序對於除錯和優化應用程式非常重要。

3.7 實戰:自定義 Spring Boot 應用程式

現在讓我們通過一個實戰專案來整合我們學到的知識。我們將創建一個可配置的自定義應用程式:

package com.example.myfirstapp;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@SpringBootApplication
public class MyFirstAppApplication {

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

@Configuration
class CustomConfig {

    // 自定義配置 Bean
    @Bean
    public AppSettings appSettings() {
        AppSettings settings = new AppSettings();
        settings.setAppName("My First Spring Boot App");
        settings.setVersion("1.0.0");
        settings.setEnvironment("development");
        return settings;
    }
}

class AppSettings {
    private String appName;
    private String version;
    private String environment;

    // getters and setters
    public void printInfo() {
        System.out.println("=================================");
        System.out.println("應用名稱: " + appName);
        System.out.println("版本: " + version);
        System.out.println("環境: " + environment);
        System.out.println("=================================");
    }

    // getters and setters
    public String getAppName() { return appName; }
    public void setAppName(String appName) { this.appName = appName; }
    public String getVersion() { return version; }
    public void setVersion(String version) { this.version = version; }
    public String getEnvironment() { return environment; }
    public void setEnvironment(String environment) { this.environment = environment; }
}
Enter fullscreen mode Exit fullscreen mode

現在我們可以在任何地方注入 AppSettings Bean 來使用配置:

package com.example.myfirstapp.service;

import com.example.myfirstapp.AppSettings;
import org.springframework.stereotype.Service;

@Service
public class InfoService {

    private final AppSettings appSettings;

    // 構造函數注入(推薦方式)
    public InfoService(AppSettings appSettings) {
        this.appSettings = appSettings;
    }

    public void displayInfo() {
        appSettings.printInfo();
    }
}
Enter fullscreen mode Exit fullscreen mode

3.8 常見問題與解答

在這一章中,我們學習了 @SpringBootApplication 的深度知識。以下是一些常見問題的解答:

問題一:為什麼我的 Bean 沒有被注入?這通常是由於元件掃描範圍不正確造成的。確保被標記為 @Component@Service@Repository 等註解的類位於主應用程式類的包或其子包中。如果需要掃描其他包,可以使用 @ComponentScan 註解自定義掃描範圍。

問題二:@Configuration@Component 有什麼區別?如前所述,@Configuration 類會被 CGLIB 代理,確保 @Bean 方法之間的依賴關係被正確處理。如果你只需要將類註冊為 Bean,且不需要這種代理行為,可以使用 @Component

問題三:如何排除特定的自動配置?可以使用 @SpringBootApplicationexclude 屬性:

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class, HibernateJpaAutoConfiguration.class})
public class MyApplication {
    // ...
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)