Spring之Bean元件介紹
spring-bean 是 spring 家族中最核心的一個元件,從抽象層面來說,我們可以把它當成:
- 通用的物件工廠。這個有點像我們常用的
**Factory
,通過它,我們可以獲取到所需的物件。 - 全域性的上下文。把某個物件丟進這個上下文,然後可以在應用的任何位置獲取到這個物件。
針對 spring-bean 元件,本文主要講的是:
- spring-bean 是什麼?用來解決什麼問題?
- 幾個重要的概念,例如什麼是 bean?
- 如何使用 spring-bean?
spring-bean用來解決什麼問題?
spring-bean 主要是用來解耦實現類。這裡我用一個例子來說明,會更好理解一點。
假如我的業務系統使用的是 mysql 資料庫,我通過下面的方式獲取資料庫驅動物件。
public void save(User user) {
java.sql.Driver driver = new com.mysql.cj.jdbc.Driver();
Connection connection = driver.connect(url, properties);
// do something
}
看著是沒什麼問題,然而,有一天我需要把資料庫更換為 oracle,於是,我不得不更改程式碼。
public void save(User user) {
java.sql.Driver driver = new oracle.jdbc.driver.OracleDriver();
Connection connection = driver.connect(url, properties);
// do something
}
顯然,這是不合理的。那麼,有什麼辦法能做到不更改程式碼就能切換資料庫呢?
JDBC 規範中使用了DriverManager
來解耦資料庫實現,可以做到不更改程式碼就能切換資料庫。它是通過系統引數 或 SPI 來找到實現類。
public void save(User user) {
java.sql.Driver driver = DriverManager.getDriver(url);
Connection connection = driver.connect(url, properties);
// do something
}
大家應該都使用過 JDBC 吧,DriverManager
UserService
配套一個UserServiceManager
,給DepartmentService
配套一個DepartmentServiceManager
······,這是非常繁瑣的。類似的,常用的**Factory
也存在同樣的問題。
這時我們就會想,我不要那麼多的**Manager
或者**Factory
行不行?有沒有一個通用的物件工廠?
spring-bean 滿足了這種需求,它就是一個通用的物件工廠,可以用來建立UserService
,也可以用來建立DepartmentService
。當然,前提是,你需要告訴 beanFactory 如何建立這個物件。
public void save(User user) {
java.sql.Driver driver = beanFactory.getBean(java.sql.Driver.class);
Connection connection = driver.connect(url, properties);
// do something
}
所以,spring-bean 本質上就是用來解耦實現類。除此之外,spring-bean 也是一個全域性的上下文,我把某個物件丟進這個上下文,然後可以在應用的任何位置獲取到這個物件。這個比較簡單,就不展開討論了。
幾個重要的概念
在介紹如何使用 spring-bean 之前,先來看看幾個重要的概念。
什麼是bean
按照官方的說法, bean 是一個由 Spring IoC 容器例項化、組裝和管理的物件。
我認為,官方的表述是錯誤的。在後面的使用例子中,我們會發現,如果純粹把 spring-bean 當成一個全域性的上下文,我們放進這個上下文的物件已經是一個完整的物件例項,並不會由 Spring IoC 例項化、組裝,所以,更準確的表述應該是這樣:
通過 beanFactory 獲取到的物件都屬於 bean。至於什麼是 IoC 容器,在 spring-bean 元件中,我認為,beanFactory 就屬於 IoC 容器。
粗俗一點地比喻,人的消化系統就是一個 IoC 容器,拉出來的粑粑就是 bean,而拉出來的粑粑可以是你吃進去的各種食物“組裝”出來的,也可以是你直接吃進去的。
例項化、屬性裝配和初始化
在 spring-bean 元件的設計中,例項化、屬性裝配和初始化,它們完整、有序地描述了建立一個新物件的整個流程,它們是非常重要的理論基礎。具體含義如下:
- 例項化:new 一個新物件。
- 屬性裝配:給物件的成員屬性賦值。
- 初始化:呼叫物件的初始化方法。
下面通過一段程式碼來簡單演示下這個流程。
public class User {
private String name;
private Integer age;
public User() {
super();
System.err.println("主流程:User物件例項化中。。-->\n\t||\n\t\\/");
}
public void init() {
System.err.println("主流程:User物件初始化中。。-->\n\t||\n\t\\/");
}
public void setName(String name) {
System.err.println("主流程:User物件屬性name裝配中。。-->\n\t||\n\t\\/");
this.name = name;
}
}
如果我們將這個物件交給 spring-bean 管理,建立 bean 時會在控制檯列印以下內容:
如何使用spring-bean
依賴引入
除了引入 spring,這裡還額外引入了日誌和單元測試(可選)。
<dependencies>
<!-- spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>5.2.6.RELEASE</version>
</dependency>
<!-- junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<!-- logback -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
<type>jar</type>
</dependency>
</dependencies>
作為全域性上下文使用
spring-bean 是一個全域性的上下文,我把某個物件丟進這個上下文,然後可以在應用的任何位置獲取到這個物件。注意,這種方式註冊的 bean,例項化、屬性裝配和初始化並不由 spring-bean 來管理。
public void testContext() {
// 建立beanFactory
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
User user = new User("zzs001", 18);
// 存物件
beanFactory.registerSingleton("user", user);
// 取物件
User user2 = (User)beanFactory.getBean("user");
assertEquals(user, user2);
}
作為物件工廠使用
如果把 spring-bean 當成物件工廠使用,我們需要告訴它如何建立物件,而beanDefinition 就包含了如何建立物件的所有資訊。
public void testObjectFactory() {
// 建立beanFactory
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
// 定義一個beanDefinition
BeanDefinition rootBeanDefinition = BeanDefinitionBuilder.rootBeanDefinition(User.class).getBeanDefinition();
// 屬性裝配
rootBeanDefinition.getPropertyValues().add("name", "zzs001");
rootBeanDefinition.getPropertyValues().add("age", 18);
// 初始化方法
rootBeanDefinition.setInitMethodName("init");
// 單例還是多例,預設單例
rootBeanDefinition.setScope(BeanDefinition.SCOPE_PROTOTYPE);
// 註冊bean
beanFactory.registerBeanDefinition("user", rootBeanDefinition);
// 獲取bean
User user = (User)beanFactory.getBean("user");
assertNotNull(user);
}
多種獲取bean的方式
實際使用中,我們更多的會使用 beanType 而不是 beanName 來獲取 bean,beanFactory 也提供了相應的支援。我們甚至還可以同時使用 beanName 和 beanType,獲取到指定 beanName 的 bean 後會進行型別檢查,如果不通過,將會報錯。
public void testGetBeanWays() {
// 建立beanFactory
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
// 註冊bean
BeanDefinition rootBeanDefinition = BeanDefinitionBuilder.rootBeanDefinition(User.class).getBeanDefinition();
beanFactory.registerBeanDefinition("user", rootBeanDefinition);
// 獲取bean--通過beanName
User user1 = (User)beanFactory.getBean("user");
assertNotNull(user1);
// 獲取bean--通過beanType
User user2 = beanFactory.getBean(User.class);
assertNotNull(user2);
// 獲取bean--通過beanName+beanType的方式
User user3 = beanFactory.getBean("user", User.class);
assertNotNull(user3);
}
使用TypeConverter獲取自定義型別的物件
在上面的例子中,當使用 beanName + beanType 來獲取 bean 時,如果獲取到的 bean 不是指定的型別,這時,並不會立即報錯,beanFactory 會嘗試使用合適TypeConverter
來強制轉換(需要我們註冊上去)。
public void testTypeConverter() {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
// 註冊型別轉換器
beanFactory.setTypeConverter(new TypeConverterSupport() {
@SuppressWarnings("unchecked")
@Override
public <T> T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType, @Nullable TypeDescriptor typeDescriptor) throws TypeMismatchException {
if(UserVO.class == requiredType && value instanceof User) {
User user = (User)value;
UserVO userVO = new UserVO();
userVO.setName(user.getName());
userVO.setAge(user.getAge());
return (T)userVO;
}
return null;
}
});
BeanDefinition rootBeanDefinition = BeanDefinitionBuilder.rootBeanDefinition(User.class).getBeanDefinition();
beanFactory.registerBeanDefinition("user", rootBeanDefinition);
UserVO bean = beanFactory.getBean("user", UserVO.class);
Assert.assertNotNull(bean);
}
bean衝突的處理
通過 beanName 獲取 bean 和 通過 beanType 獲取 bean 的區別在於,前者能唯一匹配到所需的 bean,後者就不一定了。如果我註冊了兩個相同 beanType 的 bean(這是允許的),通過 beanType 獲取 bean 時就會報錯。
public void testPrimary() {
// 建立beanFactory
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
// 註冊bean
BeanDefinition rootBeanDefinition = BeanDefinitionBuilder.rootBeanDefinition(User.class).getBeanDefinition();
beanFactory.registerBeanDefinition("user", rootBeanDefinition);
beanFactory.registerSingleton("user2", new User("zzs002", 19));
beanFactory.registerSingleton("user3", new User("zzs003", 18));
// 獲取bean
User user = beanFactory.getBean(User.class);
assertNotNull(user);
}
執行以上方法,將出現 NoUniqueBeanDefinitionException 的異常。
通過 beanType 獲取 bean 時,當存在多個同類型 bean 的時候,spring-bean 的處理邏輯是這樣的:
- 只保留通過 registerSingleton 註冊的以及通過registerBeanDefinition註冊且autowireCandidate = true的;
- 檢查是否存在唯一一個
isPrimary = true
的 bean,存在的話將它返回; - 通過
OrderComparator
來計算每個 bean 的 priority,取 priority 最小的返回(OrderComparator
需要我們自己註冊)。注意,通過 registerSingleton 註冊的和通過 registerBeanDefinition 註冊的,比較的物件是不一樣的,前者比較的物件是 bean 例項,後者比較的物件是 bean 型別,另外,這種方法不能存在相同 priority 的 bean。
所以,為了解決這種衝突,可以採取三種方法:
- 僅保留一個 beanDefinition 的 autowireCandidate = true。全部 beanName 都是通過 registerBeanDefinition 註冊的才有效。
- 設定其中一個 beanDefinition 的 isPrimary = true。
- 為 beanFactory 註冊
OrderComparator
(這種用的不多)。
程式碼如下:
public void testPrimary() {
// 建立beanFactory
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
// 為BeanFactory設定比較器,比較少用
beanFactory.setDependencyComparator(new OrderComparator() {
@Override
public Integer getPriority(Object obj) {
return obj.hashCode();
}
});
// 註冊bean
BeanDefinition rootBeanDefinition = BeanDefinitionBuilder.rootBeanDefinition(User.class).getBeanDefinition();
// rootBeanDefinition.setAutowireCandidate(false);
// rootBeanDefinition.setPrimary(true); // 設定bean優先
beanFactory.registerBeanDefinition("user", rootBeanDefinition);
beanFactory.registerSingleton("user2", new User("zzs002", 19));
beanFactory.registerSingleton("user3", new User("zzs003", 18));
// 獲取bean
User user = beanFactory.getBean(User.class);
assertNotNull(user);
}
一種特殊的bean--FactoryBean
beanFactory 還支援註冊一種特殊的物件--factoryBean,當我們獲取 bean 時,拿到的不是這個 factoryBean,而是 factoryBean.getObject() 所返回的物件。那我就是想返回 factoryBean 怎麼辦?可以通過以下形式的 beanName 獲取:一個或多個& + beanName。
public void testFactoryBean() throws BeansException, Exception {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
// 註冊bean--註冊一個 factoryBean
UserFactoryBean userFactoryBean = new UserFactoryBean();
beanFactory.registerSingleton("user", userFactoryBean);
// 通過beanName獲取
assertEquals(User.class, beanFactory.getBean("user").getClass());
// 通過beanType獲取
assertEquals(User.class, beanFactory.getBean(User.class).getClass());
// 通過&+factoryBeanName的方式
assertEquals(UserFactoryBean.class, beanFactory.getBean("&user").getClass());
}
自動裝配
預設情況下,beanFactory 會讀取 beanDefinition 物件中的 propertyValues 來裝配成員屬性,所以,我們想要裝配哪個成員屬性,只要把鍵值對 add 進這個 propertyValues 就行。前提是我們的 bean 必須包含對應成員屬性的 setter 方法。
spring-bean 還提供了更有趣的功能--自動裝配。我只需要將 beanDefinition 的 autowireMode 設定為自動裝配,beanFactory 就會幫我把包含 setter 方法的所有成員屬性都賦值(當然,要有值才會賦)。
public void testAutowire() {
// 建立beanFactory
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
// 註冊userService
AbstractBeanDefinition userServiceBeanDefinition = BeanDefinitionBuilder.rootBeanDefinition(UserService.class).getBeanDefinition();
beanFactory.registerBeanDefinition("userService", userServiceBeanDefinition);
// 註冊userDao
AbstractBeanDefinition userDaoBeanDefinition = BeanDefinitionBuilder.rootBeanDefinition(UserDao.class).getBeanDefinition();
beanFactory.registerBeanDefinition("userDao", userDaoBeanDefinition);
// 給userService設定裝配屬性userDao
// userServiceBeanDefinition.getPropertyValues().add("userDao", userDaoBeanDefinition);
userServiceBeanDefinition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
// 獲取bean
UserService userService = (UserService)beanFactory.getBean("userService");
assertNotNull(userService.getUserDao());
}
bean 例項化、屬性裝配和初始化的處理器
前面講到,我們將 bean 的例項化、屬性裝配和初始化都交給了 spring-bean 處理,然而,有時我們需要在這些節點對 bean 進行自定義的處理,這時就需要用到 beanPostProcessor。
這裡我簡單演示下如何新增處理器,以及處理器的執行時機,至於處理器的具體實現,我就不多擴充套件了。
public void testPostProcessor() {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
// 新增例項化處理器
beanFactory.addBeanPostProcessor(new InstantiationAwareBeanPostProcessor() {
// 例項前處理
// 如果這裡我們返回了物件,則beanFactory會將它直接返回,不再進行bean的例項化、屬性裝配和初始化等操作
public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException {
System.out.println("處理器:bean例項化之前的處理。。 --> ");
return null;
}
// 例項後處理
// 這裡判斷是否繼續對bean進行屬性裝配和初始化等操作
public boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException {
System.out.println("處理器:bean例項化之後的處理。。 --> ");
return true;
}
});
// 新增裝配處理器
beanFactory.addBeanPostProcessor(new InstantiationAwareBeanPostProcessor() {
// 屬性裝配前
// 這裡可以在屬性裝配前對引數列表進行調整
public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) throws BeansException {
System.out.println("處理器:屬性裝配前對引數列表進行調整。。--> ");
return InstantiationAwareBeanPostProcessor.super.postProcessProperties(pvs, bean, beanName);
}
});
// 新增初始化處理器
beanFactory.addBeanPostProcessor(new BeanPostProcessor() {
// 初始化前
// 這裡可以在初始化前對bean進行改造
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
System.out.println("處理器:初始化前,對bean進行改造。。 --> ");
return bean;
}
// 初始化後
// 這裡可以在初始化後對bean進行改造
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
System.out.println("處理器:初始化後,對bean進行改造。。 --> ");
return bean;
}
});
// 定義一個beanDefinition
BeanDefinition rootBeanDefinition = BeanDefinitionBuilder.rootBeanDefinition(User.class).getBeanDefinition();
// 屬性裝配
rootBeanDefinition.getPropertyValues().add("name", "zzs001");
rootBeanDefinition.getPropertyValues().add("age", 18);
// 初始化方法
rootBeanDefinition.setInitMethodName("init");
// 單例還是多例,預設單例
rootBeanDefinition.setScope(BeanDefinition.SCOPE_PROTOTYPE);
// 註冊bean
beanFactory.registerBeanDefinition("user", rootBeanDefinition);
User user = (User)beanFactory.getBean("user");
assertNotNull(user);
}
執行以上方法,控制檯打印出了整個處理流程。實際開發中,我們可以通過設定處理器來改造生成的 bean 。
以上,基本介紹完 spring-bean 元件的使用。