標題中的咖啡罐指的是Spring容器,容器里裝的當然就是被稱作Bean的豆子。本文我們會以一個最基本的例子來熟悉Spring的容器管理和擴展點。
閱讀PDF版本
為什么要讓容器來管理對象?
首先我們來聊聊這個問題,為什么我們要用Spring來管理對象(的生命周期和對象之間的關系)而不是自己new一個對象呢?大家可能會回答是方便,為了解耦。我個人覺得除了這兩個原因之外,還有就是給予了我們更多可能性。如果我們以容器為依托來管理所有的框架、業務對象,那么不僅僅我們可以無侵入調整對象的關系,還有可能無侵入隨時調整對象的屬性甚至悄悄進行對象的替換。這就給了我們無限多的可能性,大大方便了框架的開發者在程序背后實現一些擴展。不僅僅Spring Core本身以及Spring Boot大量依賴Spring這套容器體系,一些外部框架也因為這個原因可以和Spring進行無縫整合。
Spring可以有三種方式來配置Bean,分別是最早期的XML方式、后來的注解方式以及現在最流行的Java代碼配置方式。
Bean的回調事件
在前文parent模塊(空的一個SpringBoot應用程序)的基礎上,我們先來創建一個beans模塊:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>me.josephzhu</groupId><artifactId>spring101-beans</artifactId><version>0.0.1-SNAPSHOT</version><packaging>jar</packaging><name>spring101-beans</name><description></description><parent><groupId>me.josephzhu</groupId><artifactId>spring101</artifactId><version>0.0.1-SNAPSHOT</version></parent><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>
</project>
然后來創建我們的豆子:
package me.josephzhu.spring101beans;import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;@Component
public class MyService implements InitializingBean, DisposableBean {public int increaseCounter() {this.counter++;return counter;}public int getCounter() {return counter;}public void setCounter(int counter) {this.counter = counter;}private int counter=0;public MyService(){counter++;System.out.println(this + "#constructor:" + counter);}public String hello(){return this + "#hello:" + counter;}@PreDestroypublic void preDestroy() {System.out.println(this + "#preDestroy:" + counter);}@Overridepublic void afterPropertiesSet() {counter++;System.out.println(this + "#afterPropertiesSet:" + counter);}@PostConstructpublic void postConstruct(){counter++;System.out.println(this + "#postConstruct:" + counter);}@Overridepublic void destroy() {System.out.println(this + "#destroy:" + counter);}
}
這里可以看到,我們的服務中有一個counter字段,默認是0。這個類我們實現了InitializingBean接口和DisposableBean接口,同時還創建了兩個方法分別加上了@PostConstruct和@PreDestroy注解。這兩套實現方式都可以在對象的額外初始化功能和釋放功能,注解的實現不依賴Spring的接口,侵入性弱一點。
接下去,我們創建一個Main類來測試一下:
package me.josephzhu.spring101beans;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;import javax.annotation.Resource;@SpringBootApplication
public class Spring101BeansApplication implements CommandLineRunner {@Autowiredprivate ApplicationContext applicationContext;@Resourceprivate MyService helloService;@Autowiredprivate MyService service;public static void main(String[] args) {SpringApplication.run(Spring101BeansApplication.class, args);}@Overridepublic void run(String... args) throws Exception {System.out.println("====================");applicationContext.getBeansOfType(MyService.class).forEach((name, service)->{System.out.println(name + ":" + service);});System.out.println("====================");System.out.println(helloService.hello());System.out.println(service.hello());}
}
ApplicationContext直接注入即可,不一定需要用ApplicationContextAware方式來獲取。執行程序后可以看到輸出如下:
me.josephzhu.spring101beans.MyService@7fb4f2a9#constructor:1
me.josephzhu.spring101beans.MyService@7fb4f2a9#postConstruct:2
me.josephzhu.spring101beans.MyService@7fb4f2a9#afterPropertiesSet:3
====================
myService:me.josephzhu.spring101beans.MyService@7fb4f2a9
====================
me.josephzhu.spring101beans.MyService@7fb4f2a9#hello:3
me.josephzhu.spring101beans.MyService@7fb4f2a9#hello:3
me.josephzhu.spring101beans.MyService@7fb4f2a9#preDestroy:3
me.josephzhu.spring101beans.MyService@7fb4f2a9#destroy:3
這里我們使用@Resource注解和@Autowired注解分別引用了兩次對象,可以看到由于Bean默認配置為singleton單例,所以容器中MyService類型的對象只有一份,代碼輸出也可以證明這點。此外,我們也通過輸出看到了構造方法以及兩套Bean回調的次序是:
- 類自己的構造方法
- @PostConstruct注釋的方法
- InitializingBean接口實現的方法
- @PreDestroy注釋的方法
- DisposableBean接口實現的方法
Java 代碼方式創建Bean
從剛才的輸出中可以看到,在剛才的例子中,我們為Bean打上了@Component注解,容器為我們創建了名為myService的MyService類型的Bean。現在我們再來用Java代碼方式來創建相同類型的Bean,創建如下的文件:
package me.josephzhu.spring101beans;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;@Configuration
public class ApplicationConfig {@Bean(initMethod = "init")public MyService helloService(){MyService myService = new MyService();myService.increaseCounter();return myService;}}
這里可以看到在定義Bean的時候我們關聯了一個initMethod,因此我們需要修改Bean加上這個方法:
public void init() {counter++;System.out.println(this + "#init:" + counter);}
現在我們運行代碼看看結果,得到了如下錯誤:
Field service in me.josephzhu.spring101beans.Spring101BeansApplication required a single bean, but 2 were found:- myService: defined in file [/Users/zyhome/IdeaProjects/spring101/spring101-beans/target/classes/me/josephzhu/spring101beans/MyService.class]- helloService: defined by method 'helloService' in class path resource [me/josephzhu/spring101beans/ApplicationConfig.class]
出現錯誤的原因是@Autowired了一個MyService,@Resource注解因為使用Bean的名稱來查找Bean,所以并不會出錯,而@Autowired因為根據Bean的類型來查抄Bean找到了兩個匹配所有出錯了,解決方式很簡單,我們在多個Bean里選一個作為主Bean。我們修改一下MyService加上注解:
@Component
@Primary
public class MyService implements InitializingBean, DisposableBean
這樣,我們的@Resource根據名字匹配到的是我們@Configuration出來的Bean,而@Autowired根據類型+Primary匹配到了@Component注解定義的Bean,重新運行代碼來看看是不是這樣:
me.josephzhu.spring101beans.MyService@6cd24612#constructor:1
me.josephzhu.spring101beans.MyService@6cd24612#postConstruct:3
me.josephzhu.spring101beans.MyService@6cd24612#afterPropertiesSet:4
me.josephzhu.spring101beans.MyService@6cd24612#init:5
me.josephzhu.spring101beans.MyService@7486b455#constructor:1
me.josephzhu.spring101beans.MyService@7486b455#postConstruct:2
me.josephzhu.spring101beans.MyService@7486b455#afterPropertiesSet:3
====================
myService:me.josephzhu.spring101beans.MyService@7486b455
helloService:me.josephzhu.spring101beans.MyService@6cd24612
====================
me.josephzhu.spring101beans.MyService@6cd24612#hello:5
me.josephzhu.spring101beans.MyService@7486b455#hello:3
me.josephzhu.spring101beans.MyService@7486b455#preDestroy:3
me.josephzhu.spring101beans.MyService@7486b455#destroy:3
me.josephzhu.spring101beans.MyService@6cd24612#preDestroy:5
me.josephzhu.spring101beans.MyService@6cd24612#destroy:5
從輸出中我們注意到幾點:
- 先輸出的的確是helloService,說明@Resource引入的是我們Java代碼配置的MyService,helloService由于在我們配置的多調用了一次increaseCounter()以及關聯的initMethod,所以counter的值是5
- initMethod執行的順序在@PostConstruct注釋的方法和InitializingBean接口實現的方法之后
- 雖然我們的MySerive的兩種Bean的定義都是單例,但是這不代表我們的Bean就是一套,在這里我們通過代碼配置和注解方式在容器內創建了兩套MyService類型的Bean,它們都經歷了自己的初始化過程。通過@Resource和@Autowired引入到了是不同的Bean,當然也就是不同的對象
你還可以試試在使用@Autowired引入MyService的時候直接指定需要的Bean:
@Autowired
@Qualifier("helloService")
private MyService service;
兩個重要的擴展點
我們來繼續探索Spring容器提供給我們的兩個有關Bean的重要擴展點。
- 用于修改Bean定義的BeanFactoryPostProcessor。所謂修改定義就是修改Bean的元數據,元數據有哪些呢?如下圖所示,類型、名字、實例化方式、構造參數、屬性、Autowire模式、懶初始化模式、初始析構方法。實現了這個接口后,我們就可以修改這些已經定義的元數據,實現真正的動態配置。這里需要注意,我們不應該在這個接口的實現中去實例化Bean,否則這相當于提前進行了實例化會破壞Bean的生命周期。
- 用于修改Bean實例的BeanPostProcessor。在這個階段其實Bean已經實例化了,我們可以進行一些額外的操作對Bean進行修改。如下圖,我們可以清晰的看到Bean的生命周期如下(BeanPostProcessor縮寫為BPP):
- Bean定義加載
- BeanFactoryPostProcessor來修改Bean定義
- Bean逐一實例化
- BeanPostProcessor預處理
- Bean初始化
- BeanPostProcessor后處理
好,我們現在來實現這兩種類型的處理器,首先是用于修改Bean定義的處理器:
package me.josephzhu.spring101beans;import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.stereotype.Component;@Component
public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {@Overridepublic void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {BeanDefinition beanDefinition = configurableListableBeanFactory.getBeanDefinition("helloService");if (beanDefinition != null) {beanDefinition.setScope("prototype");beanDefinition.getPropertyValues().add("counter", 10);}System.out.println("MyBeanFactoryPostProcessor");}
}
這里,我們首先找到了我們的helloService(Java代碼配置的那個Bean),然后修改了它的屬性和Scope(還記得嗎,在之前的圖中我們可以看到,這兩項都是Bean的定義,定義相當于類描述,實例當然就是類實例了)。
然后,我們再來創建一個修改Bean實例的處理器:
package me.josephzhu.spring101beans;import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;@Component
public class MyBeanPostProcessor implements BeanPostProcessor {@Overridepublic Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {if (bean instanceof MyService) {System.out.println(bean + "#postProcessAfterInitialization:" + ((MyService)bean).increaseCounter());}return bean;}@Overridepublic Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {if (bean instanceof MyService) {System.out.println(bean + "#postProcessBeforeInitialization:" + ((MyService)bean).increaseCounter());}return bean;}
}
實現比較簡單,在這個處理器的兩個接口我們都調用了一次增加計數器的操作。我們運行代碼來看一下這兩個處理器執行的順序是否符合剛才那個圖的預期:
MyBeanFactoryPostProcessor
me.josephzhu.spring101beans.MyService@41330d4f#constructor:1
me.josephzhu.spring101beans.MyService@41330d4f#postProcessBeforeInitialization:11
me.josephzhu.spring101beans.MyService@41330d4f#postConstruct:12
me.josephzhu.spring101beans.MyService@41330d4f#afterPropertiesSet:13
me.josephzhu.spring101beans.MyService@41330d4f#init:14
me.josephzhu.spring101beans.MyService@41330d4f#postProcessAfterInitialization:15
me.josephzhu.spring101beans.MyService@6f36c2f0#constructor:1
me.josephzhu.spring101beans.MyService@6f36c2f0#postProcessBeforeInitialization:11
me.josephzhu.spring101beans.MyService@6f36c2f0#postConstruct:12
me.josephzhu.spring101beans.MyService@6f36c2f0#afterPropertiesSet:13
me.josephzhu.spring101beans.MyService@6f36c2f0#init:14
me.josephzhu.spring101beans.MyService@6f36c2f0#postProcessAfterInitialization:15
me.josephzhu.spring101beans.MyService@3b35a229#constructor:1
me.josephzhu.spring101beans.MyService@3b35a229#postProcessBeforeInitialization:2
me.josephzhu.spring101beans.MyService@3b35a229#postConstruct:3
me.josephzhu.spring101beans.MyService@3b35a229#afterPropertiesSet:4
me.josephzhu.spring101beans.MyService@3b35a229#postProcessAfterInitialization:5
====================
me.josephzhu.spring101beans.MyService@6692b6c6#constructor:1
me.josephzhu.spring101beans.MyService@6692b6c6#postProcessBeforeInitialization:11
me.josephzhu.spring101beans.MyService@6692b6c6#postConstruct:12
me.josephzhu.spring101beans.MyService@6692b6c6#afterPropertiesSet:13
me.josephzhu.spring101beans.MyService@6692b6c6#init:14
me.josephzhu.spring101beans.MyService@6692b6c6#postProcessAfterInitialization:15
myService:me.josephzhu.spring101beans.MyService@3b35a229
helloService:me.josephzhu.spring101beans.MyService@6692b6c6
====================
me.josephzhu.spring101beans.MyService@41330d4f#hello:15
me.josephzhu.spring101beans.MyService@6f36c2f0#hello:15
me.josephzhu.spring101beans.MyService@3b35a229#preDestroy:5
me.josephzhu.spring101beans.MyService@3b35a229#destroy:5
這個輸出結果有點長,第一行就輸出了MyBeanFactoryPostProcessor這是預料之中,Bean定義的修改肯定是最先發生的。我們看下輸出的規律,1、11、12、13、14、15出現了三次,之所以從1跳到了11是因為我們的BeanFactoryPostProcessor修改了其中的counter屬性的值為10。這說明了,我們的helloService的初始化進行了三次:
- 第一套指針地址是5a7fe64f,對應輸出第一個hello(),這是我們@Resource引入的
- 第二套指針地址是69ee81fc,對應輸出第二個hello(),這是我們@Autowird+@Qualifier引入的(剛才一節最后我們指定了helloService)
- 第三套指針地址是29f7cefd,這是我們getBeansOfType的時候創建的,對應下面Key-Value的輸出:
這里的輸出說明了幾點:
- 我們的BeanFactoryPostProcessor生效了,不但修改了helloService的Scope為prototype而且修改了它的counter屬性
- 對于Scope=ptototype的Bean,顯然在每次使用Bean的時候都會新建一個實例
- BeanPostProcessor兩個方法的順序結合一開始說的Bean事件回調的順序整體如下:
- 類自己的構造方法
- BeanFactoryPostProcessor接口實現的postProcessBeforeInitialization()方法
- @PostConstruct注釋的方法
- InitializingBean接口實現的afterPropertiesSet()方法
- Init-method定義的方法
- BeanFactoryPostProcessor接口實現的postProcessAfterInitialization()方法
- @PreDestroy注釋的方法
- DisposableBean接口實現的destroy()方法
最后,我們可以修改BeanFactoryPostProcessor中的代碼把prototype修改為singleton看看是否我們的helloService這個Bean恢復為了單例:
MyBeanFactoryPostProcessor
me.josephzhu.spring101beans.MyService@51891008#constructor:1
me.josephzhu.spring101beans.MyService@51891008#postProcessBeforeInitialization:11
me.josephzhu.spring101beans.MyService@51891008#postConstruct:12
me.josephzhu.spring101beans.MyService@51891008#afterPropertiesSet:13
me.josephzhu.spring101beans.MyService@51891008#init:14
me.josephzhu.spring101beans.MyService@51891008#postProcessAfterInitialization:15
me.josephzhu.spring101beans.MyService@49c90a9c#constructor:1
me.josephzhu.spring101beans.MyService@49c90a9c#postProcessBeforeInitialization:2
me.josephzhu.spring101beans.MyService@49c90a9c#postConstruct:3
me.josephzhu.spring101beans.MyService@49c90a9c#afterPropertiesSet:4
me.josephzhu.spring101beans.MyService@49c90a9c#postProcessAfterInitialization:5
====================
myService:me.josephzhu.spring101beans.MyService@49c90a9c
helloService:me.josephzhu.spring101beans.MyService@51891008
====================
me.josephzhu.spring101beans.MyService@51891008#hello:15
me.josephzhu.spring101beans.MyService@51891008#hello:15
me.josephzhu.spring101beans.MyService@49c90a9c#preDestroy:5
me.josephzhu.spring101beans.MyService@49c90a9c#destroy:5
me.josephzhu.spring101beans.MyService@51891008#preDestroy:15
me.josephzhu.spring101beans.MyService@51891008#destroy:15
本次輸出結果的hello()方法明顯是同一個bean,結果中也沒出現三次1、11、12、13、14、15。
總結
本文以探索的形式討論了下面的一些知識點:
- 容器管理對象的意義是什么
- Bean的生命周期回調事件
- Spring提供的Bean的兩個重要擴展點
- @Resource和@Autowired的區別
- 注解方式和代碼方式配置Bean
- @Primary和@Qualifier注解的作用
- Bean的不同類型的Scope