Spring Framework 總共有十幾個元件,但真正核心的元件隻有三個:Spring Core,Spring Context 和 Spring Bean,它們奠定了 Spring 的基礎并撐起了 Spring 的架構結構
盡管希臘哲學家赫拉克利特(Heraclitus)并不作為一名軟體開發人員而聞名,但他似乎深谙此道。他的一句話經常被引用:“唯一不變的就是變化”,這句話抓住了軟體開發的真谛。
我們現在開發應用的方式和1年前、5年前、10年前都是不同的,更别提15年前了,當時RodJohnson的圖書 Expert One-on-One J2EE Design and Development 介紹了Spring架構的初始形态。當時,最常見的應用形式是基于浏覽器的Web應用,後端由關系型資料庫作為支撐。盡管這種形式的開發依然有它的價值,Spring也為這種應用提供了良好的支援,但是我們現在感興趣的還包括如何開發面向雲的由微服務組成的應用,這些應用會将資料儲存到各種類型的資料庫中。
另外一個嶄新的關注點是反應式程式設計,它緻力于通過非阻塞操作提供更好的擴充性并提升性能。随着軟體開發的發展,Spring架構也在不斷變化,以解決現代應用開發中的問題,其中就包括微服務和反應式程式設計。Spring還通過引入Spring Boot簡化自己的開發模型。
Spring 的核心
任何實際的應用程式都是由很多元件組成的,每個元件負責整個應用功能的一部分,這些元件需要與其他的應用元素進行協調以完成自己的任務。當應用程式運作時,需要以某種方式建立并引入這些元件。
Spring Framework 總共有十幾個元件,但真正核心的元件隻有三個:Spring Core,Spring Context 和 Spring Bean,它們奠定了 Spring 的基礎并撐起了 Spring 的架構結構。Spring 的其它功能特性例如 Web、AOP、JDBC 等都是在其基礎上發展實作的。
Spring之中最重要的當屬Bean了,Spring實際上就是面向Bean的程式設計,Bean對于Spring的意義就好比Object對于OOP的意義一樣。那麼,三個核心元件之間是如何協同工作的呢?如果把Bean比作一場演出中的演員,那麼Context就是這場演出的舞台,Core就是演出的道具,至于演出的節目,就是Spring的一系列特色功能了。
我們知道Bean包裹的是Object,而Object中必然有資料,Context就是給這些資料提供生存環境,發現每個Bean之間的關系,為他們建立并維護好這種關系。這樣來說,Context就是一個Bean關系的集合,這個關系集合就是我們所說的IOC容器。那麼Core又有什麼作用呢?Core就是發現、建立和維護每個Bean之間的關系所需的一系列工具,就是我們經常說的Util。
Bean 元件
Bean元件在Spring的org.springframework.beans包下,主要完成了Bean的建立、Bean的定義以及Bean的解析三件事。
SpringBean的建立是典型的工廠模式,其工廠的繼承層次關系如圖所示:
Spring 使用工廠模式來管理程式中使用的對象(Bean),Bean 工廠最上層的接口為 BeanFactory,簡單來看,工廠就是根據需要傳回相應的 Bean 執行個體。
public interface BeanFactory {
//...
Object getBean(String name);
}
在工廠模式中,在工廠的實作類中生成 Bean 傳回給調用用戶端,這就要求用戶端提供生成自己所需類執行個體的工廠類,增加客戶負擔。Spring 結合控制反轉和依賴注入為用戶端提供所需的執行個體,簡化了用戶端的操作。具體的實作方式大緻如下。
public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFactory
implements ConfigurableListableBeanFactory, BeanDefinitionRegistry, Serializable {
/** Map of bean definition objects, keyed by bean name */
private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<String, BeanDefinition>;
public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition){
//...
}
}
beanDefinitionMap 作為具體的 Bean 容器,Spring 建立的對象執行個體儲存其中。用戶端需要時,使用工廠的 getBean 方法去試圖得到相應的執行個體,如果執行個體已存在,則傳回該執行個體;如果執行個體不存在,則首先産生相應執行個體并通過 registerBeanDefinition 方法将其儲存在 beanDefinitionMap 中(Lazy Initialization),然後傳回該執行個體給用戶端。
Spring Bean 工廠的繼承關系beanDefinitionMap 并不直接儲存執行個體本身,而是将執行個體封裝在 BeanDefinition 對象後進行儲存。BeanDefinition 包含了執行個體的所有資訊,其簡化版的定義如下。
public class BeanDefinition {
private Object bean;
private Class<?> beanClass;
private String beanClassName;
// Bean 屬性字段的初始化值
private BeanPropertyValues beanPropertyValues;
//...
}
Spring Bean 工廠生産 Bean 時
- 先将執行個體的類型參數儲存到 beanClass 和 beanClassName,将需要初始化的字段名和值儲存到 beanPropertyValues 中,這個過程 Spring 通過控制反轉來實作,本文第二小節将予以簡要說明
-
生成 bean 執行個體,并利用反射機制将需要初始化的字段值寫入 bean 執行個體,将執行個體儲存在 bean 中,完成 BeanDefinition 的建構。
假設我們已經完成了步驟 1) 的操作,之後的過程用代碼表述如下所示。
public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition){
//生成 bean 執行個體,并完成初始化
Object bean = createBean(beanDefinition);
//将 bean 執行個體儲存在 beanDefinition 中
beanDefinition.setBean(bean);
//将 beanDefinition 執行個體儲存在 Spring 容器中
beanDefinitionMap.put(beanName, beanDefinition);
}
protected Object createBean(BeanDefinition beanDefinition) {
try{
Object bean = beanDefinition.getBeanClass().newInstance();
try {
setBeanPropertyValues(bean, beanDefinition);
} catch (NoSuchFieldException | SecurityException | IllegalArgumentException e) {
e.printStackTrace();
}
return bean;
}catch(InstantiationException e){
e.printStackTrace();
}catch(IllegalAccessException e){
e.printStackTrace();
}
return null;
}
protected void setBeanPropertyValues(Object bean, BeanDefinition beanDefinition) throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException{
for(PropertyValue pv : beanDefinition.getBeanPropertyValues().getBeanPropertyValues()){
Field beanFiled = bean.getClass().getDeclaredField(pv.getName());
beanFiled.setAccessible(true);
beanFiled.set(bean, pv.getValue());
}
}
Context 元件
前面說到,Context元件的作用是給Spring提供一個運作時的環境,用以儲存各個對象的狀态,我們來看一下與Context相關的類結構圖。
從圖中可以看出,Context類結構的頂級父類是ApplicationContext,它除了能辨別一個應用環境的基本資訊以外,還繼承了5個接口,這5個接口主要是擴充了Context的功能。ApplicationContext的子類主要包含兩個方向,圖中已作說明。再往下就是建構Context的檔案類型,接着就是通路Context的方式。
一般地,傳統的程式設計中,無論是使用工廠建立執行個體,或是直接建立執行個體,執行個體調用者都要先主動建立執行個體,而後才能使用。控制反轉(Inverse of Control) 将執行個體的建立過程交由容器實作,調用者将控制權交出,是所謂控制反轉。
依賴注入(Dependence Injection) 在控制反轉的基礎上更進一步。如果沒有依賴注入,容器建立執行個體并儲存後,調用者需要使用 getBean(String beanName) 才能擷取到執行個體。使用依賴注入時,容器會将 Bean 執行個體自動注入到完成相應配置的調用者,供其進一步使用。Context 元件借助上述的控制反轉和依賴注入,協助實作了 Spring 的 Ioc 容器。下面我們以一個 Service 類作為所需的 Bean 執行個體進行說明。實際應用中,我們會需要 Spring 管理很多 Bean 執行個體。
public class SampleService {
private String service;
public String getService() {
return service;
}
public void setService(String service) {
this.service= service;
}
}
在程式運作過程中,需要一個 SampleService ,我們不讓調用者 new 一個執行個體,而是在配置檔案中表明該 SampleService 的執行個體交由 Spring 容器進行管理,并指定其初始化參數。配置檔案即資源,其内容如下。
<?xml version="1.0" encoding="UTF-8"?>
<beans>
<bean name="sampleService " class="com.service.SampleService ">
<property name="service" value="This is a service"></property>
</bean>
</beans>
Spring Core 元件提供 ResourceLoader 接口,便于讀入 xml 檔案或其他資源檔案。其核心功能代碼應該提供如下方法。
public class ResourceLoader {
public Resource getResource(String location){
URL resource = this.getClass().getClassLoader().getResource(location);
return new UrlResource(resource);
}
}
// UrlResource 的功能代碼
public class UrlResource implements Resource {
private final URL url;
public UrlResource(URL url){
this.url = url;
}
@Override
public InputStream getInputStream() throws IOException {
URLConnection urlConnection = url.openConnection();
urlConnection.connect();
return urlConnection.getInputStream();
}
}
即加載資源檔案,并以資料流的形式傳回。Context 根據資源中的定義,生成相應的 bean 并儲存在容器中,bean 的名字是 sampleService ,供程式進一步使用。這樣就完成了控制反轉的工作。接下來就需要把 sampleService 注入到需要使用它的地方,亦即完成依賴注入操作。現在假設 SampleController 中使用 SampleService 的對象,Spring 提供三種依賴注入的方式,構造器注入、setter 注入和注解注入。
public class SampleController {
/**
* 3\. 注解注入
**/
/* @Autowired */
private SampleService sampleService;
/**
* 1\. 構造器注入
**/
public SampleController(SampleService sampleService){
this.sampleService = sampleService;
}
//無參構造函數
public SampleController(){}
// 類的核心功能
public void process(){
System.out.println(sampleService.getService());
}
/**
* 2\. setter 注入
**/
/*public void setService(SampleService service) {
this.service= service;
}*/
}
三種注入方式在配置檔案中對應不同的配置方式,在前面 xml 檔案的基礎上,我們可以分别實作這三種注入方式。需要注意的是,這裡 SampleController 也是使用 Spring 的 Ioc 容器生成管理的。
<?xml version="1.0" encoding="UTF-8"?>
<beans>
<bean name="sampleService " class="com.service.SampleService ">
<property name="service" value="This is a service"></property>
</bean>
<!-- 1\. 構造器注入方式為SampleContorller 的 bean 注入 SampleService -->
<bean name="sampleContorller" class="com.controller.SampleContorller">
<!-- index 是構造方法中相應參數的順序 -->
<constructor-arg index="0" ref="sampleService"></constructor-arg>
</bean>
<!-- 2\. setter 注入方式為SampleContorller 的 bean 注入 SampleService -->
<!--
<bean name="sampleContorller" class="com.controller.SampleContorller">
<property name="sampleService " ref="sampleService"></property>
</bean>
-->
<!-- 3\. 注解注入方式為SampleContorller 的 bean 注入 SampleService -->
<!--
<bean name="sampleContorller" class="com.controller.SampleContorller">
<!-- 不需要配置,Spring 自動按照類型注入相應的 bean -->
</bean>
-->
</beans>
Core元件
Core元件一個重要的組成部分就是定義了資源的通路方式。Core組價把所有的資源都抽象成一個接口,這樣,對于資源使用者來說,不需要考慮檔案的類型。對資源提供者來說,也不需要考慮如何将資源包裝起來交給别人使用(Core元件内所有的資源都可以通過InputStream類來擷取)。另外,Core元件内資源的加載都是由ResourceLoader接口完成的,隻要實作這個接口就可以加載所有的資源。
那麼,Context和Resource是如何建立關系的呢?通過前面Context的介紹我們知道,Context元件裡面的類或者接口最終都實作了ResourcePatternResolver接口,ResourcePatternResolver接口的作用就是加載、解析和描述資源。這個接口相當于Resource裡面的一個接頭人,它把Resource裡的資源加載、解析和定義整合到一起,便于其他元件使用。
前面介紹了三大核心元件的結構與互相關系,那麼,這三大元件是如何讓Spring完成諸如IOC和AOP等各種功能的呢?敬請期待下一篇文章!