Java系列--第八篇 基于Maven的SSME之定时邮件发送

关于ssme这个我的小示例项目,想做到麻雀虽小,五脏俱全,看到很多一些web都有定时发送邮件的功能,想我ssme也加入一下这种功能,经查询相关文档,发现spring本身自带了一个调度器quartz,下面就来试试用这个东西来实现一下自动发送一个本地化了出件出来,请注意两个重点特色,一个是自动,另一个邮件本地化。

说一段题外话, 以前一直嫌Java麻烦,不好弄环境,写个什么东西测试要搞这配那的,这不今天,我想回顾一下AOP的实现,用最简单的InvocationHandler接口,却发现也没有以前那么麻烦了。几分钟上的Demo源码就出来了,写写小程序有时候是不是一种小乐趣呢,你怎么认为?

mvn archetype:generate -DgroupId=com.vanceinfo.javaserial -Dar
tifactId=myaop -DarchetypeArtifactId=maven-archetype-quickstart -DpackageName=co
m.vanceinfo.javaserial

maven-archetype-quickstart

public interface IAnimal {
public void run();
public void jump();
}

动物接口可跑可跳

public class Dog implements IAnimal{
public void run() {
System.out.println("Dog Start running...");
}
public void jump() {
System.out.println("Dog Start jumping...");
}
}

动物小狗会跑会跳

public class MyProxy implements InvocationHandler {

    private Object targetObject;
public Object createInstance(Object targetObj){
this.targetObject=targetObj;
return Proxy.newProxyInstance(targetObj.getClass().getClassLoader(), targetObj.getClass().getInterfaces(),this);
}
private void beforeMethod(){
System.out .println("before method..");
} private void endMethod(){
System.out .println("End method..");
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
beforeMethod();
Object obj = method.invoke(targetObject, args);
endMethod();
return obj;
}
}

代理实现InvocationHandler

public class App
{
public static void main( String[] args )
{
MyProxy myProxy = new MyProxy();
IAnimal animal =(IAnimal) myProxy.createInstance(new Dog());
animal.run();
animal.jump();
}
}

客户端调用代理创建具体动物小狗

上面的这段小代码,纯粹是我一时兴起,不想另开一篇,强行插入的哈。书归正卷,回到今天的主题。

1,首先引用POM

<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.2.1</version>
</dependency>

quartz2.2.1

今天在程序中偶然发现当我用@Resource标签时,Eclipse给了我一个提示了:

Access restriction: The type Resource is not accessible due to restriction on required library E:\Java\jdk1.7.0_25\jre\lib\rt.jar

百思不得其解,其实去到properties里面删掉JRE System Library,然后再加回来就OK了,即

1,Go to the Build Path settings in the project properties.
2,Remove the JRE System Library
3,Add it back; Select "Add Library" and select the JRE System Library. The default worked for me.

国外人给的答案

2,配置文件添加片断

     <!-- scheduling taskExecutor -->
<bean id="schedulingEmailTaskExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
<property name="corePoolSize" value="5" />
<property name="maxPoolSize" value="10" />
<property name="queueCapacity" value="25" />
</bean> <bean id="schedulingEmailJob" class="com.vanceinfo.javaserial.scheduling.SchedulingEmailJob"/> <bean id="jobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
<property name="targetObject" ref="schedulingEmailJob" />
<property name="targetMethod" value="execute" />
</bean>
<!--<bean id="schedulingCronTrigger" class="org.springframework.scheduling.quartz.CronTriggerBean"> -->
<bean id="schedulingCronTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
<property name="jobDetail" ref="jobDetail" />
<property name="cronExpression" value="${com.vanceinfo.scheduling/jobCronExpression}" />
</bean> <bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="triggers">
<list>
<ref bean="schedulingCronTrigger" />
</list>
</property>
</bean>

scheduling taskExecutor

注意第16行我注释的那一行,<bean id="schedulingCronTrigger" class="org.springframework.scheduling.quartz.CronTriggerBean">,在1.x下面,很多都是这样子配置CronTrigger,以致于现在好多同学,都认为只能用低版本的Quartz,比方说1.8.4. 甚至有人还分析说:

Caused by: org.springframework.beans.factory.CannotLoadBeanClassException: Error loading class [org.springframework.scheduling.quartz.CronTriggerBean] for bean with name 'mytrigger' defined in class path resource [applicationContext.xml]: problem with class file or dependent class; nested exception is java.lang.IncompatibleClassChangeError: class org.springframework.scheduling.quartz.CronTriggerBean has interface org.quartz.CronTrigger as super class

查看发现spring3.0.5中org.springframework.scheduling.quartz.CronTriggerBean继承了org.quartz.CronTrigger(public class CronTriggerBeanextends CronTrigger),而在quartz2.0.2中org.quartz.CronTrigger是个接口(publicabstract interface CronTrigger extends Trigger),而在quartz1.8.5及1.8.4中org.quartz.CronTrigger是个类(publicclass CronTrigger extends Trigger),从而造成无法在applicationContext中配置触发器。这是spring3.0.5和quartz2.0.2版本不兼容的一个bug。

使用quartz2.2.1的出错信息

实则不然,在查看CronTriggerBean的API时有句话写得是:

NOTE: This convenience subclass does not work against Quartz 2.0. Use Quartz 2.0's native JobDetailImpl class or the new Quartz 2.0 builder API instead. Alternatively, switch to Spring's CronTriggerFactoryBean which largely is a drop-in replacement for this class and its properties and consistently works against Quartz 1.x as well as Quartz 2.x.

这个说明一件事情,有的时候,官网上的资料才是最权威,也是解决问题最直接的地方,对于咱们中国人来说,也说明另一件事就是要学好英语,如果这里是一段中文注意在这里,相信网上这些说要用低版本的Quartz估计就没有人不喷了。

3,看到配置,很明显,我们要有一个SchedulingEmailJob类。比较简单,贴代码

package com.vanceinfo.javaserial.scheduling;

import java.util.Date;

import org.apache.log4j.Logger;

import javax.annotation.Resource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.task.TaskExecutor; import com.vanceinfo.javaserial.services.IUserService; public class SchedulingEmailJob { @Resource(name = "schedulingEmailTaskExecutor")
private TaskExecutor taskExecutor; private IUserService userService; /**
* @return the userService
*/
public IUserService getUserService() {
return userService;
} /**
* @param userService the userService to set
*/
@Autowired
public void setUserService(IUserService userService) {
this.userService = userService;
} /**
* @return Returns the taskExecutor.
*/
public TaskExecutor getTaskExecutor() {
return taskExecutor;
} /**
* @param taskExecutor The taskExecutor to set.
*/
public void setTaskExecutor(TaskExecutor taskExecutor) {
this.taskExecutor = taskExecutor;
} private static Logger LOGGER = Logger.getLogger(SchedulingEmailJob.class); public void execute() {
Date taskStartTime = new Date();
LOGGER.info("Scheduled sending email task start! Server date time: " + taskStartTime);
taskExecutor.execute(new SchedulingEmailRunnable(userService));
Date taskEndTime = new Date();
LOGGER.info("Scheduled sending email task finish! Server date time: " + taskEndTime);
} }

SchedulingEmailJob

至于SchedulingEmailRunnable嘛,实际上就是在run里面调用我们前面的userService里面的方法来发送邮件

package com.vanceinfo.javaserial.scheduling;

import com.vanceinfo.javaserial.services.IUserService;

public class SchedulingEmailRunnable implements Runnable {

    private IUserService userService;

    public SchedulingEmailRunnable(IUserService userService){
this.userService = userService;
} public void run() {
userService.getUserById(1, true);
} }

SchedulingEmailRunnable

由于这里我只是演示,所以直接传了个用户id=1,实际应用中这里应该接合数据库或别的什么来得到邮件地址,然后一个一个发送出去。引申出去,我们完全可以做一个auto-bot的东东出来。

4,关于cronExpression,心中默记6位,用空格来格开。秒分时日月年,顺序决不能混。但好象有人总结也有7位的。

* * * * * ? 
- - - - - -
| | | | | |
| | | | | +----- day of week (MON-SUN) 
| | | | +------- month (1 - 12)
| | | +--------- day of month (1 - 31)
| | +----------- hour (0 - 23)
| +------------- min (0 - 59)
+------------- sec (0 - 59)

看这位兄弟的总结

一个cronExpression表达式有至少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)

0 0 10,14,16 * * ?

每天上午10点,下午2点和下午4点

0 0,15,30,45 * 1-10 * ?

每月前10天每隔15分钟

30 0 0 1 1 ? 2012

在2012年1月1日午夜过30秒时

0 0 8-5 ? * MON-FRI

每个工作日的工作时间

各个时间可用值如下:

秒 0-59 , - * /

分 0-59 , - * /

小时 0-23 , - * /

日 1-31 , - * ? / L W C

月 1-12 or JAN-DEC , - * /

周几 1-7 or SUN-SAT , - * ? / L C #

年 (可选字段) empty, 1970-2099 , - * /

可用值详细分析如下:

“*”——字符可以用于所有字段,在“分”字段中设为"*"表示"每一分钟"的含义。

“?”——字符可以用在“日”和“周几”字段. 它用来指定 '不明确的值'. 这在你需要指定这两个字段中的某一个值而不是另外一个的时候会被用到。在后面的例子中可以看到其含义。

“-”——字符被用来指定一个值的范围,比如在“小时”字段中设为"10-12"表示"10点到12点"。

“,”——字符指定数个值。比如在“周几”字段中设为"MON,WED,FRI"表示"the days Monday, Wednesday, and Friday"。

“/”——字符用来指定一个值的的增加幅度. 比如在“秒”字段中设置为"0/15"表示"第0, 15, 30, 和 45秒"。而 "5/15"则表示"第5, 20, 35, 和 50". 在'/'前加"*"字符相当于指定从0秒开始. 每个字段都有一系列可以开始或结束的数值。对于“秒”和“分”字段来说,其数值范围为0到59,对于“小时”字段来说其为0到23, 对于“日”字段来说为0到31, 而对于“月”字段来说为1到12。"/"字段仅仅只是帮助你在允许的数值范围内从开始"第n"的值。

“L”——字符可用在“日”和“周几”这两个字段。它是"last"的缩写, 但是在这两个字段中有不同的含义。例如,“日”字段中的"L"表示"一个月中的最后一天" —— 对于一月就是31号对于二月来说就是28号(非闰年)。而在“周几”字段中, 它简单的表示"7" or "SAT",但是如果在“周几”字段中使用时跟在某个数字之后, 它表示"该月最后一个星期×" —— 比如"6L"表示"该月最后一个周五"。当使用'L'选项时,指定确定的列表或者范围非常重要,否则你会被结果搞糊涂的。

“W”——可用于“日”字段。用来指定历给定日期最近的工作日(周一到周五) 。比如你将“日”字段设为"15W",意为: "离该月15号最近的工作日"。因此如果15号为周六,触发器会在14号即周五调用。如果15号为周日, 触发器会在16号也就是周一触发。如果15号为周二,那么当天就会触发。然而如果你将“日”字段设为"1W", 而一号又是周六, 触发器会于下周一也就是当月的3号触发,因为它不会越过当月的值的范围边界。'W'字符只能用于“日”字段的值为单独的一天而不是一系列值的时候。

“L”和“W”可以组合用于“日”字段表示为'LW',意为"该月最后一个工作日"。

“#”—— 字符可用于“周几”字段。该字符表示“该月第几个周×”,比如"6#3"表示该月第三个周五( 6表示周五而"#3"该月第三个)。再比如: "2#1" = 表示该月第一个周一而 "4#5" = 该月第五个周三。注意如果你指定"#5"该月没有第五个“周×”,该月是不会触发的。

“C”—— 字符可用于“日”和“周几”字段,它是"calendar"的缩写。 它表示为基于相关的日历所计算出的值(如果有的话)。如果没有关联的日历, 那它等同于包含全部日历。“日”字段值为"5C"表示"日历中的第一天或者5号以后",“周几”字段值为"1C"则表示"日历中的第一天或者周日以后"。

对于“月份”字段和“周几”字段来说合法的字符都不是大小写敏感的。

一些例子:

"0 0 12 * * ?" 每天中午十二点触发 
"0 15 10 ? * *" 每天早上10:15触发 
"0 15 10 * * ?" 每天早上10:15触发 
"0 15 10 * * ? *" 每天早上10:15触发 
"0 15 10 * * ? 2005" 2005年的每天早上10:15触发 
"0 * 14 * * ?" 每天从下午2点开始到2点59分每分钟一次触发 
"0 0/5 14 * * ?" 每天从下午2点开始到2:55分结束每5分钟一次触发 
"0 0/5 14,18 * * ?" 每天的下午2点至2:55和6点至6点55分两个时间段内每5分钟一次触发 
"0 0-5 14 * * ?" 每天14:00至14:05每分钟一次触发 
"0 10,44 14 ? 3 WED" 三月的每周三的14:10和14:44触发 
"0 15 10 ? * MON-FRI" 每个周一、周二、周三、周四、周五的10:15触发 
"0 15 10 15 * ?" 每月15号的10:15触发 
"0 15 10 L * ?" 每月的最后一天的10:15触发 
"0 15 10 ? * 6L" 每月最后一个周五的10:15

5, 进行到这里,我们似乎忘记了还有一个特点没有讲解,就是邮件的多国语言了。由于我要将一些字串转化成Locale,即LocaleUtils.toLocale("Str")所以,我需要再引入一个依赖

<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.1</version>
</dependency>

commons-lang3_3.1

接着在SendEmailImpl中加入/修改以下两句

//这句很关键,因为象我们中国语言之类的,如果 encoding不对时,是会显示乱码的。
MimeMessageHelper message = new MimeMessageHelper(mimeMessage, true,"UTF-8"); //这里只是一个示例,字串zh_CN我们应该根据页面语言获得
model.put("locale",org.apache.commons.lang3.LocaleUtils.toLocale("zh_CN"));

SendEmailImpl

当然messageSource和locale还是要放进model里的

//这一句不解释,接合我前面的系列来看,可以明白为什么直接可以这样子写
@Resource(name = "messageSource")
private MessageSource messageSource; //另外在还要加上以下两句。
model.put("messages", this.messageSource);
model.put("locale",org.apache.commons.lang3.LocaleUtils.toLocale("zh_CN"));

第三步,在zh_CN里加入两条记录

email.Bullet1_Name=注意, {0}. 您获得了以下物品:
email.Bullet2_Email=您的邮件地址是 {0}.

并且在我的邮件模板中加入

#set($userName = ["${emailPlaceHolder.name}"])
#set($userEmail=["${emailPlaceHolder.email}"]) <p class="emailTagline">$messages.getMessage("email.Bullet1_Name",$userName.toArray(), $locale)</p>
<p>$messages.getMessage("email.Bullet2_Email",$userEmail.toArray(), $locale)</p>

myEmail.vm

关于vm的语法,可以看这里。当然,我个人的印象最深的就是vm里没有for(int=0;i<n;i++)之类的。判断一个对象为空的语法是“”==$user, 贴一个我曾经印象最深的vm模板在这里。

<html>
<head>
<style type="text/css">
body {
margin: 0 auto;
max-width: 900px;
font-family: Arial, "Helvetica Neue", Helvetica, sans-serif;
font-size: 1em;
line-height: 1.2em;
background-color: #F5FFFF;
color: #333;
}
a,a:visited {
text-decoration: none;
color: #006699;
}
a:hover {
color: #EC891D;
}
.emailTagline {
margin-top: 2.6em;
font-size: 1.2em;
}
</style> </head> <body>
<a href="http://www.expediapartnercentral.com" style="outline:none;"><img src="cid:logoName"/></a>
#set($userName = ["${emailPlaceHolder.userName}"])
#set($discountStr = ["${emailPlaceHolder.drrInfo.discountStr}%"])
#set($bookStartDateStr = ["${emailPlaceHolder.drrInfo.bookStartDateStr}"])
#set($travelDatesStr = ["${emailPlaceHolder.drrInfo.travelStartDateStr}","${emailPlaceHolder.drrInfo.travelEndDateStr}"])
#set($promotionCode=["${emailPlaceHolder.drrInfo.notificationCode}"]) #if(""==$emailPlaceHolder.drrInfo.ratePlans)
#set($promotionroomsrates=[" "])
#else
#set($promotionroomsrates=["<table width='750' border='0' cellspacing='0' cellpadding='1'>
<tr><td width='240' height='30'>$messages.getMessage('roomTypeLbl',null,$locale)</td>
<td width='250'>$messages.getMessage('ratePlanLbl',null,$locale)</td>
<td width='250'>$messages.getMessage('rateTypeLbl',null,$locale)</td></tr>
#set($index=0)
#foreach($ratePlan in $emailPlaceHolder.drrInfo.ratePlans)
#if($index%2==0)
<tr style='background-color: #ebf5fb'>
<td height='30'>$ratePlan.roomType</td>
<td>$ratePlan.ratePlanName</td>
<td>$ratePlan.ratePlanType</td>
</tr>
#else
<tr>
<td height='30'>$ratePlan.roomType</td>
<td>$ratePlan.ratePlanName</td>
<td>$ratePlan.ratePlanType</td>
</tr>
#end
#set($index=$index+1)
#end
</table>"])
#end #if(""==$emailPlaceHolder.drrInfo.blackOutDates)
#set($blackoutdates=["$messages.getMessage('noBlackoutInfo',null, $locale)"])
#else
#set($blackoutdates=["<ul>
#foreach($blackOut in $emailPlaceHolder.drrInfo.blackOutDates)
#if($blackOut.startDate==$blackOut.endDate)
<li><span>$blackOut.startDateStr</span></li>
#else
<li><span>$blackOut.startDateStr</span><span>$messages.getMessage('travelDates_To',null, $locale)</span><span>$blackOut.endDateStr</span></li>
#end
#end
</ul>"])
#end <p class="emailTagline">$messages.getMessage("dotdWonAuction_1",$userName.toArray(), $locale)</p> <p>$messages.getMessage("dotdWonAuction_2",$bookStartDateStr.toArray(), $locale)</p> <p>$messages.getMessage("dotdWonAuction_3",null, $locale)</p> <p>$messages.getMessage("dotdWonAuction_Bullet1_Name",$promotionCode.toArray(), $locale)</p> <p>$messages.getMessage("dotdWonAuction_Bullet2_Discount",$discountStr.toArray(), $locale)</p> <p>$messages.getMessage("dotdWonAuction_Bullet3_BookingDates",$bookStartDateStr.toArray(), $locale)</p> <p>$messages.getMessage("dotdWonAuction_Bullet4_TravelDates",$travelDatesStr.toArray(), $locale)</p> <p>$messages.getMessage("dotdWonAuction_Bullet5_AppliesTo",$promotionroomsrates.toArray(), $locale)</p> <p>$messages.getMessage("dotdWonAuction_Bullet6_BlackoutDays",$blackoutdates.toArray(), $locale)</p> </body>
</html>

我印象深刻的一个我曾写的第一个vm模板

好了,运行程序,1分钟之后,我收到的邮件内容如下:

Java系列--第八篇 基于Maven的SSME之定时邮件发送

上一篇:Oracle 12c创建用户时出现“ORA-65096: invalid common user or role name”的错误


下一篇:Java系列--第三篇 基于Maven的Android开发CAIO