依賴注入與JSR-330的參考實現——Guice
原文出處: wwsun.me
依賴注入(控制反轉的一種形式),它是Java開發主流中一個重要的編程范式(思維方式)。簡單的說,使用DI技術可以讓對象從別處獲得依賴項,而不是由它自己來構造。使用DI有很多好處,它能降低代碼之間的耦合度,讓代碼更易于測試、更易讀。Java DI的官方標準是JSR-330,本文我們還會介紹JSR-330的參考實現Guice 3,一個輕量級 的DI框架。
IoC and DI
使用IoC范式編程時,程序邏輯的流程通常是由一個功能中心來控制的。而使用IoC,這個“中心控制”的設計原則會被反轉過來。調用者的代碼處理程序的執行順序,而程序邏輯則被封裝在接受調用的子流程中。通過一個例子來理解IoC:
在GUI應用中,GUI框架負責控制調用事件處理器,而不是應用邏輯。當用戶點擊了一個動作,比如“向前”,GUI框架會自動調用 對應的事件處理器,而應用邏輯可以把重點放在處理動作上。程序的控制被反轉了,將控制權由應用邏輯轉移到了GUI框架。
IoC也被稱為好萊塢原則:會有另一段代碼擁有最初的控制線程,并且由它來調用你的代碼,而不是由你的代碼調用它。
不要給我們打電話,我們會打給你。——好萊塢原則
IoC有多種不同的實現,包括工廠模式、服務器定位模式,當然還有依賴注入。需要注意的是,DI并不等于IoC,DI只是IoC的一種實現方式,IoC是一種機制。
DI是IoC的一種特定形態,是指尋找依賴項的過程不在當前執行代碼的直接控制之下。Java中為DI提供的容器有Guice、Spring、PicoContainer等。DI的好處有:松耦合、易測試、強內聚、可重用、更輕盈的代碼。
Example
下面編寫代碼來解釋到底什么是DI,如何使用DI。我們首先編寫傳統的代碼,然后使用工廠模式解耦,進而再使用DI來改進代碼,通過這個過程,你將了解到DI的精妙之處。這些改進都基于同一個關鍵技術,即面向接口編程。
假設你想找到所有對Java可開發人員比較友善的好萊塢經紀人。首先,我們有了一個AgentFinder接口,及其兩個實現類SpreadSheetAgentFinder和WebServiceAgentFinder。AgentFinder接口如下:
public interface AgentFinder { public List<Agent> findAllAgents(); }
傳統方式尋找友善經紀人
為了找到經紀人,項目中有個默認的HollywoodService類,它會從SpreadSheetAgentFinder里得到一個經紀人列表,并且過濾出友善的經紀人,最終返回該列表。
public class HollywoodService { public static List<Agent> getFriendlyAgents() { AgentFinder finder = new SpreadsheetAgentFinder(); List<Agent> agents = finder.findAllAgents(); List<Agent> friendlyAgents = filterAgents(agents,"Java Developers");return friendlyAgents; } //filterAgents
}</pre>
這是最傳統的編碼方式,顯然,HollywoodService被SpreadsheetAgentFinder這個AgentFinder的具體實現死死的綁定住了。
為了改進這個問題,通常我們會想到一個常用的設計模式——工廠模式。工廠模式可以一定程度上解耦程序,它也是IoC的一種實現方式。
工廠模式
利用工廠模式(其實這里用到的是一個簡單工廠)重新編寫上面的代碼,如下:
public List<Agent> getFriendlyAgents(String agentFinderType) { AgentFinderFactory factory = AgentFinderFactory.getInstance(); AgentFinder finder = factory.getAgentFinder(agentFinderType); List<Agent> agents = finder.findAllAgents(); return filterAgents(agents, "Java Developers"); }如你所見,這里不再有具體的實現來黏住你,而是通過注入agentFinderType的方式讓你選擇想要的AgentFinder。但這里仍然還有問題:
- 代碼注入的僅僅是一個引用憑據(agentFinderType),而不是真正實現AgentFinder的對象
- 方法getFriendlyAgents中還有獲取其依賴的代碼,達不到只關注自身智能的理想狀態
我們需要通過DI來達成這兩個目標。
手工實現DI
public static List<Agent> getFriendlyAgents(AgentFinder finder){ List<Agent> agents = finder.findAllAgents(); return filterAgents(agents,"Java Developers"); }上面的代碼是一個純手工打造的DI方案,AgentFinder被直接注入到getFriendlyAgents方法中。現在這個getFriendlyAgents方法干凈利落,只專注于純業務邏輯。
但是,這種手工方式的DI顯然存在問題,如何配置AgentFinder具體實現的問題并沒有解決,原本AgentFinderFactory要做的工作還是要找一個地方去做。解決這個問題的方式是借助DI框架,而DI框架就是把你的代碼打包起來的運行時環境,在你需要的時候注入依賴項。
使用Guice
Java中DI方面的標準規范是JSR-330,它提供了統一的DI通用功能的標準,而且提供了你需要了解的幕后規則及限制。這里不具體介紹JSR-330,有興趣的可以查找相關資料閱讀。我們重點介紹JSR-330的參考實現Guice,它是一個由Google實現的針對Java 6以上版本的流行的、輕量級的DI框架。
下面我們使用Guice來解決手工方式實現DI的不足問題:
首先我們需要創建一個定義綁定關系的AgentFinderModule模塊(配置類)。
public class AgentFinderModule extends AbstractModule{ @Override protected void configure() { bind(AgentFinder.class).to(WebServiceAgentFinder.class); } }綁定關系的確立在調用Guice的bind方法時發生,把要綁定的類(AgentFinder)傳給它,然后調用to方法指明要注入到哪個實現類。
下面我們來Guice版本的HollywoodService:
public class HollywoodServiceGuice { private AgentFinder finder = null; @Inject public HollywoodServiceGuice(AgentFinder finder){ this.finder = finder; } public List<Agent> getFriendlyAgents(){ List<Agent> agents = finder.findAllAgents(); return filterAgents(agents, "Java Developers"); } //... }我們在模塊中聲明了綁定關系,下面我們就可以讓注入器構建對象關系圖了。接下來我們看看在獨立Java程序和Web應用程序這兩種情況下分別要如何實現:
1.構建Guice對象關系圖——獨立Java程序
public class HollywoodServiceClient { public static void main(String[] args) { Injector injector = Guice.createInjector(new AgentFinderModule()); HollywoodServiceGuice hollywoodServiceGuice = injector.getInstance(HollywoodServiceGuice.class); List<Agent> agents = hollywoodServiceGuice.getFriendlyAgents(); } }2.構建Guice對象關系圖——Web應用程序 把Gucie-servlet.jar加到web應用的類庫中,然后再web.xml中添加下面的配置項:
<filter> <filter-name>guiceFilter</filter-name> <filter-class>com.google.inject.servlet.GuiceFilter</filter-class> </filter> <filter-mapping> <filter-name>guiceFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>3.然后是標準動作,擴展ServletContextListener以便使用Guice的ServeltModule:
public class MyGuiceServletConfig extends GuiceServletContextListener { @Override protected Injector getInjector() { return Guice.createInjector(new ServletModule()); } }4.最好,將其加入到web.xml文件中,以便servlet容器在部署應用時觸發該類:
<listener> <listener-class>com.example.MyGuiceServletConfig</listener-class> </listener>經由注入器創建HollywoodServiceGuice,你得到一個配置完備的類,馬上就可以調用其中的getFriendlyAgents方法了。
關于JSR-330和Guice的更多用法,請參考其Github頁面。
References
- https://github.com/google/guice/wiki/GettingStarted
- The Well-Grounded Java Developer, chapter 3, Dependency Injection