Spring框架簡介

1、使用框架的意義與Spring的主要內容

隨着軟件結構的日益龐大,軟件模塊化趨勢出現,軟件開發也需要多人合作,隨即分工出現。如何劃分模塊,如何定義接口方便分工成爲軟件工程設計中越來越關注的問題。良好的模塊化具有以下優勢:可擴展、易驗證、易維護、易分工、易理解、代碼複用。

優良的模塊設計往往遵守“低耦合高內聚”的原則。而“框架”是對開發中良好設計的總結,把設計中經常使用的代碼獨立出來,所形成的一種軟件工具。用戶遵守它的開發規則,就可以實現良好的模塊化,避免軟件開發中潛在的問題。廣義上的框架無處不再,一個常見的例子就是PC硬件體系結構,人們只要按照各自需要的主板、顯卡、內存等器件就可以任意組裝成自己想要的電腦。而做主板的廠商不用關心做顯卡廠商的怎麼實現它的功能。軟件框架也是如此,開發人員只要在Spring框架中填充自己的業務邏輯就能完成一個模塊劃分清晰紛的系統。

這裏主要通過一個銀行通知用戶月收支記錄的小例子來介紹輕型J2EE框架Spring的主要內容、它所解決的問題和實現的方法。

Spring框架主要可以分爲3個核心內容:

  1. 容器

  2. 控制反轉(IoC ,Inversion of Control)

  3. 面向切面編程(AOP ,Aspect-Oriented Programming)

例子中依次對這些特性進行介紹,描述了軟件模塊化後存在的依賴與問題,以及Spring框架如何解決這些問題。 

2、一個簡單的例子程序

假設有一個如下應用場景:(1)一個銀行在每月的月初都需要向客戶發送上個月的賬單,賬單發送的方式可以爲紙質郵寄、或者短信方式。(2)還有一個潛在的需求:爲了安全起見,在每個函數操作過程中都需要記錄日誌,記錄參數傳入是否正常,函數是否正常結束,以便出錯時系統管理員查賬。

那麼對這個需求進行簡單實現。系統框圖如下所示:


首先定義一個賬單輸出的接口:

//接口
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()代替。

3、Spring中的容器

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<StringObject> components; 

   public Container(){ 
       component = new HashMap<StringObject>(); 
       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不需要修改任何代碼。一定程度上降低了模塊之間的耦合。

4、Spring中的控制反轉

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,完成依賴注入。

5、Spring中的面向切面編程

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簡單很多。

6、Spring總結

銀行月賬單報表例子通過使用Spring框架後變成了如下結構:


在Spring框架的基礎上原來存在耦合的程序被分成鬆耦合的三個模塊。無論那個模塊修改,對其他模塊不需要額外改動。這就完成了一種良好的架構,使軟件易理解,模塊分工明確,爲軟件的擴展、驗證、維護、分工提供了良好基礎。這就是Spring框架作用。當然Spring除了容器、控制反轉、面向切面之外還有許多其他功能,但都是在這三個核心基礎上實現的。

我有一個微信公衆號,經常會分享一些Java技術相關的乾貨;如果你喜歡我的分享,可以用微信搜索“Java團長”或者“javatuanzhang”關注。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章