你好,這裡是codetrend專欄“Spring6全攻略”。
典型的企業級應用程式並非僅由單個物件(在Spring術語中稱為bean)組成。即使是最簡單的應用程式,也會包含一些協同工作的物件,共同呈現出終端使用者眼中連貫一致的應用程式形態。
以下mermaid流程圖簡單展示了Spring工作過程。
業務類與配置後設資料相結合,使得在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的探索與實踐,持續迭代中。
歡迎關注或者點個小紅心~