Spring Ioc (反射) 精華一頁紙

weixin_33727510發表於2017-04-11

反射是Java實現模組化的一個非常基礎的功能,通過載入類的位元組碼,然後動態的在記憶體中生成物件。也是深入Java 研究的第一個高階主題。關於載入器和位元組碼部分的內容,可以參見本博的 《java Class和載入機制精華一頁紙》

Spring 框架基礎的Ioc就是採用了反射的功能,實現了框架。

1、反射

I、反射操作經典步驟

一、獲取 Class物件

a、最常用的就是 Class.forName(className)

b、如果知道類名字,直接通過類獲取 String.class

c、如果已有一個物件 object.getClass

二、獲取 Method物件

a、通過Class物件的getdeclaredMethods 獲取所有方法

b、通過名字和引數型別列表,獲取具體的方法getdeclaredMethod

三、例項化該Class的物件

Class.newInstance

四、呼叫方法

Method.invoke(newobject,new Object[]{parmalist}

II、反射的作用

反射是實現抽象的一個基礎設施。單個應用內的模組化和解耦, 大家都比較熟悉, 比如 面向介面程式設計, 工廠模式等等。

iterface a = Factory.create;

在Factory 裡面,我們是知道這個具體的實現類的。

但如果是應用模組之間呢, 不同人或者團隊開發的, 商量好名字? 如果 名字改變後呢?

這樣耦合性太強, 每次修改都會要帶來程式碼重新修改和編譯。

反射正是可以解決這個問題的工具。靜態編譯時, 並不需要知道具體的名字;在載入時, 通過傳入名稱引數, 獲取到這個類

比如, 配置檔案中配置了 具體實現類的名字, 只要在一個ClassPath下,就可以載入到具體的實現類。

Class c = Class.forName( param ); // 此處param可以是載入檔案\其他應用傳入的引數等等

iterface a = c.newInstance();

這個解耦套路,就是 傳統的框架 套路

2、傳統模組間解耦框架 - 依賴查詢(DL)

依賴查詢, 有個最經典的例子就是 JNDI , JavaEE 就是通過這個實現模組間物件的訪問, 比如EJB, 下面是 tomcat下一個依賴查詢的例子

I、context or server 配置檔案

type="javax.sql.DataSource" auth="Container"

driverClassName="com.mysql.jdbc.Driver"

maxActive="4000"

maxIdle="60"

maxWait="15000"

url="jdbc:mysql://localhost:3306/mysql?useUnicode=true&characterEncoding=UTF-8"

username="root"

password="root"/>

II、程式碼中依賴查詢

Context ctx = new InitialContext();

DataSource ds = (DataSource) ctx.lookup("java:comp/env/jdbc/DefaultDS");

III、依賴查詢的問題

依賴查詢的關鍵問題是 對程式碼侵入性強, 帶來的結果就是 模組整合、單元測試等等工作很難操作, 比如測試一個EJB呼叫的程式碼, 必須要有完整的 Web框架, 要配置好基礎設施;而 這段程式碼只是要測試自己的邏輯和介面。

3、輕量級模組間解耦框架 - 依賴注入(DI)/控制反轉(Ioc)

這兩個概念自從Spring橫空出世以後, 一直抄的非常火熱。先解釋一下兩個名詞

依賴注入:是從應用角度出發, 需要的物件是從 外面注入進來的, 屬於被動接受物件;而不像傳統的 依賴查詢, 主動的去查詢物件。

控制反轉: 是從框架和容器的角度出發, 建立物件的工作, 由應用 讓渡給 容器來完成, 物件間的依賴, 也都由容器完成。

依賴注入/控制反轉,看起來很神奇, 其實,如果遵循 開發的幾大原則, 面向介面、職責單一、介面隔離、開放封閉等(可以參照本博《設計模式 精華一頁紙》),就會發現, 這是一種比較自然和優雅的架構設計。

傳統的依賴查詢,雖然解開了模組間的耦合,但他違背了職責單一的要求,對於 應用而言, 只需要瞭解和呼叫 介面中的方法, 而查詢這個工作不應該放在應用中。所以,可以對查詢這個過程進行封裝。

Object o = Lookup.get(xxx);

-- 這裡的 Lookup 封裝了物件的查詢過程

再進一步封裝和解耦,查詢物件的過程對應用徹底遮蔽隔離、在應用的程式碼中不再出現 查詢的程式碼。要完成這個工作

a、 首先,查詢獲取的物件 要設定到 使用該物件的目標物件的應用程式碼中, 也就是所謂的 注入工作

b、 其次,要完成注入工作,要麼 把目標物件的引用傳遞給框架, 要麼目標物件本身就是框架建立的

c、 從解耦、隔離的角度看, 框架建立管理物件更符合要求。

框架管理物件的生命週期、提供物件的注入工作。

......

Spring Ioc 框架就是在這個基礎上產生了。

4、Spring Ioc 框架

從上面的討論, 可以瞭解, 物件都交由框架管理和構造, 所以、首先要有物件的管理容器;其次要有注入的介面,實現裝配工作。

I、Bean 工廠/容器

某種角度上,Spring Ioc就是一個物件容器, 依賴注入這些只是提供的功能而已

public interface BeanFactory{

Object getBean(String name) throws BeansException

Object getBean(String name, Class requiredType)throws BeansException

boolean containsBean(String name)

boolean isSingleton(String name)throws NoSuchBeanDefinitionException

String[] getAliases(String name)throws NoSuchBeanDefinitionException

}

四級介面

BeanFactory作為最基礎的介面,只提供了基本功能。

秉著 介面隔離的設計原則, 從BeanFactory開始的繼承體系

二級介面 AutowireCapableBeanFactory ListableBeanFactory HierarchicalBeanFactory

分別對應 自動裝配 Bean工廠 : 作用是不在Spring(主要是 ApplicationContext)中管理的物件, 如果在應用中用到了,Spring 無法注入,比如如果用到Tomcat已存在的物件,通過這個工廠把 這些物件引入並注入應用物件。

迭代Bean的 Bean工廠 : 提供對容器中的Bean訪問功能

訪問父介面的 Bean工廠 : 提供對父容器的訪問功能

三級介面 ConfigurableBeanFactory :疊加配置功能(是否單例、範圍、Bean依賴等等)

四級介面 ConfigurableListBeanFactory : 大合集功能的 介面, 繼承之前面的介面

第一個預設的實現類 DefaultListableBeanFactory

一個比較有意思的問題: BeanFactory 和 FactoryBean 的區別?

這其實是兩個完全不同層次的內容

BeanFactory 是 Ioc 容器的介面,管理Bean的核心介面

FactoryBean 則是 適配 第三方應用的一個介面, 提供了對第三方Bean的適配, 以便更好的整合到Spring中來

通過工廠Bean,應用不需要自己寫適配類去裝配其他應用

org.springframework.jndi.JndiObjectFactoryBean -- 提供JNDI查詢的物件

org.springframework.orm.hibernate.LocalSessionFactoryBean -- 提供Hibernate SessionFactory

org.springframework.orm.jdo.LocalPersistenceManagerFactoryBean -- 提供JDO PersistenceManagerFactory的

org.springframework.aop.framework.ProxyFactoryBean -- 獲取AOP的動態代理,實現AOP切面功能

org.springframework.transaction.interceptor.TransactionProxyFactoryBean -- 建立事務代理

org.springframework.ejb.access.LocalStatelessSessionProxyFactoryBean -- EJB業務介面

...

org.springframework.remoting.caucho.HessianProxyFactoryBean -- Hessian 遠端協議的代理

org.springframework.remoting.caucho.BurlapProxyFactoryBean -- Burlap遠端協議的代理

II、Bean的生命週期

容器託管了 Bean的建立, 所以容器需要負責管理 Bean的生命週期。

a、生命週期

例項化 -> 設值注入 -> 設定Bean ID -> 設定工廠 -> 設定上下文 -> 初始化(開始\初始化\結束)

正常構建Bean的這些過程, 不需要應用介入。如果有特殊需要介入的地方。Spring開放了二次介面。

如果需要在構造物件的時候提供 初始化和 銷燬時 額外處理的能力

方法一:Spring提供了回撥介面 BeanNameAware| ApplicationContextAware | BeanPostProcessor | InitializingBean | BeanPostProcessor | DisposableBean 等等對應不同的構造階段二次介面

org.springframework.beans.factory.InitializingBean 該介面提供了物件構造後 afterProperiesSet() throws Exception 方法

org.springframework.beans.fatory.Disposable 該介面提供了一個物件銷燬後呼叫的 destory() throws Exception 方法

@PostConstruct 註解 | @PreDestory 初始化呼叫和銷燬呼叫

方法二:Spring 可以指定屬性配置

這樣,在引入第三方元件時,可以不用依賴Spring容器,第三方元件不需要修改程式碼,或者為Spring寫介面卡

也可以配置全域性的 init-method/destroy-method 方法

方法三:Spring提供的Bean工廠介面,Bean實現該介面,可以獲取Bean工廠的引用,可以獲取對其他Bean的引用,實現生命週期干預

org.springframework.beans.factory.BeanFactoryAware 該介面提供一個 setBeanFactory(BeanFactory beanFactory) throws BeanException

如何選擇?

如果希望解耦Spring 框架, 則可以使用 方法二 指定屬性, 這樣配置方法干預初始化和銷燬;否則建議使用 註解

b、作用域

singlton - 一個Spring容器對應一個 物件

prototype - 每獲取一個物件

request | session | gloabl - Web應用的作用域,每作用域一個物件

預設是 singlton 作用域

Web應用 DispatchServlet 會預設管理作用域,預設是request

c、建立和銷燬

何時被建立?

預設是隨容器啟動建立

可以配置為 lazy-init="true" 獲取時建立

何時被銷燬?

singlton, 在容器關閉時銷燬,平時一直駐留

prototype 銷燬由應用管理

- 因為只有 singlton的 物件才會進入 Bean容器工廠的ConcurrentHashMap 快取。這也是為什麼 prototype 型別的物件, 無法進行銷燬回撥, 因為物件的控制權交給了應用

III、 應用上下文(org.springframework.context.ApplicationContext)

工廠介面提供Bean管理的核心功能, 如果要把這個工廠應用到具體專案中, 還需要很多基礎設施, ApplicationContext就是這個功能合集。

a、繼承了Bean工廠的功能,繼承了 ListableBeanFactory | HierarchicalBeanFactory

b、提供資源的管理,主要是載入各種配置檔案

c、國際化資訊,主要是各種資訊的國際化

通過委託給代理類 ResourceBundleMessageSource實現國際化

d、提供事件管理

繼承自Java自帶的事件分發

事件ApplicationEvent -> 繼承 EventObject

監聽者 ApplicationListener -> 繼承 EventListener

提供了 ApplicationEventPublisher 事件管理器(分發)

具體參見本博 《java 觀察者、事件機制 到Spring 事件分發機制》

e、lifycycle 生命週期管理

容器的生命週期管理提供 Lifecycle 介面, 提供給任何實現 該介面的Bean, 通過LifecycleProcessor 執行回撥介面, 可以和容器的生命週期管理同步。

提供 start | stop | isRunning | onRefresh 等回撥介面

常用的容器實現物件

ClassPathXmlApplicationContext

FileSystemXmlApplicationContext

XmlWebApplicationContext

5、Spring Ioc例項

I、基本使用 設值和構造子

undefinedundefined

undefinedundefined

設值是通過 setter 方式注入;構造子按照順序注入

II、集合裝配

子節點有 (可巢狀)

成員有

成員比較簡單,就是

value a

value b

III、工廠裝配

-- 靜態工廠 static

-- 動態工廠 new

IV、SPEL表示式

#{xxx} 其實是一種佔位替換表示式語法, 類似的有很多比如 Freemarker 的${}, angular JS的 {{}}, 支援對記憶體物件的訪問和簡單表示式操作, 這些語法也很類似

常量 #{xx} 等同於 xx 常量一般直接用的很少

引用 #{xxx.xxx} -- 屬性 #{xxx.getxxx()} -- 方法

靜態屬性 #{T(ClassXXX).xxx}

各種運算(算術|邏輯|正則) #{1+2} #{a == b && b == c}

V、自動裝配

byName -- 根據Bean名稱和屬性名稱進行匹配 缺點是名稱要一致,如果多個名稱類似,就要避開重複

byType -- 根據Bean型別和屬性型別進行裝配 缺點是不能存在相同型別的多個bean(解決方法,首選bean,排除其他bean)

constructor -- 把具有相同型別的 type 構造到屬性中

autodetect -- 首先嚐試 constuctor 裝配,失敗採用 byType

指定單個Bean autowire="byName"

指定全域性 default-autowire

開啟自動裝配

VI、註解

a、注入

@Autowired 實現 構造和設定注入

@Qualifier("guitar") 指定Bean注入,甚至可以自定義 註解

@Inject -- 使用JCP的Inject註解

b、bean定義

@Component -- 通用構造性註解

@Controller -- Spring MVC Controller

@Repository -- 標記為資料倉儲

@Service -- 標記為服務

經過測試發現,XML手工配置的 注入,會覆蓋 註解注入的值,應該Spring的順序最後是手工

c、用Spring配置類來替代注入的工作

// 定義全域性檔案的 Beans 測試的時候發現,SpringConfig 類,Spring使用了CGlib(asm) 技術重新處理了位元組碼

// 主要原因是,Spring 並不是直接 呼叫方法返回物件的,比如如下 duke() 方法,Spring會攔截,針對單例的情況

// Spring 會從自己的上下文返回一個已經存在的物件

@Configuration

public class SpringConfig {

// 定義一個名為 duke 的Bean

@Bean

public Performer duke(){

return new Juggler();

}

@Bean

public Instrument guitar(){

return new Guitar(0);

}

@Bean

public Performer kenny(){

Instrumentalist kenny = new Instrumentalist();

kenny.setSong("aaa");

kenny.setInstrument(guitar());

return kenny;

}

}

使用 Java 配置的問題是,SpringConfig 就相當於facade 門面的實現,使用了 Spring的 Context 來管理物件的生命週期。這種方式,物件間的依賴關係還是硬編碼到了程式碼中。

相關文章