Spring6框架中依賴注入的多種方式(推薦構造器注入)

落叶微风發表於2024-05-26

你好,這裡是codetrend專欄“Spring6全攻略”。

一個典型的企業應用程式不是由單個物件(或在Spring術語中稱為bean)組成的。

即使是最簡單的應用程式也有一些物件一起工作,呈現給終端使用者看到的內容形成一個連貫的應用程式。

要實現多個bean的連貫工作,這裡就要使用到Spring的核心技術:依賴注入(DI)。

依賴注入(DI)是一種過程,物件透過建構函式引數、工廠方法的引數或在物件例項構建後設定的屬性來定義它們的依賴關係(即與其一起工作的其他物件)。

容器在建立bean時注入這些依賴關係。這個過程基本上是bean本身不再透過直接構造類或使用Service Locator模式控制其依賴項的例項化或位置,因此被稱為控制反轉(Inversion of Control)。

遵循DI原則的程式碼更加清晰,物件提供其依賴關係時解耦更有效。

該物件不會查詢其依賴項,也不知道依賴項的位置或類別。

因此類變得更易於測試,特別是當依賴項是介面或抽象基類時,可以在單元測試中使用存根或模擬實現。

依賴注入有兩種主要變體:基於建構函式的依賴注入和基於Setter的依賴注入。

基於建構函式的依賴注入

基於建構函式的依賴注入是Spring6中的一種依賴注入策略,主要用於確保在物件建立時其必需依賴已經得到初始化。

在建構函式注入中,物件的依賴關係明確地透過建構函式的引數傳遞給物件。

這意味著在例項化一個類時,Spring IoC容器會分析建構函式簽名中的引數型別,然後從容器中查詢並提供相匹配的bean作為依賴注入的目標物件。

下面的程式碼是一個完整的示例,展示了基於建構函式的依賴注入:

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

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

/**
 * 基於建構函式的依賴注入
 * @author nine
 * @since 1.0
 */
public class ConstructorDIDemo {
    public static void main(String[] args) {
        // 建立一個基於 Java Config 的應用上下文
        ApplicationContext context = new AnnotationConfigApplicationContext(ConstructorAppConfig.class);
        // 從上下文中獲取名bean,其型別為PetStoreService
        SimpleMovieLister bean = context.getBean(SimpleMovieLister.class);
        // 呼叫獲取的bean的方法
        bean.listMovies();
    }
}

/**
 * App配置
 */
@Configuration
class ConstructorAppConfig{

    @Bean
    public MovieFinder movieFinder() {
        return new MovieFinder();
    }

    @Bean
    public SimpleMovieLister simpleMovieLister(MovieFinder movieFinder) {
        return new SimpleMovieLister(movieFinder);
    }
}

/**
 * 服務程式碼
 */
@Slf4j
class SimpleMovieLister {
    private final MovieFinder movieFinder;
    public SimpleMovieLister(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }
    public void listMovies() {
        log.info("電影列表列印中");
        movieFinder.findMovies().forEach(log::info);
    }
}
@Slf4j
class MovieFinder {
    public List<String> findMovies() {
        return Arrays.asList("電影1", "電影2", "電影3");
    }
}

在Spring配置檔案或Java配置類中,容器會根據建構函式引數型別找到符合條件的bean,並自動呼叫帶有適當引數的建構函式來例項化SimpleMovieLister。這種方式的優勢在於:

  1. 確保物件例項化時就有所有的必需依賴項,增強了物件狀態的完整性。
  2. 由於建構函式私有的強制性依賴無法為null,提高了程式碼健壯性。
  3. 有利於實現不可變物件,也就是在屬性上面加了final修飾符,提升多執行緒環境下物件的安全性。
  4. 使得依賴關係清晰可見,利於閱讀和理解程式碼。

Spring6推薦優先使用建構函式注入,尤其是對於必需的、不可缺失的依賴。而對於可選依賴或易於變更的配置屬性,則更適合使用setter方法注入。

基於Setter的依賴注入

基於Setter方法的依賴注入是Spring6框架中另一種常用的依賴注入策略。

它允許在物件例項化之後透過呼叫setter方法來設定依賴關係。

這種方法允許物件在構造完成後繼續接受依賴注入,這在依賴不是必須的情況下特別有用,因為物件可以先建立一個預設狀態,然後再透過setter方法補充注入依賴。

把建構函式注入修改為如下程式碼,這是一個完整的示例,展示了基於Setter的依賴注入:

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 基於Setter的依賴注入
 * @author nine
 * @since 1.0
 */
public class SetterDIDemo {
    public static void main(String[] args) {
        // 建立一個基於 Java Config 的應用上下文
        ApplicationContext context = new AnnotationConfigApplicationContext(SetterAppConfig.class);
        // 從上下文中獲取名bean,其型別為PetStoreService
        SimpleMovieListerSet bean = context.getBean(SimpleMovieListerSet.class);
        // 呼叫獲取的bean的方法
        bean.listMovies();
    }
}

/**
 * App配置
 */
@Configuration
class SetterAppConfig{
    @Bean
    public MovieFinder movieFinder() {
        return new MovieFinder();
    }

    @Bean
    public SimpleMovieListerSet simpleMovieLister() {
        return new SimpleMovieListerSet();
    }
}

@Slf4j
class SimpleMovieListerSet {
    private MovieFinder movieFinder;
    @Autowired
    public void setMovieFinder(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }
    public void listMovies() {
        log.info("電影列表列印中");
        movieFinder.findMovies().forEach(log::info);
    }
}

在這種情況下,Spring容器會在建立完SimpleMovieListerSet例項後,查詢型別匹配的MovieFinder bean,並呼叫setMovieFinder()方法將其注入。

setter注入的優點包括:

  1. 可以延遲注入可選依賴,允許類在沒有所有依賴的情況下也能建立例項。
  2. 更容易適應配置變化,因為可以在執行時重新配置或替換已注入的依賴項。
  3. 有時候對於第三方類庫或不能更改原始碼的情況,如果只能透過setter暴露依賴,則setter注入可能是唯一可行的DI方式。

然而,相比於建構函式注入,setter注入的一個潛在缺點是可能導致物件在未完全初始化時就被使用,增加了程式碼理解和維護的難度,以及可能引入執行時錯誤的風險。

其它依賴注入方式

  • 屬性注入(Field Injection)

屬性注入是指直接在類的成員變數上使用@Autowired@Inject註解來宣告依賴。Spring容器會在bean初始化時自動為這些欄位賦值。例如:

public class UserService {
    @Autowired
    private UserRepository userRepository;
    // ...
}
  • 方法注入(Method Injection)

方法注入允許在非建構函式的方法中注入依賴。這包括像Spring Test框架中測試方法的引數注入,以及在方法級別處理依賴,如Spring的@PostConstruct@PreDestroy生命週期回撥方法。例如:

@Component
public class MyService {
    private SomeDependency someDependency;

    @Autowired
    public void init(SomeDependency someDependency) {
        this.someDependency = someDependency;
    }
    // ...
}
  • 註解驅動的配置(Annotation-based Configuration)

使用@Configuration@Bean等註解編寫Java配置類,以宣告式的方式來定義bean及其依賴關係。例如:

@Configuration
public class AppConfig {
    @Bean
    public UserService userService(UserRepository userRepository) {
        return new UserService(userRepository);
    }

    @Bean
    public UserRepository userRepository() {
        return new UserRepositoryImpl();
    }
}
  • JSR-330註解(Java Dependency Injection)

Spring同時支援JSR-330規範中的註解,如@javax.inject.Inject,可以用它代替Spring的@Autowired來實現依賴注入。

Dependency Resolution Process 依賴注入解析過程

Spring框架中的依賴注入解析過程主要包括以下幾個步驟:

配置後設資料載入

  • 應用程式啟動時,Spring IoC容器首先讀取和解析配置後設資料,這些後設資料可以來自於XML配置檔案、Java配置類(透過@Configuration註解)或元件類上的註解(如@Component@Service@Repository@Controller等)。

Bean定義註冊

  • 容器根據配置後設資料建立Bean Definition物件,這些物件包含了如何建立Bean的全部資訊,如Bean的型別(類)、構造器引數、屬性值、依賴關係和其他生命週期回撥方法等。

依賴解析

  • 當Spring容器建立一個Bean時,它會檢視Bean Definition中關於依賴的描述。如果是構造器注入,容器會識別並獲取構造器引數所需的Bean,透過呼叫構造器來注入依賴。
  • 如果是Setter注入,容器會在Bean例項化後遍歷其setter方法,找到那些帶有@Autowired或其他相關注解的setter方法,然後查詢並注入相應的依賴Bean。
  • 若是欄位注入,容器則會直接找到類中帶有@Autowired等註解的欄位,為它們注入合適的Bean。

依賴注入

  • 容器根據Bean定義中定義的依賴關係,從IoC容器中查詢或建立需要注入的Bean,並將這些依賴注入到目標Bean中。
  • 注入過程中,容器會解決依賴的迴圈引用問題,保證依賴鏈的完整性,並可以處理多種作用域的Bean之間的依賴關係。

Bean生命週期管理

  • 容器除了注入依賴外,還會執行Bean生命週期的相關回撥方法,如@PostConstruct@PreDestroy等,以確保Bean在初始化和銷燬時能正確執行相應操作。

整個過程體現了控制反轉(IoC)的原則,Spring容器扮演了協調者角色,負責建立、裝配和管理應用程式中的所有物件,使得物件之間相互解耦,提高了程式碼的可測試性和可維護性。

整個過程都包含在 BeanFactory 中,這裡的程式碼示例就是這行程式碼 ApplicationContext context = new AnnotationConfigApplicationContext(SetterAppConfig.class);

// 建構函式分為3個步驟
public AnnotationConfigApplicationContext(Class<?>... componentClasses) {
    this();
    register(componentClasses);
    refresh();
}
//在this()初始化Spring相關的工具庫,一個reader和一個scanner
public AnnotationConfigApplicationContext() {
    StartupStep createAnnotatedBeanDefReader = getApplicationStartup().start("spring.context.annotated-bean-reader.create");
    this.reader = new AnnotatedBeanDefinitionReader(this);
    createAnnotatedBeanDefReader.end();
    this.scanner = new ClassPathBeanDefinitionScanner(this);
}
// register(componentClasses); 是程式碼的核心,註冊配置類裡面的相關資訊,主要呼叫了私有方法doRegisterBean

doRegisterBean的核心程式碼如下:

// 1. 載入配置後設資料
// 此方法負責將給定的類轉換為AnnotatedGenericBeanDefinition,從而提取類上的後設資料資訊
private <T> void doRegisterBean(Class<T> beanClass, @Nullable String name,
        @Nullable Class<? extends Annotation>[] qualifiers, @Nullable Supplier<T> supplier,
        @Nullable BeanDefinitionCustomizer[] customizers) {

    // 建立一個基於給定類的AnnotatedGenericBeanDefinition物件
    AnnotatedGenericBeanDefinition abd = new AnnotatedGenericBeanDefinition(beanClass);

    // 2. 判斷是否需要跳過此Bean的註冊(條件評估)
    if (this.conditionEvaluator.shouldSkip(abd.getMetadata())) {
        return;
    }

    // 標記候選Bean屬性
    abd.setAttribute(ConfigurationClassUtils.CANDIDATE_ATTRIBUTE, Boolean.TRUE);

    // 設定例項供應商,用於懶載入或延遲初始化
    abd.setInstanceSupplier(supplier);

    // 3. 解析作用域後設資料並設定Bean的作用域
    ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(abd);
    abd.setScope(scopeMetadata.getScopeName());

    // 生成或使用指定的Bean名稱
    String beanName = (name != null ? name : this.beanNameGenerator.generateBeanName(abd, this.registry));

    // 處理通用定義註解(如@Component, @Service等)
    AnnotationConfigUtils.processCommonDefinitionAnnotations(abd);

    // 處理限定符註解(如@Primary, @Lazy等)
    if (qualifiers != null) {
        for (Class<? extends Annotation> qualifier : qualifiers) {
            if (Primary.class == qualifier) {
                abd.setPrimary(true); // 設定為主Bean
            } else if (Lazy.class == qualifier) {
                abd.setLazyInit(true); // 設定為懶載入
            } else {
                abd.addQualifier(new AutowireCandidateQualifier(qualifier)); // 新增自定義限定符
            }
        }
    }

    // 4. 應用自定義Bean定義配置
    if (customizers != null) {
        for (BeanDefinitionCustomizer customizer : customizers) {
            customizer.customize(abd); // 根據使用者提供的定製器調整Bean定義
        }
    }

    // 建立BeanDefinitionHolder物件,封裝了最終的Bean定義和名稱
    BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(abd, beanName);

    // 根據作用域後設資料應用代理模式(如果需要)
    definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);

    // 5. 註冊Bean定義到BeanDefinitionRegistry中
    BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, this.registry);
}

doRegisterBean主要執行以下邏輯:

  1. 配置後設資料載入:從給定的類beanClass中提取後設資料,並封裝成AnnotatedGenericBeanDefinition物件。
  2. Bean定義註冊前的準備工作:判斷Bean是否滿足註冊條件,設定候選屬性、作用域後設資料和Bean名稱,處理通用定義註解和限定符註解,以及應用使用者自定義的Bean定義配置。
  3. 依賴解析和注入:這部分主要是透過設定作用域、限定符和自定義配置來預備Bean的依賴解析和注入過程,但具體的依賴注入發生在後續的Bean例項化階段。在這裡,Bean定義已經被完善並準備註冊到BeanDefinitionRegistry中,後續容器在初始化Bean時會根據這些定義資訊完成依賴注入。

關於作者

來自全棧程式設計師nine的探索與實踐,持續迭代中。

歡迎關注或者點個小紅心~

相關文章