《Spring3实战》摘要(14)其他Spring技巧
第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("xxx@spitter.com");
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("xxx@spitter.com");
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() 来判断结果的状态。