Spring3.X學習筆記-IoC容器概述
2017即將接近尾聲,每當年末的時候總要檢查下今年還有什麼事情沒做,結果卻發現今年好像也沒做多少事情。於是就想著把之前一直想梳理的Spring知識,趁著年末好好整理下。本系列筆記基於”Spring3.x企業應用開發實戰“一書,說來慚愧,書買了好多年了也沒認真的看一遍,於是就有了這個想法,算是2017年的最後給自己的一份交代。
在開始之前,先簡要介紹下Spring吧!
Spring是分層的Java SE/EE應用一站式的輕量級開源框架,由Rod Johnson建立,以IoC和AOP為核心,提供了展現層Spring MVC和持久層Spring JDBC以及業務層事務管理等眾多企業級應用技術,並以海納百川的胸懷整合了開源世界裡眾多的企業級應用技術,逐漸成為使用最多的Java EE企業應用開發框架。
1、IoC概述
1.1 IoC的概念
IoC(控制反轉:Inverse of Control)是一個重要的面向物件程式設計理論,Spring核心模組實現了IoC的功能。Spring中的其他模組,像AOP、宣告式事務等功能都是建立在IoC的基礎之上,它將類和類之間的依賴從程式碼中脫離出來,用配置的方式進行依賴關係描述,由IoC容器負責依賴類之間的建立、拼接、管理、獲取等工作。一般來說IoC的概念有兩種表示方式,一個叫控制反轉,一個叫依賴注入。由於控制反轉並不好理解,業界也曾進行廣泛的討論,最終軟體界的泰斗級人物Martin Folwer提出了DI(依賴注入:Dependency Injection)的概念用以代替IoC。
控制反轉:對於軟體來說,即某一介面具體實現類的選擇控制權從呼叫類中移除,轉交給第三方決定。
依賴注入:呼叫類對某一介面實現類的依賴關係由第三方(容器或協作類)注入,以移除呼叫類對某一介面實現類的依賴。
上面兩個概念第一次接觸的時候,你會發現都不太好理解,那是因為還不清楚IoC的實現機制,隨著對IoC瞭解的深入,你會發現依賴注入
的概念直接明瞭。
1.2 IoC的型別
從注入方法上看,主要可以劃分為三種類型:建構函式注入、屬性注入和介面注入。Spring支援建構函式注入和屬性注入。
- 建構函式注入: 在建構函式注入中,我們通過呼叫類的建構函式,將介面實現類通過建構函式變數傳入。
- 屬性注入: 屬性注入是指通過Setter方法完成呼叫類所需依賴的注入。
- 介面注入: 將呼叫類所有依賴注入的方法抽取到一個介面中,呼叫類通過實現該介面提供相應的注入方法。
由於通過介面注入需要額外宣告一個介面,增加了類的數目,而且它的效果和屬性注入並無本質區別,所以不提倡採用這種方式。
2、IoC的底層實現原理
Spring的核心模組實現了IoC的功能,它通過配置檔案或註解描述類和類之間的依賴關係,自動完成類的初始化和依賴注入的工作。讓開發者們從底層實現類的例項化、依賴關係裝配等工作中脫離出來,專注於更有意義的業務邏輯開發工作。這種“神奇”的力量歸功於Java語言本身的類反射功能。
2.1 Java反射知識
Java語言允許通過程式化的方式間接對Class的物件例項操作,Class檔案由類裝載器裝載後,在JVM中將形成一份描述CLass結構的元資訊物件,通過該元資訊物件可以獲知Class的結構資訊:如建構函式、屬性和方法等。Java允許使用者藉由這個Class相關的元資訊物件間接呼叫Class物件的功能,這就為使用程式化方式操作Class物件開闢了途徑。下面先看一個例子:
public class Car {
private String brand;
private String color;
private int maxSpeed;
public Car() {}
public Car(String brand, String color, int maxSpeed) {
this.brand = brand;
this.color = color;
this.maxSpeed = maxSpeed;
}
public void introduce() {
System.out.println("brand:" + brand + ";color:" + color + ";maxSpeed:" + maxSpeed);
}
// 省略引數的getter/Setter方法
}
public class ReflectTest {
public static Car initByDefaultConst() throws Throwable {
// 通過類裝載器獲取Car類物件
ClassLoader loader = Thread.currentThread().getContextClassLoader();
Class<?> clazz = loader.loadClass("com.hhxs.bbt.web.Car");
// 獲取類的預設構造器物件並通過它例項化Car
Constructor<?> cons = clazz.getDeclaredConstructor((Class[]) null);
Car car = (Car) cons.newInstance();
// 通過反射方法設定屬性
Method setBrand = clazz.getMethod("setBrand", String.class);
setBrand.invoke(car, "紅旗CA72");
Method setColor = clazz.getMethod("setColor", String.class);
setColor.invoke(car, "黑色");
Method setMaxSpeed = clazz.getMethod("setMaxSpeed", int.class);
setMaxSpeed.invoke(car, 200);
return car;
}
public static void main(String[] args) throws Throwable {
Car car1 = initByDefaultConst();
car1.introduce();
}
}
通過檢視執行結果,可以看到這和直接通過建構函式和方法呼叫類功能的效果是一致的,只不過前者是間接呼叫,後者是直接呼叫罷了。這說明我們完全可以通過程式設計方式呼叫Class的各項功能。如果我們將這些資訊以一個配置檔案的方式提供,就可以使用Java語言的反射功能編寫一段通用程式碼對類似於Car的類進行例項化及功能呼叫操作了。有沒有感覺這和上面提到的Spring框架的實現機制很相似,Spring正是基於Java語言自帶的反射機制實現了IoC的功能。
下面簡要介紹下上述例子用到得三個主要反射類,這些反射物件類在java.reflect
包中定義:
- Constructor: 類的建構函式反射類,通過Class#getConstructors()方法可以獲得類的所有建構函式反射物件陣列。Constructor的一個主要方法是newInstance(Object[]… initargs),通過該方法可以建立一個物件類的例項,相當
new
關鍵字。 - Method: 類方法的反射類,通過Class#getDeclaredMethods()方法可以獲取類的所有方法發射類物件陣列Method[]。Method最主要的方法是invoke(Object obj, Objcet… args),obj表示操作的目標物件,args為方法入參。
- Field: 類的成員變數反射類,通過Class#getDeclaredFields()方法可以獲取類的成員變數反射物件陣列。Filed類最主要的方法是set(Object obj, Object value),obj表示操作的目標物件,通過value為目標類物件的成員變數設定值。
此外,Java還為包提供了Package反射類,在JDK5.0中還未註解提供了AnnotatedElement反射類。總之,Java的反射體系保證了可以通過程式化的方式訪問目標類中的所有元素,對於private或protected的成員變數和方法,只要JVM的安全機制允許,也可以通過反射進行呼叫setAccessible(boolean access)
。
3、三個核心介面
Spring通過一個配置檔案描述Bean及Bean之間的依賴關係,利用Java語言的反射功能例項化Bean並建立Bean之間的依賴關係。Spring的IoC容器在完成這些底層工作的基礎上,還提供了Bean例項快取、宣告週期管理、Bean例項代理、事件釋出、資源裝載等高階服務。
3.1 BeanFactory
Bean工廠(com.springframework.beans.factory.BeanFactory)是Spring框架最核心的介面,它提供了高階IoC的配置機制。BeanFactory使管理不同不同型別的Java物件成為可能,一般稱BeanFactory為IoC容器。BeanFactory是類的通用工廠,它可以建立並管理各種類的物件,Spring稱這些被建立和管理的Java物件為Bean。Bean最主要的方法就是getBean(String beanName)
,該方法從容器中返回特定名稱的Bean。下面看一個小例子:
// spring檔案
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
<bean id="car" class="com.hhxs.bbt.web.Car"
p:brand="紅旗CA72"
p:color="黑色"
p:maxSpeed="200" />
</beans>
// JAVA程式碼
public class BeanFactoryTest {
public static void main(String[] args) throws Throwable{
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
Resource res = resolver.getResource("classpath:spring/beans.xml");
System.out.println(res.getURL());
BeanFactory bf = new XmlBeanFactory(res);
System.out.println("init BeanFactory.");
Car car = bf.getBean("car",Car.class);
System.out.println("car bean is ready for use!");
car.introduce();
}
}
注意: 通過BeanFactory啟動IoC容器時,並不會初始化配置檔案中定義的Bean,初始化動作發生在第一次呼叫時。對於單例項的Bean來說,BeanFactory會快取Bean例項,所以第二次使用getBean()獲取Bean時將直接從IoC容器的快取中獲取Bean例項。Spring在DefaultSingletonBeanRegistry類中提供了一個用於快取單例項Bean的快取器,它是一個用HashMap實現的快取器,單例項的Bean以beanName為鍵儲存在這個HashMap中。
3.2 ApplicationContext
應用上下文(com.springframework.context.ApplicationContext)建立在BeanFactory基礎之上,提供了更多面嚮應用的功能,它提供了國際化支援和框架事件體系,更易於建立實際應用。一般稱ApplicationContext為應用上下文或者Spring容器。
ApplicationContext的主要實現類是ClassPathXmlApplicationContext和FileSystemXmlApplicationContext,前者預設從類路徑中載入配置檔案,後者預設從檔案系統中裝載配置檔案。
ApplicationContext ctx = new ClassPathXmlApplicationContext("spring/beans.xml");
//或者
ApplicationContext ctx = new FileSystemXmlApplicationContext("spring/beans.xml");
在獲取ApplicationContext例項後,就可以像BeanFactory一樣呼叫getBean(beanName)返回Bean了。需要注意的是:ApplicationContext在初始化應用上下文時就例項化所有單例項的Bean。因此,相比BeanFactory,初始化時間也會相對較長些,不過之後的呼叫就不在有“第一次懲罰”的問題。
3.3 WebApplicationContext
WebApplicationContext是專門為Web應用準備的,它允許應用從相對於Web根目錄的路徑中裝載配置檔案完成初始化工作。從WebApplicationContext中可以獲得ServletContext的引用,整個Web應用上下文物件將作為屬性放置到ServletContext中,以便Web應用環境可以訪問Spring應用上下文。
WebApplicationContext的初始化方式和BeanFactory、ApplicationContext有所區別,因為WebApplicationContext需要ServletContext例項,也就是說它必須在擁有Web容器的前提下才能完成啟動的工作。有過Web開發經驗的讀者都知道可以在web.xml中配置自啟動的Servlet(spring3.0及以後版本中已經刪除)或定義Web容器監聽器(ServletContextListener),藉助這兩者中的任何一個都可以完成啟動Spring Web應用上下文的工作。
通過web容器監聽器啟動:web.xml
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
classpath:/spring/spring-context.xml
</param-value>
</context-param>
<!-- spring容器啟動監聽器 -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
4、Bean的生命週期
4.1 BeanFactory中Bean的生命週期
我們知道Web容器中的Servlet擁有明確的生命週期,Spring容器中的Bean也擁有相似的生命週期。我們可以從兩個層面定義Bean的生命週期:第一個層面是Bean的作用範圍;第二個層面是例項化Bean時所經歷的一系列階段。
1. 當呼叫者通過getBean(beanName)向容器請求某一個Bean時,如果容器註冊了org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessor介面,在例項化Bean之前,將呼叫介面的postProcessBeforeInstantiation()方法;
2. 根據配置情況呼叫Bean建構函式或工廠方法例項化Bean;
3. 如果容器註冊了InstantiationAwareBeanPostProcessor介面,在例項化Bean之後,呼叫該介面的postProcessAfterInstantiationn()方法,可以在這裡對已經例項化的物件進行處理。
4. 如果Bean配置了屬性資訊,容器在這一步著手將配置值設定到Bean對應的屬性中,不過在設定每個屬性值之前先呼叫InstantiationAwareBeanPostProcessor介面的postProcessPropertyValues()方法;
5. 呼叫Bean的屬性設定方法設定屬性值;
6. 如果Bean實現了org.springframework.beans.factory.BeanNameAware介面,將呼叫setBeanName()介面方法,將配置檔案中該Bean對應的名稱設定到Bean中;
7. 如果Bean實現了org.springframework.bean.factory.BeanFactoryAware介面,將呼叫setBeanFactory()介面方法,將BeanFactory容器例項設定到Bean中;
8. 如果BeanFactory裝配了org.springframework.beans.factory.config.BeanPostProcessor後處理器,將呼叫BeanPostProcessor的Object postProcessBeforeInstantiation(Object bean, Stringn beanName)
介面方法對Bean進行加工操作。其中入參bean是當前正在處理的Bean,而beanName是當前Bean的配置名,返回的物件為加工處理後的Bean。BeanPostProcessor在Spring框架中佔有重要的地位,為容器提供duiBean進行後續架構處理的切入點,Spring容器所提供的各種“神奇功能”(如AOP,動態代理等)都通過BeanPostProcessor實施;
9. 如果Bean實現了InitializingBean的介面,將呼叫該介面的afterPropertiesSet()方法;
10. 如果在通過init-method屬性定義了初始化方法,將執行這個方法;
11. BeanPostProcessor後處理定義了兩個方法:其一時postProcessBeforeInstantiation()在第8步呼叫;其二是Object postProcessAfterInstantiationn(Object bean, String beanName)方法,這個方法在此時呼叫,容器中再次獲得對Bean進行加工處理的機會。
12. 如果在中指定Bean的作用範圍是scope=”prototype”,將Bean返回給呼叫者,呼叫者負責Bean後續生命的管理,Spring不再管理這個Bean的生命週期。如果作用範圍設定為scope=“singleton”,則將Bean放入到Spring IoC容器的快取池中,並將Bean引用返回給呼叫者,Spring繼續對這些Bean進行後續的生命管理。
13. 對於scope=“singleton”的Bean,當容器關閉時,將觸發Spring對Bean的後續生命週期的管理工作,首先如果Bean實現了DisposableBean介面,則將呼叫介面的afterPropertiesSet()方法,可以在此編寫釋放資源、記錄日誌等操作。
14. 對於scope=“singleton”的Bean,如果通過的destroy-method屬性指定了Bean的銷燬方法,Spring將執行Bean的這個方法,完成Bean資源的釋放等操作。
Bean生命週期例項:
public class Car implements BeanFactoryAware, BeanNameAware, InitializingBean, DisposableBean {
private String brand;
private String color;
private int maxSpeed;
private BeanFactory beanFactory;
private String beanName;
public Car() {
System.out.println("呼叫Car()建構函式。");
}
public Car(String brand, String color, int maxSpeed) {
this.brand = brand;
this.color = color;
this.maxSpeed = maxSpeed;
}
public void introduce() {
System.out.println("brand:" + brand + ";color:" + color + ";maxSpeed:" + maxSpeed);
}
public String getBrand() {
return brand;
}
public void setBrand(String brand) {
System.out.println("呼叫setBrand()設定屬性。");
this.brand = brand;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
public int getMaxSpeed() {
return maxSpeed;
}
public void setMaxSpeed(int maxSpeed) {
this.maxSpeed = maxSpeed;
}
// 5 DisposableBean方法
@Override
public void destroy() throws Exception {
System.out.println("呼叫DisposaleBean.destroy()。");
}
// 4 IntializingBean介面方法
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("呼叫IntializingBean.afterPropertiesSet()。");
}
// 3 BeanNameAware介面方法
@Override
public void setBeanName(String name) {
System.out.println("呼叫BeanNameAware.setBeanName()。");
this.beanName = beanName;
}
// 2 BeanFactoryAware介面方法
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
System.out.println("呼叫BeanFactoryAware.setBeanFactory()。");
this.beanFactory = beanFactory;
}
// 6 通過<bean>的init-method屬性指定的初始化方法
public void myInit() {
System.out.println("呼叫init-method所指定的myInit(),將maxSpeed設定為240。");
this.maxSpeed = 240;
}
// 7 通過<bean>的destroy-method屬性指定的銷燬方法
public void myDestroy() {
System.out.println("呼叫destroy-method所指定的myDestroy()。");
}
}
public class MyInstantiationAwareBeanPostProcessor extends InstantiationAwareBeanPostProcessorAdapter {
// 1 在例項化Bean前進行呼叫
public Object postProcessBeforeInitialization(Class beanClass, String beanName) throws BeansException {
// 1-1 僅對容器中Car Bean進行處理
if ("car".equals(beanName)) {
System.out.println("InstantiationAware BeanPostProcessor.postProcessBeforeInstantiation");
}
return null;
}
// 2 在例項化Bean後呼叫
public boolean postProcessAfterInstantiation(Object bean, String beanName) {
// 2-1 僅對容器中Car Bean進行處理
if ("car".equals(beanName)) {
System.out.println("InstantiationAware BeanPostProcessor.postProcessAfterInstantiation");
}
return true;
}
// 3 在設定某個屬性時呼叫
public PropertyValues postProcessPropertyValues(PropertyValues pvs, PropertyDescriptor[] pds, Object bean,
String beanName) throws BeansException {
// 3-1 僅對容器中Car Bean進行處理,還可以通過pdst入參進行過濾,僅對car的某個特定屬性時進行處理
if("car".equals(beanName)) {
System.out.println("InstantiationAware AwareBeanPostProcessor.postProcessPropertyValues");
}
return pvs;
}
}
public class MyBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if(beanName.equals("car")) {
Car car = (Car)bean;
if(car.getColor() == null) {
System.out.println("呼叫BeanPostProcessor.postProcessBeforeInitialization(),color為空,設定為預設黑色。");
car.setColor("黑色");
}
}
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if(beanName.equals("car")) {
Car car = (Car)bean;
if(car.getMaxSpeed() >= 200) {
System.out.println("呼叫BeanPostProcessor.postProcess AfterInitialization(), 將maxSpeed調整為200。");
car.setMaxSpeed(200);
}
}
return bean;
}
}
**beans.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" xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
<bean id="car" class="com.hhxs.bbt.web.Car"
init-method="myInit"
destroy-method="myDestroy"
p:brand="紅旗CA72"
p:maxSpeed="200"
scope="singleton" />
</beans>
public class BeanLifeCycle {
private static void lifeCycleInBeanFactory() {
Resource res = new ClassPathResource("spring/beans.xml");
BeanFactory bf = new XmlBeanFactory(res);
// 向容器中註冊後處理器
((ConfigurableBeanFactory)bf).addBeanPostProcessor(new MyBeanPostProcessor());
((ConfigurableBeanFactory)bf).addBeanPostProcessor(new MyInstantiationAwareBeanPostProcessor());
// 第一次從容器中獲取Car,將觸發容器例項化該Bean,這將引發Bean生命週期方法的呼叫。
Car car1 = (Car)bf.getBean("car");
car1.introduce();
car1.setColor("紅色");
car1.introduce();
// 第二次從容器中獲取Car,直接從快取池中獲取
Car car2 = (Car)bf.getBean("car");
// 檢視car1和car2是否指向同一引用
System.out.println("car1==car2:" + (car1==car2));
// 關閉容器
((XmlBeanFactory)bf).destroySingletons();
}
public static void main(String[] args) {
lifeCycleInBeanFactory();
}
}
執行上述程式碼,我們在控制檯上得到以下輸出資訊,仔細觀察,將發現它驗證了我們前面所介紹的Bean生命週期過程。
呼叫Car()建構函式。
InstantiationAware BeanPostProcessor.postProcessAfterInstantiation
InstantiationAware AwareBeanPostProcessor.postProcessPropertyValues
呼叫setBrand()設定屬性。
呼叫BeanNameAware.setBeanName()。
呼叫BeanFactoryAware.setBeanFactory()。
呼叫BeanPostProcessor.postProcessBeforeInitialization(),color為空,設定為預設黑色。
呼叫IntializingBean.afterPropertiesSet()。
呼叫init-method所指定的myInit(),將maxSpeed設定為240。
呼叫BeanPostProcessor.postProcess AfterInitialization(), 將maxSpeed調整為200。
brand:紅旗CA72;color:黑色;maxSpeed:200
brand:紅旗CA72;color:紅色;maxSpeed:200
car1==car2:true
呼叫DisposaleBean.destroy()。
呼叫destroy-method所指定的myDestroy()。
4.2 ApplicationContext中Bean的生命週期
Bean在應用上下文中的生命週期和BeanFactory中生命週期類似,不同的是,如果Bean實現org.springframework.context.ApplicationContextAware介面,會增加一個呼叫該介面方法setApplicationContext()的步驟。
此外,如果配置檔案中聲明瞭工作後處理器介面BeanFactoryPostProcessor的實現類,則應用上下文在裝載配置檔案之後初始化Bean例項之前將呼叫這些BeanFactoryPostProcessor對配置資訊進行加工處理。工廠後處理器是容器級的,僅在應用上下文初始化時呼叫一次,其目的是完成一些配置檔案的加工處理工作。
ApplicationContext和BeanFactory另一最大的不同之處在於:前者會利用Java反射機制自動識別出配置檔案中定義的BeanPostProcessor、InstantiationAwareBeanPostProcessor和BeanFactoryPostProcessor,並自動將它們註冊到應用上下文中;而後者需要在程式碼中拿你給通過手工呼叫addBeanPostProcessor()方法進行註冊。這也是為什麼在應用開發時,我們普遍使用ApplicationContext而很少使用BeanFactory的原因之一。