簡單上手SpringBean的整個裝配過程

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

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

典型的企業級應用程式並非僅由單個物件(在Spring術語中稱為bean)組成。即使是最簡單的應用程式,也會包含一些協同工作的物件,共同呈現出終端使用者眼中連貫一致的應用程式形態。

以下mermaid流程圖簡單展示了Spring工作過程。

graph LR A[業務類POJO] --> C[Spring容器ApplicationContext] B[配置後設資料Configuration Metadata] --> C C --產生--> D[可執行的系統/應用程式]

業務類與配置後設資料相結合,使得在Spring容器ApplicationContext被建立並初始化後,得到的是一個完全配置好且可執行的系統或應用程式。

下文將從定義一系列獨立的bean定義出發,進而構建出一個物件間相互協作以達成目標的完全成型的應用程式。

配置後設資料 Configuration Metadata

Spring IoC 容器透過消費一種形式的配置後設資料。這些配置後設資料代表了您作為應用程式開發者告訴 Spring 容器如何例項化、配置和組裝應用程式中的物件。

配置後設資料方式如下:

  • 基於 XML 格式配置
  • 基於 Groovy 格式配置
  • 基於Java類和註解進行配置

雖然配置的形式不一樣,但是配置內容和api基本一樣的。

以下是基於 XML 格式、Groovy 格式和 Java 類與註解的方式來配置 Spring IoC 容器的示例:

  • 基於 XML 格式配置:
<!-- applicationContext.xml -->
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="userService" class="com.example.UserService">
        <property name="userDao" ref="userDao"/>
    </bean>

    <bean id="userDao" class="com.example.UserDao"/>

</beans>
  • 基於 Groovy 格式配置:
// applicationContext.groovy
beans {
    userService(com.example.UserService) {
        userDao = ref('userDao')
    }

    userDao(com.example.UserDao)
}
  • 基於 Java 類和註解進行配置:
// AppConfig.java
@Configuration
public class AppConfig {

    @Bean
    public UserService userService() {
        UserService userService = new UserService();
        userService.setUserDao(userDao());
        return userService;
    }

    @Bean
    public UserDao userDao() {
        return new UserDao();
    }
}

以上示例分別展示了使用 XML、Groovy 和 Java 類與註解的方式來配置 Spring IoC 容器。無論使用哪種配置方式,都可以定義和組裝應用程式中的物件,並且相應的 API 在實現上基本一致。

這三種配置方式各有優劣,開發者可以根據專案需求和個人喜好選擇合適的方式。

Ioc容器使用初體驗

Ioc容器在Spring6框架中也就是各種BeanFactory的實現類來建立和管理物件。

ClassPathXmlApplicationContext就是透過讀取xml配置初始化bean的一種方法。

啟動類的程式碼如下:

/**
 * 寵物測試app
 * @author nine
 * @since 1.0
 */
public class PetApp {
    public static void main(String[] args) {
        // 建立一個類路徑下的XML應用上下文,並指定配置檔案
        ApplicationContext context = new ClassPathXmlApplicationContext("s104/services.xml", "s104/daos.xml");
        // 從上下文中獲取名為"petStore"的bean,其型別為PetStoreServiceImpl,其中petStoreAlias是別名
        PetStoreService petStoreService = context.getBean("petStoreAlias", PetStoreServiceImpl.class);
        // 呼叫獲取的bean的buyPet方法
        petStoreService.buyPet(new Pet("Tom", "Cat",1));
    }
}

bean的配置如下:

<!-- daos.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
  https://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="accountDao"
          class="io.yulin.learn.spring.s104.AccountDao">
    </bean>

    <bean id="itemDao" class="io.yulin.learn.spring.s104.ItemDao">
    </bean>
</beans>

<!-- services.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
  https://www.springframework.org/schema/beans/spring-beans.xsd">
    <!-- services -->
    <bean id="petStore" class="io.yulin.learn.spring.s104.PetStoreServiceImpl" >
        <property name="accountDao" ref="accountDao"/>
        <property name="itemDao" ref="itemDao"/>
    </bean>
    <!--命名別名-->
    <alias name="petStore" alias="petStoreAlias" />
</beans>

對應bean如下。其中這些bean也就是簡單的業務bean。

@Slf4j
@Data
public class PetStoreServiceImpl implements PetStoreService {
    private  AccountDao accountDao;
    private  ItemDao itemDao;
    @Override
    public boolean buyPet(Pet pet) {
        log.info("buy pet: {}", pet);
        accountDao.store(pet);
        itemDao.minus(pet);
        return true;
    }
}

@Slf4j
public class ItemDao {
    public void minus(Pet pet) {
        log.info("minus pet num: {}", pet.getNum());
    }
}

@Slf4j
public class AccountDao {
    public void store(Pet pet) {
        log.info("增加收入: {}", pet);
    }
}

這個過程與開發者編寫工具類一樣,沒有任何註解、匯入依賴這些配置。習慣使用springboot的開發者可能對此表示有點不習慣。

但是xml後設資料配置+BeanFactory一起,就組合成了一個單獨的app。執行起來就和springboot無異。

講上述程式碼修改為基於Java類和註解進行配置的程式碼如下。

public class PetAppJavaConfig {
    public static void main(String[] args) {
        // 建立一個基於 Java Config 的應用上下文
        ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        // 從上下文中獲取名為"petStoreService"的bean,其型別為PetStoreService
        PetStoreService petStoreService = context.getBean(PetStoreService.class);
        // 呼叫獲取的bean的buyPet方法
        petStoreService.buyPet(new Pet("Tom", "Cat", 1));
    }
}
@Configuration
class AppConfig {
    @Bean
    public PetStoreService petStoreService(AccountDao accountDao, ItemDao itemDao) {
        PetStoreServiceImpl petStoreService = new PetStoreServiceImpl();
        petStoreService.setAccountDao(accountDao);
        petStoreService.setItemDao(itemDao);
        return petStoreService;
    }
    @Bean
    public AccountDao accountDao() {
        return new AccountDao();
    }
    @Bean
    public ItemDao itemDao() {
        return new ItemDao();
    }
}

可以看出程式碼簡潔明瞭不少,程式碼輸出結果都是一致的。

11:38:11.646 [main] INFO io.yulin.learn.spring.s104.PetStoreServiceImpl -- buy pet: Pet(name=Tom, type=Cat, num=1)
11:38:11.653 [main] INFO io.yulin.learn.spring.s104.AccountDao -- 增加收入: Pet(name=Tom, type=Cat, num=1)
11:38:11.653 [main] INFO io.yulin.learn.spring.s104.ItemDao -- minus pet num: 1

完整專案原始碼資訊檢視可以在gitee或者github上搜尋r0ad檢視。(外鏈稽核太嚴格~木辦法)

配置Bean初體驗

Spring IoC容器管理一個或多個bean。這些bean是根據您提供給容器的配置後設資料建立的(例如,以XML <bean/> 定義的形式)。

在容器內部,bean 定義被表示為 BeanDefinition 物件,其中包含(除其他資訊外)以下後設資料:

  • 一個包限定的類名:通常是所定義的 bean 的實際實現類。
  • Bean 行為配置元素,用於說明 bean 在容器中應如何執行(作用域、生命週期回撥等)。
  • 引用其他 bean,這些 bean 是該 bean 執行工作所需的。這些引用也稱為協作者或依賴項。
  • 其他配置設定用於設定新建立物件中的值,例如,管理連線池的 bean 中的池大小限制或要使用的連線數。

/**
 * 說明備案definition的例子
 *
 * @author nine
 * @since 1.0
 */
public class BeanDefinitionProcessDemo {
    public static void main(String[] args) {
        // ️GenericApplicationContext 是一個【乾淨】的容器
        GenericApplicationContext context = new GenericApplicationContext();
        // 用原始方法註冊三個 bean
        context.registerBean("bean1", Bean1.class);
        // 初始化容器
        // 執行beanFactory後處理器, 新增bean後處理器, 初始化所有單例
        context.refresh();
        Bean1 bean = context.getBean(Bean1.class);
        bean.print();
        // 銷燬容器
        context.close();
    }
}

@Slf4j
class Bean1 {
    public void print() {
        log.info("I am bean1");
    }
}

透過這個程式碼可以發現,Bean1在呼叫registerBean介面後從一個普通的pojo類變成了一個bean。

org.springframework.context.support.GenericApplicationContext#registerBean為了方便使用有很多過載方法。

透過原始碼可以發現,普通類透過ClassDerivedBeanDefinition的建構函式轉換為BeanDefinition。也就是該class透過setBeanClass成為BeanDefinition的屬性beanClass

後續透過一些列操作,自定義、名字處理、註冊容器等等新增了其他的屬性資訊或者進行二次處理。

具體原始碼如下。

public <T> void registerBean(@Nullable String beanName, Class<T> beanClass, @Nullable Supplier<T> supplier, BeanDefinitionCustomizer... customizers) {
    // 建立一個 ClassDerivedBeanDefinition 物件,用於封裝 Bean 的定義資訊
    ClassDerivedBeanDefinition beanDefinition = new ClassDerivedBeanDefinition(beanClass);

    // 如果存在 supplier,則設定到 BeanDefinition 中
    if (supplier != null) {
        beanDefinition.setInstanceSupplier(supplier);
    }

    // 對 BeanDefinition 進行定製處理
    for (BeanDefinitionCustomizer customizer : customizers) {
        customizer.customize(beanDefinition);
    }

    // 如果指定了 beanName,則使用指定的名稱,否則使用 beanClass 的名稱
    String nameToUse = (beanName != null ? beanName : beanClass.getName());

    // 將封裝好的 BeanDefinition 註冊到容器中
    registerBeanDefinition(nameToUse, beanDefinition);
}

bean的生命週期回撥

Spring6 中 Bean 的生命週期可以透過 InitializingBean 和 DisposableBean 介面、@PostConstruct 和 @PreDestroy 註解以及配置檔案中的 init-method 和 destroy-method 方法來管理。

把上面手動注入的bean的demo修改,增加實現 Bean 的初始化和銷燬回撥:

public class BeanDefinitionProcessDemo {
    public static void main(String[] args) {
        // GenericApplicationContext 是一個乾淨的容器
        GenericApplicationContext context = new GenericApplicationContext();
        // 用原始方法註冊 bean1,並指定初始化和銷燬方法
        context.registerBean("bean1", Bean1.class, Bean1::new, beanDefinition -> {
            beanDefinition.setInitMethodName("init");
            beanDefinition.setDestroyMethodName("destroy");
        });
        // 初始化容器
        context.refresh();
        Bean1 bean = context.getBean(Bean1.class);
        bean.print();
        // 銷燬容器
        context.close();
    }
}

@Slf4j
class Bean1 {
    public void print() {
        log.info("I am bean1");
    }
    public void init() {
        log.info("Bean1 is being initialized");
    }
    public void destroy() {
        log.info("Bean1 is being destroyed");
    }
}

透過輸出可以發現,bean1的初始化和銷燬回撥被呼叫了。

15:14:23.631 [main] INFO io.yulin.learn.spring.s104.Bean1 -- Bean1 is being initialized
15:14:23.669 [main] INFO io.yulin.learn.spring.s104.Bean1 -- I am bean1
15:14:23.670 [main] INFO io.yulin.learn.spring.s104.Bean1 -- Bean1 is being destroyed

把整個過程改為更熟悉的基於註解驅動開發的方式,程式碼如下。

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

/**
 * 透過註解方式配置Bean
 * @author nine
 * @since 1.0
 */
public class BeanDefinitionProcessAnnotationDemo {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        context.register(BeanAnnotation1.class);
        context.refresh();

        BeanAnnotation1 bean = context.getBean(BeanAnnotation1.class);
        bean.print();
        context.close();
    }
}

@Slf4j
class BeanAnnotation1 {
    public void print() {
        log.info("I am BeanAnnotation1");
    }

    @PostConstruct
    public void init() {
        log.info("BeanAnnotation1 is being initialized");
    }

    @PreDestroy
    public void destroy() {
        log.info("BeanAnnotation1 is being destroyed");
    }
}

輸出結果基本一致的。

透過這個轉換過程可以更能清晰的發現,Spring如何從基於xml配置、Java配置、註解配置的轉換。也能更加深刻體會到Spring的強大相容性。

例項化Bean

需要使用bean就必須例項化這個類,最簡單的方式就是new 一個物件。

但是在Spring6框架中提供了更多的配置來實現例項化bean。

如果使用基於XML的配置後設資料,可以在<bean/>元素的class屬性中指定要例項化的物件的型別(或類)。

這個class屬性(在BeanDefinition例項上內部是一個Class屬性)通常是必需的。

使用Class屬性的兩種方式之一:

  • 通常情況下,為了指定要構造的bean類,在容器本身透過呼叫其建構函式反射性地直接建立bean的情況下,類似於使用new運算子的Java程式碼。
  • 在較不常見的情況下,為了指定包含靜態工廠方法的實際類,容器呼叫該類上的靜態工廠方法來建立bean。從呼叫靜態工廠方法返回的物件型別可以是相同的類,也可以是完全不同的類。

構造器例項化bean

當透過建構函式方式建立一個bean時,所有普通類都可以被Spring使用並與之相容。

也就是說,正在開發的類不需要實現任何特定的介面或以特定方式編碼。只需指定bean類即可。

然而,根據為該特定bean使用的IoC型別,可能需要一個預設(空)建構函式。

Spring IoC容器可以管理幾乎任何希望它管理的類。它不侷限於管理真正的JavaBeans。

大多數Spring使用者更喜歡具有僅預設(無引數)建構函式。

Spring容器還可以管理非bean的類。例如,如果需要使用不符合JavaBean規範的傳統連線池,Spring也可以進行管理。

下面透過一個例子說明基於構造器例項化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;

/**
 * SpringBean建立demo
 *
 * @author nine
 * @since 1.0
 */
public class SpringBeanCreateDemo {

    public static void main(String[] args) {
        // 建立一個基於 Java Config 的應用上下文
        ApplicationContext context = new AnnotationConfigApplicationContext(AppCreateConfig.class);
        // 從上下文中獲取名bean,其型別為PetStoreService
        MyClass bean = context.getBean(MyClass.class);
        // 呼叫獲取的bean的方法
        bean.hello("jack");
    }
}

@Configuration
@Slf4j
class AppCreateConfig {
    @Bean
    public MyClass exampleBean() {
        return new MyClass("exampleConstructorArg");
    }
}

@Slf4j
class MyClass {
    public MyClass(String constructorArg) {
        log.info(constructorArg);
    }

    public void hello(String name) {
        log.info("hello " + name);
    }
}

輸出結果如下。可以看到MyClass類被正確初始化和被IoC容器管理。

09:44:57.211 [main] INFO io.yulin.learn.spring.s104.MyClass -- exampleConstructorArg
09:44:57.262 [main] INFO io.yulin.learn.spring.s104.MyClass -- hello jack

靜態工廠方法例項化bean

在定義使用靜態工廠方法建立的bean時,使用class屬性指定包含靜態工廠方法的類,並使用名為factory-method的屬性指定工廠方法本身的名稱。

應該能夠呼叫這個方法(帶有可選引數,如後面所述),並返回一個活動物件,隨後將其視為透過建構函式建立的物件。

這樣一個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;

/**
 * SpringBean建立demo
 *
 * @author nine
 * @since 1.0
 */
public class SpringBeanFactoryMethodCreateDemo {

    public static void main(String[] args) {
        // 建立一個基於 Java Config 的應用上下文
        ApplicationContext context = new AnnotationConfigApplicationContext(AppFactoryConfig.class);
        // 從上下文中獲取名bean,其型別為PetStoreService
        AppFactoryConfig.MyBean bean = context.getBean(AppFactoryConfig.MyBean.class);
        // 呼叫獲取的bean的方法
        bean.hello();
    }
}

@Slf4j
@Configuration
class AppFactoryConfig {

    @Bean
    public MyBean myBean() {
        // 呼叫帶有可選引數的靜態工廠方法建立 bean
        return MyBeanFactory.createBean("Tom");
    }

    static class MyBean {
        private String name;

        public MyBean(String name) {
            this.name = name;
        }

        public void hello() {
            log.info("Hello, " + name);
        }
    }

    static class MyBeanFactory {
        public static MyBean createBean(String parameter) {
            return new MyBean(parameter);
        }
    }
}

透過例項工廠方法例項化bean

類似於透過靜態工廠方法進行例項化,使用例項工廠方法進行例項化會呼叫容器中現有 bean 的非靜態方法來建立一個新的 bean。

要使用這種機制,將class屬性留空,在factory-bean屬性中指定當前(或父級或祖先)容器中包含要被呼叫以建立物件的例項方法的 bean 的名稱。

使用factory-method屬性設定工廠方法本身的名稱。

這個例子是基於java config來實現的。

可以發現MyBean透過現有名為MyBeanFactory的bean來建立的。

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppBeanCreateConfig {
    @Bean
    public MyBeanFactory myBeanFactory() {
        return new MyBeanFactory();
    }

    @Bean
    public MyBean myBean(MyBeanFactory myBeanFactory) {
        // 呼叫例項工廠方法建立 bean
        return myBeanFactory.createBean("optionalParameter");
    }

    static class MyBean {
        private String name;

        public MyBean(String name) {
            this.name = name;
        }

        public void hello() {
            System.out.println("Hello, " + name);
        }
    }

    static class MyBeanFactory {
        public MyBean createBean(String parameter) {
            return new MyBean(parameter);
        }
    }
}

確定Bean的執行時型別

確定Spring框架中一個特定bean的執行時型別確實需要考慮到多種複雜情況。

在bean後設資料定義中指定的類只是一個初始類引用,可能與宣告的工廠方法結合,或者是一個可能導致bean具有不同執行時型別的FactoryBean類,或者在例項級工廠方法的情況下根本沒有設定(這是透過指定的工廠-bean 名稱來解析的)。

此外,AOP代理可能會用基於介面的代理包裝一個bean例項,只暴露目標bean的實際型別(僅暴露其實現的介面)。

查詢特定bean的實際執行時型別的推薦方法是使用BeanFactory.getType呼叫指定的bean名稱。

這考慮了上述所有情況,並返回BeanFactory.getBean呼叫將為相同的bean名稱返回的物件型別。

以上面的透過例項工廠方法例項化bean為例說明使用BeanFactory.getType

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

@Slf4j
public class BeanRunTimeTypeTest {

    @Test
    public void test() {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppBeanCreateConfig.class);
        // 獲取 BeanFactory 例項
        ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
        // 使用 BeanFactory.getType 方法獲取特定 bean 的執行時型別
        Class<?> beanType = beanFactory.getType("myBean");
        log.info("The runtime type of 'myBean' is: " + beanType.getName());
        // 使用 BeanFactory.getBean 方法獲取特定 bean 的例項物件
        AppBeanCreateConfig.MyBean myBeanInstance = (AppBeanCreateConfig.MyBean) beanFactory.getBean("myBean");
        myBeanInstance.hello();
        // 關閉應用上下文
        context.close();
    }
}

輸出結果如下。可以看到例項工廠方法沒有設定class,但是執行時型別為MyBean。

10:51:25.111 [main] INFO io.yulin.learn.spring.s104.BeanRunTimeTypeTest -- The runtime type of 'myBean' is: io.yulin.learn.spring.s104.AppBeanCreateConfig$MyBean
Hello, optionalParameter

BeanFactory.getType()一些常見用途:

  • 型別檢查:透過呼叫 getType() 方法,可以獲取特定 bean 的實際型別,並根據這些型別資訊執行相應的操作。這對於在執行時進行型別檢查和驗證非常有用。
  • 動態處理:在某些情況下,您可能需要根據 bean 的型別來動態地決定如何處理該 bean。透過 getType() 方法可以獲取 bean 的型別資訊,並根據需要執行相應的處理邏輯。
  • 條件化配置:在 Spring 應用程式中,有時根據 bean 的型別來進行條件化的配置會很有用。透過 getType() 方法可以獲取 bean 的型別,從而根據不同的型別執行不同的配置。
  • 自定義邏輯:某些情況下,可能需要根據 bean 的型別來編寫特定的業務邏輯。透過 getType() 方法可以獲取 bean 的準確型別資訊,並在程式碼中編寫相應的邏輯。

關於作者

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

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

相關文章