本章向讀者展示了在Spring中如何集成其他企業(yè)服務(wù),讀者將了解到使用Spring集成郵件服務(wù)、JMS甚至EJB都是那么的容易。
Spring并沒有對(duì)一些企業(yè)服務(wù)提供直接的支持。它依賴其他API來提供有關(guān)服務(wù),但對(duì)這些服務(wù)通過相應(yīng)的抽象層進(jìn)行了封裝,因此使用起來更為方便。
一、從JNDI中獲取對(duì)象
JNDI為Java應(yīng)用程序提供了一個(gè)用于存儲(chǔ)應(yīng)用對(duì)象的中心倉庫。
Spring的JNDI抽象使你可以在應(yīng)用的配置文件中聲明JNDI對(duì)象。然后你就可以將這些對(duì)象裝配到其他Bean的屬性中,就如同JNDI對(duì)象與其他的POJO一樣。
1.使用傳統(tǒng)的JNDI
使用傳統(tǒng)的JNDI API,你編寫的代碼也許和下面的看上去差不多:
InitialContext ctx = null;
try {
ctx = new InitialContext();
DataSource ds = (DataSource) ctx.lookup("java:comp/env/jdbc/myDatasource");
} catch (NamingException ne) {
//exception
} finally {
if (ctx != null) {
try {
ctx.close();
} catch (NamingException ne) {}
}
}
這段代碼有點(diǎn)笨拙,總的說來,這種從JNDI中獲取對(duì)象的傳統(tǒng)方式的問題都來源于違背了依賴注入的原則。不是賦給你的代碼一個(gè)對(duì)象,而是你的代碼必須親自去獲取這個(gè)對(duì)象。這意味著你的代碼所做的不是真正的工作。這也意味著你的代碼不必要地與JNDI耦合了。
2.代理JNDI對(duì)象
Spring的JndiObjectFactoryBean允許你同時(shí)得到這兩個(gè)不同領(lǐng)域中的好處。它是一個(gè)工廠Bean,這意味著當(dāng)把它裝配到一個(gè)屬性上時(shí),實(shí)際上它會(huì)創(chuàng)建一個(gè)其他類型的對(duì)象供裝配時(shí)使用。對(duì)于JndiObjectFactoryBean,它實(shí)際裝配的是一個(gè)從JNDI獲取的對(duì)象。
當(dāng)Spring裝配sessionFactory Bean時(shí),它會(huì)把從JNDI中獲取的DataSource對(duì)象注入到會(huì)話工廠Bean的dataSource屬性中。使用JndiObjectFactoryBean從JNDI中查找對(duì)象的最大優(yōu)勢(shì)在于惟一知道DataSource是從JNDI中獲取的代碼就是那段dataSource Bean的XML聲明。sessionFactory Bean不知道(也不關(guān)心)DataSource來自哪里。這意味著如果以后你決定改從一個(gè)JDBC驅(qū)動(dòng)管理器獲取DataSource,只需要重新定義dataSource Bean為一個(gè)DriverManagerDataSource即可。
二、發(fā)送電子郵件
Spring中的郵件發(fā)送器由Spring的MailSender接口定義。郵件發(fā)送器抽象了某個(gè)特定的郵件實(shí)現(xiàn)。這樣就使應(yīng)用代碼和實(shí)際使用的郵件實(shí)現(xiàn)之間沒有耦合。Spring提供了這個(gè)接口的兩個(gè)實(shí)現(xiàn):
? CosMailSenderImpl——以Jason Hunter的Java Servlet Programming一書(O’Reilly,1998)中的COS(com.oreilly.servlet)實(shí)現(xiàn)為基礎(chǔ)的SMTP郵件發(fā)送器的簡單實(shí)現(xiàn)。
? JavaMailSenderImpl——一個(gè)基于JavaMail API的郵件發(fā)送器實(shí)現(xiàn)。允許發(fā)送MIME郵件以及非SMTP郵件(比如Lotus Notes)。
采用哪個(gè)都將完成發(fā)送的工作,但是我們將選擇JavaMailSenderImpl,因?yàn)樵趦烧唛g它的功能更全面。你可以在Spring配置文件中按以下方式聲明它:
<bean id="mailSender" class="org.springframework.mail.javamail.JavaMailSenderImpl">
<property name="host">
<value>mail.springtraining.com</value>
</property>
</bean>
屬性host指定了郵件服務(wù)器的主機(jī)名,在這里是Spring培訓(xùn)應(yīng)用的SMTP服務(wù)器。在默認(rèn)情況下,郵件發(fā)送器假設(shè)SMTP服務(wù)器監(jiān)聽25端口(標(biāo)準(zhǔn)的SMTP端口),但如果你的SMTP服務(wù)器監(jiān)聽在不同的端口上,可以使用JavaMailSenderImpl的port屬性來指定端口。
上面的mailSender聲明中顯式地命名了用于發(fā)送郵件的郵件服務(wù)器。然而,如果你有一個(gè)位于JNDI中的javax.mail.MailSession對(duì)象(可能位于你的應(yīng)用服務(wù)器中),則也可以選擇從JNDI中獲取它。只需簡單地使用JndiObjectFactoryBean(如第7.1節(jié)中描述的)來獲取郵件會(huì)話對(duì)象,并按照下面的方式將它裝配到mailSender的mailSession屬性中即可:
<bean id="mailSession" class="org.springframework.jndi.JndiObjectFactoryBean">
<property name="jndiName">
<value>java:comp/env/mail/Session</value>
</property>
</bean>
<bean id="mailSender" class="org.springrframework.mail.javamail.JavaMailSenderImpl">
<property name="session"><ref bean="mailSession"/></property>
</bean>
三、調(diào)度任務(wù)
Java的Timer類和OpenSymphony的Quartz調(diào)度器是兩個(gè)流行的調(diào)度API。Spring為這兩個(gè)調(diào)度器提供了一個(gè)抽象層,使你可以更容易地使用它們。
1.使用Java Timer調(diào)度任務(wù)
從Java 1.3開始,Java SDK就通過java.util.Timer類提供了基本的調(diào)度功能。這個(gè)類允許你調(diào)度一個(gè)任務(wù)(通過java.util.TimerTask子類定義)按任意周期運(yùn)行。
創(chuàng)建一個(gè)定時(shí)器任務(wù)
使用Java Timer來調(diào)度發(fā)送注冊(cè)報(bào)表郵件的第一步是從java.util.TimerTask中派生出郵件任務(wù):
publicclass EmailReportTask extends TimerTask {
public EmailReportTask() {}
publicvoid run() {
courseService.sendCourseEnrollmentReport();
}
private CourseService courseService;
publicvoid setCourseService(CourseService courseService) {
this.courseService = courseService;
}
}
run()方法定義了當(dāng)任務(wù)運(yùn)行時(shí)該做什么。在上面的例子中,它調(diào)用CourseService的sendCourseEnrollmentReport()方法來發(fā)送注冊(cè)報(bào)表郵件。CourseService是通過依賴注入方式提供給EmailReportTask的。
按以下方式在Spring配置文件中聲明EmailReportTask:
<bean id="reportTimerTask" class="com.springinaction.training.schedule.EmailReportTask">
<property name="courseService">
<ref bean="courseService"/>
</property>
</bean>
這個(gè)聲明本身只是將EmailReportTask放到應(yīng)用上下文中,并在courseService屬性中裝配courseService Bean。在你調(diào)度它之前,它不會(huì)做任何有用的事。
調(diào)度定時(shí)器任務(wù)
Spring的ScheduledTimerTask定義了一個(gè)定時(shí)器任務(wù)的運(yùn)行周期。既然課程主任要求每天向她發(fā)送注冊(cè)報(bào)表,你應(yīng)該以如下方式裝配一個(gè)ScheduledTimerTask:
<bean id="scheduledReportTask" class="org.springframework.scheduling.timer.ScheduledTimerTask">
<property name="timerTask">
<ref bean="reportTimerTask"/>
</property>
<property name="period">
<value>86400000</value>
</property>
</bean>
屬性timerTask告訴ScheduledTimerTask運(yùn)行哪個(gè)TimerTask。在這里,該屬性裝配了指向reportTimerTask的一個(gè)引用,它就是EmailReportTask。屬性period告訴ScheduledTimerTask以怎樣的頻度調(diào)用TimerTask的run()方法。這個(gè)屬性以毫秒作為單位,它被設(shè)置為86400000,指定這個(gè)任務(wù)應(yīng)該每24小時(shí)運(yùn)行一次。
啟動(dòng)定時(shí)器
最后一步是啟動(dòng)定時(shí)器。Spring的TimerFactoryBean負(fù)責(zé)啟動(dòng)定時(shí)任務(wù)。按以下方式在Spring配置文件中聲明它:
<bean class="org.springframework.scheduling.timer.TimerFactoryBean">
<property name="scheduledTimerTasks">
<list>
<ref bean="scheduledReportTask"/>
</list>
</property>
</bean>
屬性scheduledTimerTasks要求一個(gè)需要啟動(dòng)的定時(shí)器任務(wù)的列表。既然你現(xiàn)在只有一個(gè)定時(shí)器任務(wù),這個(gè)列表中只包含一個(gè)指向scheduledReportTask Bean的引用。
遺憾的是,即使這個(gè)任務(wù)已經(jīng)能夠每隔24小時(shí)運(yùn)行一次了,在這里你無法指定它應(yīng)該在一天中的哪個(gè)時(shí)間點(diǎn)執(zhí)行。ScheduledTimerTask有一個(gè)delay屬性,允許你指定當(dāng)任務(wù)第一次運(yùn)行之前應(yīng)該等待多久。例如,要將EmailReportTask的第一次運(yùn)行延遲1小時(shí),可以按照以下方式進(jìn)行配置:
<bean id="scheduledReportTask" class="org.springframework.scheduling.timer.ScheduledTimerTask">
<property name="timerTask">
<ref bean="reportTimerTask"/>
</property>
<property name="period">
<value>86400000</value>
</property>
<property name="delay">
<value>3600000</value>
</property>
</bean>
2.使用Quartz調(diào)度器
Quartz調(diào)度器為調(diào)度工作提供了更豐富的支持。和Java定時(shí)器一樣,可以使用Quartz來每隔多少毫秒執(zhí)行一個(gè)工作。但Quartz比Java Timer更先進(jìn)之處在于它允許你調(diào)度一個(gè)工作在某個(gè)特定的時(shí)間或日期執(zhí)行。
關(guān)于Quartz的更多信息,可以訪問Quartz位于http://www.opensymphony.com/quartz的主頁。
3.按調(diào)度計(jì)劃調(diào)用方法
Spring提供了MethodInvokingTimerTaskFactoryBean和MethodInvokingJobDetailFactoryBean,可以分別使用Java的定時(shí)器支持或Quartz調(diào)度器對(duì)方法調(diào)用進(jìn)行調(diào)度。
四、使用JMS發(fā)送消息
Spring提供了JMS的一個(gè)抽象層,使得訪問一個(gè)消息隊(duì)列或主題(抽象地稱為一個(gè)目標(biāo))并向目標(biāo)發(fā)布消息成為一件簡單的事。而且,Spring以非檢查的org.springframework.jms. JmsException的形式重新拋出JMS異常,使你的應(yīng)用不必處理javax.jms.JMSException。
1.使用JMS模板發(fā)送消息
當(dāng)對(duì)支付進(jìn)行授權(quán)時(shí),必須等待信用卡處理器的響應(yīng),因?yàn)槟阈枰佬庞每ǖ陌l(fā)卡行是否授權(quán)進(jìn)行支付。但當(dāng)已經(jīng)擁有恰當(dāng)?shù)氖跈?quán)之后,支付的結(jié)算可以以異步的方式進(jìn)行。在這種情況下,不需要等待響應(yīng)——你可以安全地假設(shè)支付將會(huì)被結(jié)算。
信用卡處理系統(tǒng)接受一個(gè)通過JMS發(fā)送的異步消息用于支付的結(jié)算。它接受的消息是一個(gè)javax.jms.MapMessage,其中包含了以下字段:
? authCode——從信息卡處理器得到的授權(quán)碼;
? creditCardNumber——信用卡號(hào);
? customerName——信用卡持有人的姓名;
? expirationMonth——信用卡過期的月份;
? expirationYear——信用卡過期的年份。
Spring采用回調(diào)方式處理JMS消息。這種回調(diào)方式讓人回想起第4章中描述的JDBC回調(diào)機(jī)制。回調(diào)機(jī)制由兩部分組成:一個(gè)消息創(chuàng)建器負(fù)責(zé)構(gòu)造一個(gè)JMS消息(javax.jms.Message)和一個(gè)真正發(fā)送消息的JMS模板。
2.消費(fèi)消息
現(xiàn)在假設(shè)你在編寫結(jié)算過程的接收端代碼。你需要接收消息,將它轉(zhuǎn)換成一個(gè)PaySettlement對(duì)象,然后把它傳遞到處理過程中。幸運(yùn)的是,JmsTemplate既可以用于發(fā)送消息,也可以用于接收消息。
JmsTemplate的receive()方法嘗試從指定的目標(biāo)接受一個(gè)消息。根據(jù)之前在Spring配置文件中的聲明,receive()方法會(huì)試圖從一個(gè)JNDI名字為creditCardQueue的目標(biāo)中接收一個(gè)消息。
一旦接收到消息,它會(huì)將它強(qiáng)制轉(zhuǎn)換成一個(gè)MapMessage,并使用MapMessage中字段的值來初始化一個(gè)PaySettlement對(duì)象。
在默認(rèn)情況下,receive()方法會(huì)無限期地等待消息。然而,你可能不希望讓你的應(yīng)用在等待接收消息時(shí)無限期地阻塞。最好能夠設(shè)置一個(gè)超時(shí)間隔,使receive()方法在等待一定時(shí)間后放棄。幸運(yùn)的是,可以通過設(shè)置jmsTemplate Bean的receiveTimeout屬性來指定這個(gè)超時(shí)。例如:
<bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate">
<property name="receiveTimeout">
<value>10000</value>
</property>
</bean>
屬性receiveTimeout以一個(gè)消息等待時(shí)間的毫秒數(shù)作為參數(shù)。設(shè)置它為10000,這規(guī)定了receive()方法會(huì)在10秒后放棄。如果在10秒中內(nèi)沒有收到任何消息,JmsTemplate會(huì)拋出一個(gè)非檢查的JmsException。
3.轉(zhuǎn)換消息
轉(zhuǎn)換PaySettlement消息
盡管你可以編寫自己的工具對(duì)象來處理消息轉(zhuǎn)換,Spring的org.springframework.jms.support. converter.MessageConverter接口定義了一個(gè)公共的機(jī)制在JMS Message對(duì)象和對(duì)象間進(jìn)行相互轉(zhuǎn)換。
為了演示這一點(diǎn),PaySettlementConverter實(shí)現(xiàn)了MessageConverter,使得在PaySettlement對(duì)象與JMS Message對(duì)象間的相互轉(zhuǎn)換更方便。
裝配一個(gè)消息轉(zhuǎn)換器
要使用消息轉(zhuǎn)換器,首先必須在Spring配置文件中將它作為一個(gè)Bean加以聲明:
<bean id="settlementConverter" class="com.springinaction.training.service.PaySettlementConverter">
…
</bean>
接著需要讓JmsTemplate知道消息轉(zhuǎn)換器。你是通過將PaySettlementConverter裝配到JmsTemplate的messageConverter屬性中讓JmsTemplate知道它的:
<bean id="jmsTemplate" class="org.springframework.jms.core.JmsTemplate">
…
<property name="messageConverter">
<ref bean="settlementConverter"/>
</property>
</bean>
使用SimpleMessageConverter
Spring提供了一個(gè)現(xiàn)成的MessageConverter接口的實(shí)現(xiàn)。SimpleMessageConverter在MapMessage、TextMessage以及ByteMessage和java.util.Map集合對(duì)象、String以及byte數(shù)組之間分別進(jìn)行相互轉(zhuǎn)換。
五、小結(jié)
盡管Spring提供的功能免去了大多數(shù)的EJB使用需求,仍有很多企業(yè)服務(wù)在Spring中沒有直接的替代實(shí)現(xiàn)。在那些情況下,Spring提供了各種抽象層,使你能夠容易地將這些企業(yè)服務(wù)裝配到你的Spring應(yīng)用程序中。
在本章中,你已經(jīng)看到如何取得保存在JNDI中的對(duì)象的引用。這些引用可以像本地定義的Bean一樣裝配到Bean的屬性中。本章中處處體現(xiàn)了這種方式的實(shí)用性,比如使用Spring的JNDI抽象層來查詢諸如郵件會(huì)話和JMS連接工廠等等JNDI對(duì)象。
你也看到如何通過Spring的電子郵件抽象層來發(fā)送電子郵件,并使用Java Timer或者OpenSymphony的Quartz調(diào)度器來調(diào)度任務(wù)。
最后,你看到如何通過Spring的JMS抽象層來發(fā)送和接收異步消息。
毫無疑問,Spring所提供的功能是非常強(qiáng)大的。本章的知識(shí),在我目前的工作中依然用不上,向上一章一樣,做些簡單的摘抄以后用到再回頭好好看看。