Spring 整合 Scala 程式設計【轉】
- 本文將介紹如何通過將優秀的程式語言Scala整合當今世界最為流行的框架Spring中。為了清楚地闡釋Scala與Spring的整合原理,本文將使用一個簡單的示例應用。
Scala近期正式釋出了2.8版本,這門優秀的程式語言將簡潔、清晰的語法與面向物件和函數語言程式設計正規化無縫融合起來,同時又完全兼容於Java,這樣Scala就能使用Java開發者所熟知的Java API和眾多的框架了。在這種情況下,我們可以通過Scala改進並簡化現有的Java框架。此外,Scala的學習門檻也非常低,因為我們可以輕鬆將其整合到“眾所周知的Java世界中”。
本文將介紹如何通過Scala整合當今世界最為流行的框架之一Spring。Spring不僅支援如依賴注入和麵向方面的程式設計等高效的程式設計正規化,還提供了大量的膠水程式碼與Hibernate、Toplink等框架以及JEE環境互動,後者更是可以保證Scala能平滑地融入到企業當中,毫無疑問,這是Spring的成功所在。
為了清楚地闡釋Scala與Spring的整合原理,本文將使用一個簡單的示例應用。這個應用會使用到Scala、Spring和Hibernate/JPA,其領域模型如下圖所示:
該領域模型展示了一個簡化的社交網路應用:人與人之間可以彼此連結起來。
第一步
後面的講解都將基於該領域模型。首先介紹如何實現一個泛型DAO,並通過Hibernate/JPA使用Scala為Person實體實現一個具體的DAO,該DAO的名字為PersonDao,裡面封裝了CRUD操作。如下所示:
- val p1 = new Person(“Rod Johnson”)
-
val p2 = dao.findByName(“Martin Odersky”)
- p1.link(p2)
- personDao.save(p1)
第二步
接下來介紹如何將Person實體轉換為一個“內容豐富”的領域物件,在呼叫link方法時,該物件內部會使用NotificationService執行額外的邏輯,這個服務會“神奇地”按需注入到物件中。下圖展示了這一切:
- val p1 = Person(“Martin Odersky”) //the omission of the ‘new’ keyword is intentional
- val p2 = dao.findByName(“Rod Johnson”)
- p1.link(p2) //magic happens here
- personDao.save(p1)
第三步
最後,本文將介紹Spring是如何從Scala的高階概念:特徵(traits)中受益的。特徵可以將內容豐富的Person領域物件轉換為羽翼豐滿的OO類,這個類能夠實現所有的職責,包括CRUD操作。如下所示:
- Person(“Martin Odersky”).save
第一步:使用Scala、Spring和Hibernate/JPA實現DAO
需求
毫無疑問,DAO在設計上應該有一個泛型DAO和一個針對Person實體的具體DAO。泛型DAO中應該包含基本的CRUD方法,如save、remove、findById和findAll等。由於是泛型,因此它處理的是型別而不是具體的實體實現。總的來說,這個泛型DAO具有如下的介面定義:
- trait GenericDao[T] {
- def findAll():List[T]
- def save(entity:T):T
- def remove(entity:T):Unit
- def findById(id:Serializable):T
- }
Person實體類的具體DAO應該增加一個特定於Person實體的finder方法:
- trait PersonDao extends GenericDao[Person] {
- def findByName(name:String):List[Person]
- //more finders here…
- }
我們需要考慮如下具體的實現細節以便利用上Scala提供的眾多富有成效的特性:
◆關於集合:雖然底層的JPA實現並不知道所謂的Scala集合,但DAO介面返回的卻是Scala集合型別(scala.List)而不是Java集合。因為Scala集合要比Java集合強大的多,因此DAO方法的呼叫者非常希望方法能夠返回Scala集合。這樣,我們需要將JPA返回的Java集合平滑地轉換為Scala集合。
◆關於回撥:Spring用於粘合JPA、JMS等框架的大多數膠水程式碼都是基於模板模式,比如JpaTemplate、JmsTemplate等。雖然這些模板通過一些便捷的方法在一定程度上隱藏了底層框架的複雜性,但很多時候我們還是不可避免地要直接訪問底層的實現類,如EntityManager、JmsSession等。在這種情況下,Spring通過JpaCallback等回撥類來實現我們的願望。回撥方法doIn…(..)唯一的引數就是指向實現類的引用,比如EntityManager。下面的示例闡述了這種程式設計模型:
- jpaTemplate.execute(new JpaCallback() {
- public Object doInJpa(EntityManager em) throws PersistenceException {
- //… do something with the EntityManager
- return null;
- }
- });
上面的程式碼有兩點值得我們注意:首先,匿名內部回撥類的例項化需要大量的樣板程式碼。其次,還有一個限制:匿名內部類JpaCallback之外的所有引數都必須是final的。如果從Scala的視角來看待這種回撥模式,我們發現裡面充斥的全都是某個“函式”的繁瑣實現。我們真正想要的只是能夠直接訪問EntityManager而已,並不需要匿名內部類,而且還得實現裡面的doInJpa(…)方法,這有點太小題大作了。換句話說,我們只需要下面這一行足矣:
- jpaTemplate.execute((em:EntityManager) => em.createQuery(…)// etc. );
問題在於如何通過優雅的方式實現這個功能。
◆關於getter和setter:使用了Spring bean的類至少要有一個setter方法,該方法對應於特定bean的名稱。毫無疑問,這些setter是框架所需的樣板程式碼,如果不使用構造器注入也能避免這一點豈不美哉?
實現
如果用Scala實現泛型與Person DAO,那麼上面提到的一切問題都將迎刃而解,請看:
- object GenericJpaDaoSupport {
- implicit def jpaCallbackWrapper[T](func:(EntityManager) => T) = {
- new JpaCallback {
- def doInJpa(session:EntityManager ) = func(session).asInstanceOf[Object]}
- }
- }
- import Scala.collection.jcl.Conversions._
- class GenericJpaDaoSupport[T](val entityClass:Class[T]) extends JpaDaoSupport with GenericDao[T] {
- def findAll():List[T] = {
- getJpaTemplate().find("from " + entityClass.getName).toList.asInstanceOf[List[T]]
- }
- def save(entity:T) :T = {
- getJpaTemplate().persist(entity)
- entity
- }
- def remove(entity:T) = {
- getJpaTemplate().remove(entity);
- }
- def findById(id:Serializable):T = {
- getJpaTemplate().find(entityClass, id).asInstanceOf[T];
- }
- }
- class JpaPersonDao extends GenericJpaDaoSupport(classOf[Person]) with PersonDao {
- def findByName(name:String) = { getJpaTemplate().executeFind( (em:EntityManager) => {
- val query = em.createQuery("SELECT p FROM Person p WHERE p.name like :name");
- query.setParameter("name", "%" + name + "%");
- query.getResultList();
- }).asInstanceOf[List[Person]].toList
- }
- }
使用:
- class PersonDaoTestCase extends AbstractTransactionalDataSourceSpringContextTests {
- @BeanProperty var personDao:PersonDao = null
- override def getConfigLocations() = Array("ctx-jpa.xml", "ctx-datasource.xml")
- def testSavePerson {
- expect(0)(personDao.findAll().size)
- personDao.save(new Person("Rod Johnson"))
- val persons = personDao.findAll()
- expect(1)( persons size)
- assert(persons.exists(_.name ==”Rod Johnson”))
- }
- }
接下來解釋上面的程式碼是如何解決之前遇到的那些問題的:
關於集合
Scala 2.7.x提供了一個方便的Java集合到Scala集合的轉換類,這是通過隱式轉換實現的。上面的示例將一個Java list轉換為Scala list,如下程式碼所示:
匯入Scala.collection.jcl.Conversions類的所有方法:
- import Scala.collection.jcl.Conversions._
這個類提供了隱式的轉換方法將Java集合轉換為對應的Scala集合“包裝器”。對於java.util.List來說,Scala會建立一個Scala.collection.jcl.BufferWrapper。
呼叫BufferWrapper的toList()方法返回Scala.List集合的一個例項。
下面的程式碼闡述了這個轉換過程:
- def findAll() : List[T] = {
- getJpaTemplate().find("from " + entityClass.getName).toList.asInstanceOf[List[T]]
- }
總是手工呼叫“toList”方法來轉換集合有些麻煩。幸好,Scala 2.8(在本文撰寫之際尚未釋出最終版)將會解決這個瑕疵,它可以通過scala.collection.JavaConversions類將Java轉換為Scala,整個過程完全透明。
關於回撥
可以通過隱式轉換將Spring回撥輕鬆轉換為Scala函式,如GenericJpaDaoSupport物件中所示:
- implicit def jpaCallbackWrapper[T](func:(EntityManager) => T) = {
- new JpaCallback {
- def doInJpa(session:EntityManager ) = func(session).asInstanceOf[Object]}
- }
藉助於這個轉換,我們可以通過一個函式來呼叫JpaTemplate的execute方法而無需匿名內部類JPACallback了,這樣就能直接與感興趣的物件打交道了:
- jpaTemplate.execute((em:EntityManager) => em.createQuery(…)// etc. );
這麼做消除了另一處樣板程式碼。
關於getter和setter
預設情況下,Scala編譯器並不會生成符合JavaBean約定的getter和setter方法。然而,可以通過在例項變數上使用Scala註解來生成JavaBean風格的getter和setter方法。下面的示例取自上文的PersonDaoTestCase:
- import reflect._
- @BeanProperty var personDao:PersonDao = _
@BeanProperty註解告訴Scala編譯器生成setPersonDao(…)和getPersonDao()方法,而這正是Spring進行依賴注入所需的。這個簡單的想法能為每個例項變數省掉3~6行的setter與getter方法程式碼。
第二步:按需進行依賴注入的富領域物件
到目前為止,我們精簡了DAO模式的實現,該實現只能持久化實體的狀態。實體本身並沒有什麼,它只維護了一個狀態而已。對於領域驅動設計(DDD)的擁躉來說,這種簡單的實體並不足以應對複雜領域的挑戰。一個實體若想成為富領域物件不僅要包含狀態,還得能呼叫業務服務。為了達成這一目標,需要一種透明的機制將服務注入到領域物件中,不管物件在何處例項化都該如此。
Scala與Spring的整合可以在執行期輕鬆將服務透明地注入到各種物件中。後面將會提到,這種機制的技術基礎是DDD,可以用一種優雅的方式將實體提升為富領域物件。
需求
為了說清楚何謂按需的依賴注入,我們為這個示例應用加一個新需求:在呼叫Person實體的link方法時,它不僅會連結相應的Person,還會呼叫NotificationService以通知連結的雙方。下面的程式碼闡述了這個新需求:
- class Person
- { @BeanProperty var notificationService:NotificationService = _ def link(relation:Person) =
- { relations.add(relation) notificationService.nofity(PersonLinkageNotification(this, relation))
- } //other code omitted for readability
- }
毫無疑問,在例項化完Person實體或從資料庫中取出Person實體後就應該可以使用NotificationService了,無需手工設定。
使用Spring實現自動裝配
我們使用Spring的自動裝配來實現這個功能,這是通過Java單例類RichDomainObjectFactory達成的:
- public class RichDomainObjectFactory implements BeanFactoryAware
- {
- pritic RichDomainObjectFactory singleton = new
- RichDomainObjectFactory();
- public static RichDomainObjectFactory autoWireFactory()
- {
- return singleton;
- }
- public void autowire(Object instance)
- {
- factory.autowireBeanProperties(instance)
- }
- public void setBeanFactory(BeanFactory factory) throws BeansException {
- this.factory = (AutowireCapableBeanFactory) factory;
- }
- }
通過將RichDomainObjectFactory宣告為Spring bean,Spring容器確保在容器初始化完畢後就設定好了AutowireCapableBeanFactory:
- <beanclass="org.jsi.di.spring.RichDomainObjectFactory"factory-method="autoWireFactory"/>
這裡並沒有讓Spring容器建立自己的RichDomainObjectFactory例項,而是在bean定義中使用了factory-method屬性,它會強制Spring使用autoWireFactory()方法返回的引用,該引用是單例的。這樣會將AutowireCapableBeanFactory注入到單例的RichDomainObjectFactory中。由於可以在同一個類裝載器範圍內訪問單例物件,這樣該範圍內的所有類都可以使用RichDomainObjectFactory了,它能以一種非侵入、鬆耦合的方式使用Spring的自動裝配特性。毋庸置疑,Scala程式碼也可以訪問到RichDomainObjectFactory單例並使用其自動裝配功能。
在設定完這個自動裝配工廠後,接下來需要在程式碼/框架中定義鉤子(hook)了。總的來說需要在兩個地方定義:
◆ORM層,它負責從資料庫中載入實體
◆需要“手工”建立新實體的程式碼中
自動裝配ORM層中的領域物件
由於文中的示例程式碼使用了JPA/Hibernate,因此在實體載入後需要將這些框架所提供的裝置掛載到RichDomainObjectFactory中。JPA/Hibernate提供了一個攔截器API,這樣可以攔截和定製實體載入等事件。為了自動裝配剛載入的實體,需要使用如下的攔截器實現:
- class DependencyInjectionInterceptor extends EmptyInterceptor {
- override def onLoad(instance:Object, id:Serializable, propertieValues:Array[Object],propertyNames:Array[String], propertyTypes:Array[Type]) = {
- RichDomainObjectFactory.autoWireFactory.autowire(instance)
- false
- }
- }
該攔截器需要做的唯一一件事就是將載入的實體傳遞給RichDomainObjectFactory的autowire方法。對於該示例應用來說,onLoad方法的實現保證了每次從資料庫中載入Person實體後都將NotificationService注入其中。
此外,還需要通過hibernate.ejb.interceptor屬性將攔截器註冊到JPA的永續性上下文中:
- <persistence-unitname="ScalaSpringIntegration"transaction-type="RESOURCE_LOCAL">
- <provider>org.hibernate.ejb.HibernatePersistence</provider>
- <propertyname="hibernate.ejb.interceptor"value="org.jsi.domain.jpa.DependencyInjectionInterceptor"/>
- </properties>
- <!-- more properties here-->
- </persistence-unit>
DependencyInjectionInterceptor非常強大,每次從資料庫中載入實體後它都能將在Spring中配置的服務注入其中。那如果我們在應用程式碼而非JAP等框架中例項化實體時又該怎麼辦呢?
自動裝配“手工”例項化的領域物件
要想自動裝配應用程式碼中例項化的實體,最簡單也是最笨的辦法就是通過RichDomainObjectFactory的方式顯式進行自動裝配。由於這個辦法將RichDomainObjectFactory類與實體建立程式碼緊耦合起來,因此並不推薦使用。幸好,Scala提供了“元件物件”的概念,它擔負起工廠的職責,可以靈活實現構造邏輯。
對於該示例應用,我們採用如下方式實現Person物件以便“自動”提供自動裝配功能:
- import org.jsi.di.spring.RichDomainObjectFactory._
- object Person {
- def apply(name:String) = {
- autoWireFactory.autowire(new Person(name))
- }
- }
import宣告會匯入RichDomainObjectFactory的所有靜態方法,其中的autoWireFactory()方法會處理RichDomainObjectFactory單例物件。
Scala物件另一個便利的構造手段就是apply()方法,其規則是擁有apply方法的任何物件在呼叫時可以省略掉.apply()。這樣,Scala會將對Person()的呼叫轉給Person.apply(),因此可以將自動裝配程式碼放到apply()方法中。
這樣,無需使用“new”關鍵字就可以呼叫Person()了,它會返回一個新的實體,返回前所有必要的服務都已經注入進去了,該實體也成為一個“富”DDD實體了。
現在我們可以使用富領域物件了,它是可持久化的,也能在需要時呼叫其中的服務:
- trait JpaPersistable[T] extends JpaDaoSupport {
- def getEntity:T;
- def findAll():List[T] = {
- getJpaTemplate().find("from " + getEntityClass.getName).toList.asInstanceOf[List[T]]
- }
- def save():T = {
- getJpaTemplate().persist(getEntity)
- getEntity
- }
- def remove() = {
- getJpaTemplate().remove(getEntity);
- }
- def findById(id:Serializable):T = {
- getJpaTemplate().find(getEntityClass, id).asInstanceOf[T];
- }
- //…more code omitted for readability
- }
在繼續之前,我們需要解釋一下為何要用Java而不是Scala來實現RichDomainObjectFactory,原因是由Scala處理static的方式造成的。Scala故意沒有提供static關鍵字,因為static與複合的OO/函式式正規化有衝突。Scala語言所提供的唯一一個靜態特性就是物件,其在Java中的等價物就是單例。由於Scala缺少static方法,因此Spring沒法通過上文介紹的factory-method屬性獲得RichDomainObjectFactory這樣的工廠物件。這樣,我們就沒法將Spring的AutowireCapableBeanFactory直接注入到Person物件中了。因此,這裡使用Java而非Scala來利用Spring的自動裝配功能,它能徹底填充static鴻溝。
第三步:使用Scala traits打造功能完善的領域物件
到目前為止一切尚好,此外,Scala還為OO純粹主義者提供了更多特性。使用DAO持久化實體與純粹的OO理念有些許衝突。從廣泛使用的DAO/Repository模式的角度來說,DAO只負責執行持久化操作,而實體則只維護其狀態。但純粹的OO物件不僅有狀態,還要有行為。
上文介紹的實體是擁有服務的,這些服務封裝了一些行為性職責,但持久化部分並不在其中。為什麼不把所有的行為性和狀態性職責都賦給實體呢,就像OO純粹主義者所倡導的那樣,讓實體自己負責持久化操作。事實上,這是習慣問題。但使用Java很難以優雅的方式讓實體自己去實現持久化操作。這種設計嚴重依賴於繼承,因為持久化方法要在父類中實現。這種方式相當麻煩,也缺少靈活性。Java從概念上就缺少一個良好設計的根基,沒法很好地實現這種邏輯。但Scala則不同,因為Scala有traits。
所謂trait就是可以包含實現的介面。它類似於C++中多繼承的概念,但卻沒有眾所周知的diamond syndrome副作用。通過將DAO程式碼封裝到trait中,該DAO trait所提供的所有持久化方法可自動為所有實現類所用。這種方式完美地詮釋了DRY(Don’t Repeat Yourself)準則,因為持久化邏輯只實現一次,在需要的時候可以多次混合到領域類中。
對於該示例應用來說,其DAO trait如下程式碼所示:
- trait JpaPersistable[T] extends JpaDaoSupport {
- def getEntity:T;
- def findAll():List[T] = {
- getJpaTemplate().find("from " + getEntityClass.getName).toList.asInstanceOf[List[T]]
- }
- def save():T = {
- getJpaTemplate().persist(getEntity)
- getEntity
- }
- def remove() = {
- getJpaTemplate().remove(getEntity);
- }
- def findById(id:Serializable):T = {
- getJpaTemplate().find(getEntityClass, id).asInstanceOf[T];
- }
- //…more code omitted for readability
- }
作為一個傳統的DAO,該trait繼承了Spring的JpaDaoSupport,但它並沒有提供save、update和delete方法(這些方法需要接收一個實體作為引數)轉而定義了一個抽象方法getEntity,需要持久化功能的領域物件得實現這個方法。JpaPersistable trait在內部實現中使用getEntity來儲存、更新和刪除特定的實體,如下程式碼片段所示。
- trait JpaPersistable[T] extends JpaDaoSupport {
- def getEntity:T
- def remove() = {
- getJpaTemplate().remove(getEntity);
- }
- //…more code omitted for readability
- )
實現該trait的領域物件只需實現getEntity方法即可,該方法的實現僅僅是返回一個自身引用:
- class Person extends JpaPersistable[Person] with java.io.Serializable {
- def getEntity = this
- //…more code omitted for readability
- }
這就是全部了。所有需要持久化行為的領域物件只需實現JpaPersistable trait即可。最後我們得到的是一個包含了狀態和行為功能完善的領域物件,完全符合純粹的OO程式設計的理念:
- Person(“Martin Odersky”).save
無論你是否為純粹的OO理念的擁護者,這個示例都闡釋了Scala(尤其是traits概念)是如何輕鬆實現純粹的OO設計的。
結論
本文示例介紹了Scala與Spring是如何實現互補的。Scala簡明、強大的正規化(比如函式與特徵)再結合Spring的依賴注入、AOP和Java AP為我們I提供了更廣闊的空間,相對於Java程式碼來說,Scala的實現更具表現力、程式碼量也更少。
如果具有Spring和Java基礎,Scala的學習曲線非常低,因為我們只需要學習一門新語言就行,無需再學大量的API了。
Scala和Spring所提供的眾多功能使得這一組合成為企業採用Scala的最佳選擇。總之,我們能以極低的代價遷移到更加強大的程式設計正規化上來。
關於作者
Urs Peter是Xebia的高階諮詢師,專注於企業級Java和敏捷開發。它有9年的IT從業經歷。在整個IT職業生涯中,他擔任過不同角色,從開發者、軟體架構師到Scrum Master。目前,他在下一代的荷蘭鐵路資訊系統專案中擔任Scrum Master,該專案部分使用Scala實現。他還是Xebia的一名Scala佈道師和荷蘭Scala使用者組的活躍分子。
文中所用原始碼
感興趣的讀者可以使用git:git clone git://github.com/upeter/Scala-Spring-Integration.git在http://github.com/upeter/Scala-Spring-Integration上下載完整的原始碼並使用maven構建。
檢視英文原文:Scala & Spring: Combine the best of both worlds