隨着軟件結構的日益龐大,軟件模塊化趨勢出現,軟件開發也須要多人合做,隨即分工出現。如何劃分模塊,如何定義接口方便分工成爲軟件工程設計中愈來愈關注的問題。良好的模塊化具備如下優點:可擴展、易驗證、易維護、易分工、易理解、代碼複用。java
優良的模塊設計每每遵照「低耦合高內聚」的原則。而「框架」是對開發中良好設計的總結,把設計中常用的代碼獨立出來,所造成的一種軟件工具。用戶遵照它的開發規則,就能夠實現良好的模塊化,避免軟件開發中潛在的問題。廣義上的框架無處再也不,一個常見的例子就是PC硬件體系結構,人們只要按照各自須要的主板、顯卡、內存等器件就能夠任意組裝成本身想要的電腦。而作主板的廠商不用關心作顯卡廠商的怎麼實現它的功能。軟件框架也是如此,開發人員只要在Spring框架中填充本身的業務邏輯就能完成一個模塊劃分清晰紛的系統。程序員
這裏主要經過一個銀行通知用戶月收支記錄的小例子來介紹輕型J2EE框架Spring的主要內容、它所解決的問題和實現的方法。spring
Spring框架主要能夠分爲3個核心內容:編程
容器設計模式
控制反轉(IoC ,Inversion of Control)安全
面向切面編程(AOP ,Aspect-Oriented Programming)微信
例子中依次對這些特性進行介紹,描述了軟件模塊化後存在的依賴與問題,以及Spring框架如何解決這些問題。 架構
假設有一個以下應用場景:(1)一個銀行在每個月的月初都須要向客戶發送上個月的帳單,帳單發送的方式能夠爲紙質郵寄、或者短信方式。(2)還有一個潛在的需求:爲了安全起見,在每一個函數操做過程當中都須要記錄日誌,記錄參數傳入是否正常,函數是否正常結束,以便出錯時系統管理員查帳。app
那麼對這個需求進行簡單實現。系統框圖以下所示:框架
首先定義一個帳單輸出的接口:
//接口
public interface ReportGenerator{
public void generate(String[][] table) ;
}
實現「打印紙質帳單」與「發送短信」兩個具體功能:
//帳單報表實現類
public class PageReportGenerator implement ReportGenerator {
public void generate(String[][] table) {
log4j.info( ... ); //輸出日誌
...打印操做,以便工做人員郵遞給客戶
log4j.info( ... ); //輸出日誌
}
}
//短信報表實現類
public class SMSReportGenerator implement ReportGenerator {
public void generate(String[][] table) {
log4j.info( ... );
...短信發送操做
log4j.info( ... );
}
}
上層業務邏輯對上個月的帳目進行統計並調用接口產生紙質或者短信結果:
//上層業務中的服務類
public class ReportService{
private ReportGenerator reportGenerator = new SMSReportGenerator();
public void generateMonthlyReport(int year, int month) {
log4j.info( ... );
String[][] statistics = null ;
...
reportGenerator.generate(statistics);
}
}
這個實現源代碼請查看文章結尾附錄中的"BankOld"。源代碼中與例子中程序略有區別:因爲使用log4j須要引用外部的包,而且須要寫配置文件,爲了方便,源代碼中的日誌輸出用system.out.println()代替。
A、模塊化後出現的問題與隱患
假設隨着工程的複雜化,上面的例子須要分紅兩個模塊,以便開發時分工,通常會以以下結構劃分:
劃分後再看原來的代碼:
//上層業務中的服務類
public class ReportService{
private ReportGenerator reportGenerator = new SMSReportGenerator(); //隱患
public void generateMonthlyReport(int year, int month) {
...
}
}
在服務類有private ReportGenerator reportGenerator = new SMSReportGenerator();這麼一行代碼,ReportService類與SMSReportGenerator類不屬於同一個模塊,當開發人員B對內部實現進行修改時,因爲存在依賴,開發人員A也要進行修改(好比以前喜歡短信收帳單的客戶感受短信不夠詳細,但願之後改用郵件收帳單,那麼開發人員B須要實現一個MailReportGenerator類,在開發人員B修改代碼時,開發人員A也須要改代碼------聲明部分修改)。若是系統龐大new
SMSReportGenerator()大量使用的話,修改就會十分複雜,一個聲明沒有修改就會出現大的BUG。
因此須要一種劃分,讓各個模塊儘量獨立,當開發人員B修改本身的模塊時,開發人員A不須要修改任何代碼。
B、問題出現的緣由
爲例子中的程序畫一個UML依賴圖:
能夠發現上述問題出現的緣由主要是:模塊A與模塊B不但存在接口依賴,還存在實現依賴。ReportGenerator每次修改它的實現,都會對ReportService產生影響。那麼須要重構消除這種實現依賴。
C、用容器解決問題
消除實現依賴通常能夠經過添加一個容器類來解決。在例子程序容器代碼以下:
//容器類
public class Container {
public static Container instance;
private Map<String, Object> components;
public Container(){
component = new HashMap<String, Object>();
instance = this;
ReportGenertor reportGenertor = new SMSReportGenertor();
components.put(「reportGenertor」, reportGenertor);
ReportService reportService = new ReportService();
components.put(「reportService」, reportService);
}
public Object getComponent(String id){
return components.get(id);
}
}
使用容器後,模塊A的ReportService的屬性實現方法也發生了變化。
//服務類變動,下降了耦合
public class ReportService{
//private ReportGenerator reportGenerator = new SMSReportGenerator();
private ReportGenerator reportGenerator = (ReportGenerator) Container.instance.getComponent(「reportGenerator」);
public void generateMonthlyReport(int year, int month) {
...
}
}
這樣的話,class都在容器中實現,使用者只須要在容器中查找須要的實例,開發人員修改模塊B後(在模塊中增長郵件報表生成類MailReportGenerator),只須要在容器類中修改聲明(把ReportGenertor
reportGenertor = new SMSReportGenertor();改成ReportGenertor reportGenertor = new
MailReportGenertor();)便可,模塊A不須要修改任何代碼。必定程度上下降了模塊之間的耦合。
A、還存在的耦合
使用容器後模塊A與模塊B之間的耦合減小了,可是經過UML依賴圖能夠看出模塊A開始依賴於容器類:
以前的模塊A對模塊B的實現依賴經過容器進行傳遞,在程序中用(ReportGenerator) Container.instance.getComponent(「reportGenerator」)的方法取得容器中SMSReportGenertor的實例,這種用字符(「reportGenerator」)指代具體實現類SMSReportGenertor 的方式並無徹底的解決耦合。因此在銀行帳單的例子中咱們須要消除ReportService對容器Container的依賴。
B、控制反轉與依賴注入
在咱們常規的思惟中,ReportService須要初始化它的屬性private ReportGenerator reportGenerator就必須進行主動搜索須要的外部資源。不使用容器時,它須要找到SMSReportGenertor()的構造函數;當使用容器時須要知道SMSReportGenertor實例在容器中的命名。不管怎麼封裝,這種主動查找外部資源的行爲都必須知道如何得到資源,也就是確定存在一種或強或弱的依賴。那是否存在一種方式,讓ReportService再也不主動初始化reportGenerator,被動的接受推送的資源?
這種反轉資源獲取方向的思想被稱爲控制反轉(IoC,Inversion of Control),使用控制反轉後,容器主動地將資源推送給須要資源的類(或稱爲bean)ReportService,而ReportService須要作的只是用一種合適的方式接受資源。控制反轉的具體實現過程用到了依賴注入(DI,Dependecncy Injection)的設計模式,ReportService類接受資源的方式有多種,其中一種就是在類中定義一個setter方法,讓容器將匹配的資源注入:setter的寫法以下:
//爲須要依賴注入的類加入一種被稱爲setter的方法
public class ReportService{
/*private ReportGenerator reportGenerator =
(ReportGenerator) Container.instance.getComponent(「reportGenerator」); */
private ReportGenerator reportGenerator;
public void setReportGenerator( ReportGenerator reportGenerator) {
this.reportGenerator = reportGenerator;
}
public void generateMonthlyReport(int year, int month) {
...
}
}
在容器中把依賴注入:
//容器類
public class Container {
...
public Container ( ) {
component = new HashMap<String, Object>();
instance = this;
ReportGenertor reportGenertor = new SMSReportGenertor();
components.put(「reportGenertor」, reportGenertor);
ReportService reportService = new ReportService();
reportService.setReportGenerator(reportGenerator); //使用ReportService的setter方法注入依賴關係
components.put(「reportService」, reportService);
}
...
}
這樣一來ReportService就不用管SMSReportGenertor在容器中是什麼名字,模塊A對於模塊B只有接口依賴,作到了鬆耦合。
C、Spring IoC容器的XML配置
每一個使用Spring框架的工程都會用到容器與控制反轉,爲了代碼複用,Spring把通用的代碼獨立出來造成了本身的IoC容器供開發者使用:
與上面例子中實現的容器相比,Spring框架提供的IoC容器要遠遠複雜的多,但用戶不用關心Spring
IoC容器的代碼實現,Spring提供了一種簡便的bean依賴關係配置方式------使用XML文件,在上面的例子中,配置依賴關係只要在工程根目錄下的「application.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="smsReportGenerator" class="bank.SMSReportGenerator" />
<bean id="reportService" class="bank.ReportService">
<property name="reportGenerator" ref="smsReportGenerator" />
</bean>
</beans>
<?xml version="1.0" encoding="UTF-8"?>是標準的XML頭,xmlns引用的是一些命名空間,兩個通常在工程中自動生成。後面的內容由用戶輸入,主要表示實例化SMSReportGenerator,實例化ReportService並把SMSReportGenerator的對象smsReportGenerator賦值給ReportService的屬性reportGenerator,完成依賴注入。
A、日誌問題以及延伸
在例子的需求中有一條是:須要記錄日誌,以便出錯時系統管理員查帳。回顧例子中的代碼,在每一個方法中都加了日誌操做:
//服務類
public class ReportService{
...
public void generateMonthlyReport(int year, int month) {
log4j.info( ... ); //記錄函數的初始情況參數等信息
String[ ][ ] statistics = null ;
...
reportGenerator.generate(statistics);
log4j.info( ... ); //記錄函數的執行情況與返回值
}
}
//憑條報表實現類
public class PageReportGenerator implement ReportGenerator {
public void generate(String[ ][ ] table) {
log4j.info( ... ); //記錄函數的初始情況參數等信息
…打印操做
log4j.info( ... ); //記錄函數的執行情況與返回值
}
}
能夠看出在每一個方法的開始與結尾都調用了日誌輸出,這種零散的日誌操做存在着一些隱患,會致使維護的困難。好比日誌輸出的格式發送了變化,那麼不管模塊A仍是模塊B的程序員都要對每一個方法每一個輸出逐條修改,極容易遺漏,形成日誌輸出風格的不一致。又好比不用Log4j日誌輸出工具更換其餘工具,若是遺漏一個將會出現嚴重BUG。
與日誌輸出類似的問題在編程中常常遇到,這種跨越好幾個模塊的功能和需求被稱爲橫切關注點,典型的有日誌、驗證、事務管理等。
橫切關注點容易致使代碼混亂、代碼分散的問題。而如何將很切關注點模塊化是本節的重點。
B、代理模式
傳統的面向對象方法很難實現很切關注點的模塊化。通常的實現方式是使用設計模式中的代理模式。代理模式的原理是使用一個代理將對象包裝起來,這個代理對象就取代了原有對象,任何對原對象的調用都首先通過代理,代理能夠完成一些額外的任務,因此代理模式可以實現橫切關注點。
可能在有些程序中有不少橫切關注點,那麼只須要在代理外再加幾層代理便可。以銀行帳單爲例介紹一個種用Java Reflection API動態代理實現的橫切關注點模塊化方法。系統提供了一個InvocationHandler接口:
//系統提供的代理接口
public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args) throw Throwable;
}
咱們須要實現這個接口來建立一個日誌代理,實現代碼以下:
//日誌代理實現
public class LogHandler implement InvocationHandler{
private Object target;
public LogHandler(Object target){
this.target = target;
}
public Object invoke(Object proxy, Method method, Object[] args ) throw Throwable{
//記錄函數的初始情況參數等信息
log4j.info(「開始:方法」+ method.getName() + 「參數」+Arrays.toString(args) );
Object result = method.invoke(target, args);
//記錄函數的執行情況與返回值
log4j.info(「結束:方法」+ method.getName() + 「返回值」+ result );
}
}
這樣既可使得日誌操做再也不零散分佈於各個模塊,易於管理。調用者能夠經過以下方式調用:
//主函數
public class Main{
public static void main(String[ ] args){
ReportGenerator reportGeneratorImpl = new SMSReportGenerator ();
//經過系統提供的Proxy.newProxyInstance建立動態代理實例
ReportGenerator reportGenerator = (ReportGenerator ) Proxy.newProxyInstance(
reportGeneratorImpl.getClass().getClassLoader(),
reportGeneratorImpl.getClass().getInterfaces(),
new LogHandler(reportGeneratorImpl)
) ;
...
}
}
代理模式很好的實現了橫切關注點的模塊化,解決了代碼混亂代碼分散問題,可是咱們能夠看出用 Java Reflection API 實現的動態代理結構十分複雜,不易理解,Spring框架利用了代理模式的思想,提出了一種基於JAVA註解(Annotation)和XML配置的面向切面編程方法(AOP ,Aspect-Oriented Programming)簡化了編程過程。
C、Spring AOP 使用方法
Spring AOP使用中須要爲橫切關注點(有些時候也叫切面)實現一個類,銀行帳單的例子中,切面的實現以下:
//切面模塊實現
@Aspect //註解1
public class LogAspect{
@Before(「execution(* *.*(..))」) //註解2
public void LogBefore(JoinPoint joinPoint) throw Throwable{
log4j.info(「開始:方法」+ joinPoint.getSignature().getName() );
}
@After(「execution(* *.*(..))」) //註解3
public void LogAfter(JoinPoint joinPoint) throw Throwable{
log4j.info(「結束:方法」+ joinPoint.getSignature().getName() );
}
}
註解1表示這個類是一個切面,註解2中" * *.*(..)* "是一個通配符,表示在容器中全部類裏有參數的方法。@Before(「execution(* *.*(..))」)表示在全部類裏有參數的方法前調用切面中德 LogBefore() 方法。同理,註解3中@After(「execution(* *.*(..))」)表示在全部類裏有參數的方法執行完後調用切面中的LogAfter()方法。
實現完切面類後,還須要對Spring工程中的application.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"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd" >
<aop:aspectj-autoproxy />
<bean id="smsReportGenerator" class="bank.SMSReportGenerator" />
<bean id="reportService" class="bank.ReportService">
<property name="reportGenerator" ref="smsReportGenerator" />
</bean>
<bean class="bank.LogAspect" />
</beans>
這比以前IoC依賴關係配置的XML文件多了:xmlns:aop=http://www.springframework.org/schema/aop;http://www.springframework.org/schema/aop;http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
這3個主要是聲明XML中用於AOP的一些標籤, <bean class="bank.LogAspect" /> 是在容器中聲明LogAspect切面,<aop:aspectj-autoproxy />用於自動關聯很切關注點(LogAspect)與核心關注點(SMSReportGenerator,ReportService)。不難發現Spring AOP的方法實現橫切關注點得模塊化要比用Java Reflection API簡單不少。
銀行月帳單報表例子經過使用Spring框架後變成了以下結構:
在Spring框架的基礎上原來存在耦合的程序被分紅松耦合的三個模塊。不管那個模塊修改,對其餘模塊不須要額外改動。這就完成了一種良好的架構,使軟件易理解,模塊分工明確,爲軟件的擴展、驗證、維護、分工提供了良好基礎。這就是Spring框架做用。固然Spring除了容器、控制反轉、面向切面以外還有許多其餘功能,但都是在這三個核心基礎上實現的。
我有一個微信公衆號,常常會分享一些Java技術相關的乾貨;若是你喜歡個人分享,能夠用微信搜索「Java團長」或者「javatuanzhang」關注。