反射是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 來管理對象的生命周期。這種方式,對象間的依賴關系還是硬編碼到了代碼中。