第14章 其他Spring技巧
14.1 外部化配置
Spring 自帶了幾個選項,可以藉助它們將 Spring 配置細節信息外部化到屬性文件中,這樣就能在部署的應用之外進行管理:
- 屬性佔位符配置(Property placeholder configurer),會將佔位符內的變量替換爲外部屬性文件的值;
- 屬性重寫(Property overrider),會將 Bean 屬性的值用外部屬性文件的值進行重寫。
通過上述兩種方式來外部化屬性值,可以很容易的修改數據庫dataSource等 Bean 的屬性值,而不需要重新構建和部署應用。
除此之外,開源的 Jasypt 項目提供了可選的 Spring 屬性佔位符配置和屬性重寫實現,可以從加密的屬性文件中獲取屬性值。
14.1.1 替換屬性佔位符
Spring 2.5 藉助 context 配置上下文中的 <context:property-placeholder>
元素,來實現屬性佔位符功能配置。
<!-- 示例1:佔位符配置將從名爲 db.properties 的文件中獲取屬性值,這個文件位於類路徑的根目錄下。 -->
<context:property-placeholder location="carnation:/db.properties" />
<!-- 示例2:佔位符配置將從文件系統的屬性文件中獲取配置數據 -->
<context:property-placeholder location="file:///ect/db.properties" />
現在,你可以將 Spring 配置中的硬編碼值替換爲基於 db.properties 屬性的佔位符變量:
<!-- 示例3:使用佔位符配置 dataSource 的屬性 -->
<bean id="dataSource"
class="org.springframework.jdbc.dataSource.DriverManagerDataSource"
p:driverClassName="${jdbc.driverClassName}"
p:url="${jdbc.url}"
...
/>
更重要的是,屬性佔位符配置的作用不限於 XML 中的 Bean 屬性配置。你還可以用它來配置 @Value 註解的屬性。
<!-- 示例4:將 jdbc.url 的值注入 Bean 的屬性中 -->
@value("${jdbc.url}")
String databaseUrl;
另外,你甚至還可以在屬性文件自身中使用佔位符變量。
##示例5:在外部 properties 文件中使用佔位符
jdbc.protocol=hsqldb:hsql
db.server=localhost
db.name=spitter
jdbc.url=jdbc:${jdbc.protocol}://${db.server}/${db.name}/${db.name}
14.1.1.1 替換缺失的屬性
如果一個屬性佔位符變量引用了沒有定義的屬性時,默認情況下,Spring 上下文加載以及創建 Bean 時會拋出異常。可以配置<context:property-placeholder>
的 ignore-resource-not-found 和 ignore-unresolvable 屬性:
<!-- 示例6: -->
<context:property-placeholder location="carnation:/db.properties"
ignore-resource-not-found="true"
ignore-unresolvable="true"
<!-- 如果有佔位符變量無法引用時,將會使用id爲defaultConfiguration的Bean中的默認值,見示例7 -->
properties-ref="defaultConfiguration"/>
通過將這些屬性設置爲 true ,將會隱藏在佔位符變量無法解析或者屬性文件不存在時拋出的異常。佔位符會是未解析的狀態
示例6中,properties-ref 屬性 被設置爲 java.util.Properties Bean 的 ID,這個 Bean 包含了默認使用的屬性。
<!-- 示例7:配置屬性佔位符無法引用時的默認屬性 -->
<util:properties id="defaultConfiguration">
<prop key="jdbc.url">xxxx</prop>
<prop key="jdbc.username">xxxx</prop>
...
</util:properties>
14.1.1.2 通過系統屬性解析佔位符變量
我們還可以通過系統屬性來解析佔位符變量。我們所要做的就是設置<context:property-placeholder>
的 system-properties-mode 屬性。
<!--示例8: -->
<context:property-placeholder
location="file:///etc/myconfig.properties"
ignore-resource-not-found="true"
ignore-unresolvable="true"
properties-ref="defaultConfiguration"
system-properties-mode="OVERRIDE" />
在這裏,system-properties-mode 被設置爲 OVERRIDE ,這意味着相對於 db.properties 和 defaultConfiguration Bean 中的屬性,<context:property-placeholder>
會優先使用系統屬性。OVERRIDE 只是 system-properties-mode 允許接受的3個屬性值之一。system-properties-mode 的默認值爲 FALLBACK 值。
- FALLBACK:如果不能從屬性文件中解析佔位符變量,將使用系統屬性。
- NEVER:從不使用系統屬性來解析佔位符變量。
- OVERRIDE:相對於配置文件,優先使用系統屬性。
14.1.2 重寫屬性
Spring 外部化配置的另一種方式是使用屬性文件來重寫 Bean 屬性。在這種情況下,不需要佔位符,屬性要麼使用默認值裝配要麼使用重寫值裝配。如果外部屬性於 Bean 的屬性匹配,那麼將使用外部值來替換 Spring 明確裝配的值。
屬性重寫和屬性佔位符配置很類似。區別在於我們要使用<context:property-override>
,而不是<context:property-placeholder>
:
<!--示例9: -->
<context:property-override
location="classpath:/db.properties" />
爲了讓屬性重寫知道 db.properties 中的屬性匹配到 Spring 應用上下文的哪個 Bean 屬性,你必須將 Spring 中的 Bean 和屬性名映射到屬性文件的屬性名上。如下圖所示:
14.1.3 加密外部屬性
Jasypt 項目是一個非常棒的類庫,它簡化了 Java 中的加密操作,jasypt 提供了 Spring 屬性佔位符替換和屬性重寫的實現,藉助這些實現可以從外部屬性文件中讀取加密的屬性。
Jasypt 的屬性佔位符實現和屬性重寫當前並沒有特定的配置命名空間,所以,你需要將 Jasypt 屬性佔位符和屬性重寫配置爲 <bean>
元素。
<!--示例10:Jasypt 的屬性佔位符實現 -->
<bean class="org.jasypt.spring.properties.EncryptablePropertyPlaceholderConfigurer"
p:location="file:///etc/db.properties">
<constructor-arg ref="stringEncrypter" />
</bean>
<!--示例11:Jasypt 屬性重寫實現 -->
<bean class="org.jasypt.spring.properties.EncryptablePropertyOverrideConfigurer"
p:location="file:///etc/db.properties">
<constructor-arg ref="stringEncrypter" />
</bean>
不管選擇哪個,你都需要通過 location 配置屬性文件的位置,並且它們兩個類都需要一個字符串加密器(string encryptor)對象作爲構造參數。
在Jasypt中,字符串加密器是加密 String 值的策略類。屬性佔位符配置 / 重寫將會使用這個字符串加密器來解密在外部配置文件中找到的加密器。對於我們的需求而言,Jasypt自帶的 StandardPBEStringEncryptor 就足夠了:
<!--示例12:配置 Jasypt 的加密器Bean -->
<bean id="stringEncrypter"
class="org.jasypt.encryption.pbe.StandardPBEStringEncryptor"
p:config-ref="enviromentConfig" />
爲完成其任務,StandardPBEStringEncryptor 需要用來加密數據的算法和密碼。它有 algorithm 和 password 屬性,可以直接在其 Bean 上配置它們。
示例12中,將StandardPBEStringEncryptor的config 屬性配置爲 EnvironmentStringPBEConfig,而不是直接將密碼配置在 Spring 中。EnvironmentStringPBEConfig會讓我們將加密細節(例如密碼)放在環境變量中。EnvironmentStringPBEConfig 是另一個 Bean,它的聲明如下:
<!--示例13: -->
<bean id="environmentConfig"
class="org.jasypt.encryption.pbe.config.EnvironmentStringPBEConfig"
p:algorithm="PBEWithMD5AndDES"
p:passwordEnvName="DB_ENCRYPTION_PWD" />
示例13將密碼放在環境變量中,環境變量被命名爲DB_ENCRYPTION_PWD。在應用程序啓動之前系統管理員設置環境變量的值並且一旦應用程序啓動就將其移除。
14.2 裝配 JNDI 對象
略
14.3 發送郵件
Spring 自帶了一個郵件抽象 API,它簡化了發送郵件的工作。
14.3.1 配置郵件發送器
Spring 郵件抽象的核心是 MailSender 接口。MailSender 實現了郵件的發送。Spring 自帶了一個 MailSender 的實現也就是 JavaMailSenderImpl.
爲了使用 JavaMailSenderImpl,我們需要在 Spring應用上下文中將其聲明爲一個 Bean:
<!--示例14:聲明JavaMailSenderImpl爲Bean -->
<bean id="mailSender"
class="org.springframework.mail.javamail.JavaMailSenderImpl"
p:host="${mailserver.host}" />
屬性 host 指定要用來發送電子郵件的郵件服務器主機名。這裏將其配置爲一個佔位符變量。
默認情況下,JavaMailSenderImpl 假設郵件服務器監聽 25 端口(標準的SMTP端口)。如果你的郵件服務器監聽不同的端口,那麼可以使用 port 屬性指定正確的端口號。
<!-- 示例15:用port屬性設定郵件服務器端口 -->
<bean id="mailSender"
class="org.springframework.mail.javamail.JavaMailSenderImpl"
p:host="${mailserver.host}"
p:port="${mailserver.port}" />
如果服務器需要認證,則需要設置 username 和 password 屬性:
<!-- 示例16:設置 username 和 password 屬性 -->
<bean id="mailSender"
class="org.springframework.mail.javamail.JavaMailSenderImpl"
p:host="${mailserver.host}"
p:port="${mailserver.port}"
p:username="${mailserver.username}"
p:password="${mailserver.password}" />
14.3.1.1 使用 JNDI 郵件會話
略
14.3.1.2 將郵件發送器裝配到服務 Bean 中
郵件發送器配送完成,需要將其裝配到使用它的 Bean 中。
/* 示例17:使用註解注入JavaMailSenderImpl */
@Autowired
JavaMailSender mailSender;
14.3.2 構建郵件
/* 示例18:實現發送郵件的函數 */
@Autowired
JavaMailSender mailSender;
public void sendSimpleSpittleEmail(String to, Spittle spittle){
//構建信息
SimpleMainMessage message = new SimpleMailMessage();
String spitterName = spittle.getSpitter().getFullName();
//收件地址
message.setFrom("[email protected]");
message.setTo(to);
message.setSubject("New spitter from " + spitterName);
//設置信息文本
message.setText("aksdlkasdlalksd");
//發送郵件
mailSender.send(message);
}
14.3.2.1 添加附件
發送帶有附件郵件的技巧是創建 multipart 類型的信息 —- 郵件將由多個部分組成,一部分是郵件體,其他部分是附件。
爲了發送 multipart 類型的郵件,需要創建一個 MIME( Multipurpose Internet Mail Extensions ) 的信息。可以從郵件發送器對象的 createMimeMessage() 方法開始:
/* 示例19:獲取 MimeMessage 對象 */
MimeMessage message = mailSender.createMimeMessage();
Javax.mail.internet.MimeMessage 本身的 API 有些笨重。Spring 提供了 MimeMessageHelper,我們需要實例化它並將 MimeMessage 傳給其構造方法:
/*
* 示例20:實例化 MimeMessageHelper,並將 MimeMessage 傳給其構造方法
*/
MimeMessage message = mailSender.createMimeMessage();
//參數true,表明這個信息是 multipart 類型的
MimeMessageHelper helper = new
MimeMessageHelper(message, true);
/*
* 示例21:使用 MimeMessageHelper 對象組裝郵件信息
*/
helper.setFrom("[email protected]");
helper.setTo(to);
helper.setSubject("xxx subject");
helper.setText("xxx text");
/*
* 示例22:添加圖片附件
*/
//加載圖片並將其作爲資源
FileSystemResource couponImage = new FileSystemResource("/collateral/coupon.png");
//添加圖片資源附件,第一個參數是附件名稱,第二個參數是圖片資源
helper.addAttachment("Coupon.png", couponImage);
mailSender.send(message);
14.3.2.2 發送帶有富文本內容的郵件
添加附件只是 multipart 類型的郵件能夠爲你所做的其中一件事而已。除此之外,通過將郵件體指明爲HTML,可以生成比簡單文本更漂亮的郵件。
發送富文本的郵件,關鍵是將信息的文本設置爲 HTML。要做到這一點只需要將 HTML 字符串傳遞給 helper 的 setText() 方法,並將第二個參數設置爲 true:
/*
* 示例23:將 HTML 字符串傳遞給 MimeMessageHelper 的 setText() 方法
*/
MimeMessage message = mailSender.createMimeMessage();
//參數true,表明這個信息是 multipart 類型的
MimeMessageHelper helper = new
MimeMessageHelper(message, true);
...
//第二個參數true,表明傳遞進來的第一個參數是HTML
//img 標籤中的 src 屬性可以設置爲標準的 http: URL,這裏值cid:spitterLogo 表明信息中會有一部分是圖片並以 spitterLogo 來進行標識
helper.setText("<html><body><img src='cid:spitterLogo'>"
+ "<h4>" + spittle.getSpitter().getFullName() + " says...</h4>"
+ "<i>" + spittle.getText() + "</i>")
+ "</body></html>", true);
//爲信息添加嵌入式的圖片,要調用 addInline() 方法
ClassPathResource image = new ClassPathResource("spitter_logo.png");
helper.addInline("spitterLogo",image);
14.3.2.3 創建郵件模板
略
14.4 調度和後臺任務
在 Spring 應用上下文中添加配置 <task:annotation-driven />
,將使Spring 自動支持調度和異步方法。這些方法分別使用 @Scheduled 和 @Async 來進行標註。
14.4.1 聲明調度方法
爲了調度某個方法,你只需要使用 @Scheduled 註解來標註它。
/* 示例24:使用@Scheduled 註解標註方法,這裏讓其每隔24小時(86400000毫秒)觸發一次 */
@Scheduled(fixedRate=86400000)
public void archiveOldSpittles(){
...
}
- 屬性 fixedRate :指定這個方法需要每隔指定的毫秒數進行週期性地調用。(每次方法開始調用之間 要經歷的毫秒數)
- 屬性 fixedDelay :指定調用之間的間隔(一次調用完成與下一次調用開始之間的間隔)。
- 屬性 cron :使用 Cron 表達式更精確地控制方法調用。
/* 示例25:使用@Scheduled 的cron 屬性,這裏指定每個星期六的零點觸發調度 */
@Scheduled(cron="0 0 0 * * SAT")
public void archiveOldSpittles(){
...
}
14.4.1.1 Cron表達式
Cron 表達式由6個(或者7個)空格分隔的時間元素構成。從左至右,定義爲:
1、秒(0~59)
2、分(0~59)
3、小時(0~23)
4、月份中的日期(1~31)
5、月份(1~12 或 JAN~DEC)
6、星期中的日期(1~7 或 SUN~SAT)
7、年份(1970~2099)
每個元素都可以顯式地指定值(如6)、範圍(9~12)、列表(9,11,13)或者通配符(如 *)。月份中的日期和星期中的日期這兩個元素時互斥的,因此應該通過設置一個問號(?)來表明你不想設置的那個字段。
下面是一些 Cron 表達式的例子
Cron 表達式 | 含義 |
---|---|
0 0 10,14,16 * * ? | 每天上午10點、下午兩點和下午4點 |
0 0,15,30,45 * 1-30 * ? | 每個月前30天每隔15分鐘 |
30 0 0 1 1 ? 2012 | 2012年1月1日午夜過30秒 |
0 0 8 -17 ? * MON-FRI | 每個工作日的工作時間 |
14.4.2 聲明異步方法
@Async 註解是一個很簡單的註解,它沒有要設置的屬性。你所需要做的就是將其用於 Bean 的方法上,這個方法就會成爲異步的了。
14.4.2.1 獲取異步方法的返回值
因爲 Spring 的異步方法是建立在 Java 的併發 API(Javaconcurrency API)之上的,它可以返回實現 java.util.concurrent.Future 的對象。這個接口代表了一個值的容器,而值能在方法返回後的某個時間點得到,但不一定是方法返回的時間點。Spring 還自帶了一個 Future 的便利實現,名爲 AsyncResult,藉助於它可以更容易地處理未來的值。
/* 示例26:使用@Async註解異步方法,並在方法執行完成時顯示結果 */
@Async
public Future<Long> performSomeReallyHairyMath(long input){
// ...
return new AsyncResult<long>(result);
}
在異步方法執行過程中,調用者會持有一個 Future 對象(實際上示 AsyncResult)。一旦得到結果,調用者可以通過調用 Future 對象的 get() 方法來得到它。在此之間,調用者可以使用 isDone() 和 isCancelled() 來判斷結果的狀態。