利用Spring框架封装的JavaMail现实同步或异步邮件发送

作者:张纪豪

    J2EE简单地讲是在JDK上扩展了各类应用的标准规范,邮件处理便是其中一个重要的应用。它既然是规范,那么我们就可以通过JDK遵照邮件协议编写一个邮件处理系统,但事实上已经有很多厂商和开源组织这样做了。Apache是J2EE最积极的实现者之一,当然还有我们的老大——SUN。

    聊起老大,感慨万端!他已经加入Oracle——甲骨文(不是刻在乌龟壳上的那种文字吗?是我中华,也是人类上最早的语言啊,比Java早几千年哦),其掌门人拉里·埃里森是个不错的水手,别以为那只是在帆船上,至少他不至于盖茨那么不仁道——开源万岁。有理由相信Java世界还有一段辉煌的历程。Google的Android和Chrome OS两大操作系统,还会首选Java作应用开发基础语言,即便是推出自己的易语言。同时笔者预感到ChromeOS前景不可估量,它可能是推动云计算一个重要组成部分,Andtroid(主用在移动设备上,未来可能是手机上主流的操作系统),乃至微软的Windows将来可能都是该系统的小窗口而已。微软已显得老态龙钟了,再与大势已去的雅虎合作,前进步伐必将大大减缓。投资者此时可以长线买入Google股票(投资建议,必自判断)。笔者也常用google搜索引擎、gmail。

    好了,闲话少聊,言归主题。

    可能大家如笔者一样用的最多的是老大的javamail,虽然老大实现了邮件功能,但调用起来还是需要较复杂的代码来完成,而且初学者调用成功率很低(因为它还要与外界服务器通信),这就使得初学者对于它越学越迷茫。不过这方面的例子很多,因此笔者不再在此重复这些示例代码,而着重利用Spring框架封装的邮件处理功能。

    开工之前,我们先了解下环境。笔者开的是web工程,所需要的基础配置如下:

▲ JDK 1.6



▲ J2EE 1.5 

▲ JavaMail 1.4 稍作说明:J2EE 1.5中已经纳入了邮件规范,因此在开发期不要导入javamail中的jar包,运行期则需要,因此可以将jar包放入到web容器的java库中(例如Tomcat的lib目录下),要了解其意可以参考数据库驱动包的运用。文章结尾会对其进一步说明;

▲ Spring 2.5


▲ 一个邮箱  如上所述,笔者爱用Google的Gmail邮箱。

    主要文件清单:

■ MailService.java      邮件处理对象接口

■ MailServiceImpl.java  上述实现

■ Email.java                  一个普通的JavaBean,用于封装邮件数据,并与html页面中form表单对应

■ MailController.java         动作处理器

■ spring-core-config.xml      Spring核心配置

    笔者的web工程中,WEB层使用的是Spring @MVC,当然我们仅需要了解其原理,利用servlet或struts框架来做web层动作处理器都能实现。其它的文件,例如web.xml,WEB层配置均略。

 下面开始代码:


spring-core-config.xml:

<!--①邮件服务器-->

<bean id="mailSender" class="org.springframework.mail.javamail.JavaMailSenderImpl">

	<property name="protocol" value="smtp"/>

	<property name="host" value="smtp.gmail.com"/>

	<property name="port" value="465" /><!--Gmail的SMTP端口居然是这个,去google网站上了解吧-->

	<property name="username" value="到google注册一个gmail账户"/>

	<property name="password" value="这里是密码"/>

	<property name="javaMailProperties">

		<props>

			<prop key="mail.smtp.auth">true</prop>

			<prop key="mail.smtp.starttls.enable">true</prop>

			<prop key="mail.smtp.socketFactory.class">javax.net.ssl.SSLSocketFactory</prop>
                        <!--gmail要求的ssl连接-->

		</props>

	</property>

</bean>



<!--②异步线程执行器-->

<bean id="taskExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">

	<property name="corePoolSize" value="10"/>

	<property name="maxPoolSize" value="30"/>

</bean>

    这是邮件处理的两个核心配置,第一个配置(①)是往容器中装配一个JavaMailSender Bean,它就是JavaMail的封装,其中最关键的是装配过程的属性参数,这些属性既要严格遵照JavaMail规范,又要满足邮件提供商的要求,例如SMTP服务器端口是多少、发送时是否要身份验证、服务器是否采用安全连接、连接时是否加密以及采用什么样的加密方式,邮件服务商提供的这些参数直接影响到上述的配置,这往往是新手最容易忽视的环节,因此配置之前一定要到邮件提供商的站点上详细了解邮箱的技术参数。

    同步异步发送问题:JavaMail邮件处理是同步的,即用户触发事件、与SMTP Server通信、服务器返回状态消息、程序结束是单线程内,这时往往因Socket通信、服务器业务处理速度等原因而使得处理时间是个未知数。举个简单的应用实例:若用户在提交注册的同时发送一封激活账户邮件,用户有可能不知道是因为邮件服务器那儿阻塞致半天没有反应而以为注册失败并放弃,这将是失败的设计,但异步方式能解决这些问题。异步方式简单地说就是将邮件处理任务交给另外一个线程,J2EE有两种解决方案,一是种利用JMS,JMS可以实现同步和异步的消息处理,将邮件作为一个异步的消息,就可以实现异步邮件发送。JMS属于J2EE的高级应用,所以对于仅以WEB功能的容器还不支持这种服务,例如Tomcat(当然可以找到插件来解决),由于篇幅限制,本文不再牵涉到新的模块。另一种方案是利用JDK中Executor的支持,JDK 5.0后继版本增加了java.util.concurrent一个强大的并发工具包,它包含了执行器、计时器、锁、线程安全队列、线程任务框架等等。Executor——执行器,它可以将任务的“提交”与“执行”分离解耦,我们的邮件处理任务完全可以借用它实现异步执行。而Spring框架提供了封装,见②。下面我们来看如何使用它,代码如下。

MailServiceImpl.java :

package com.zhangjihao.service.impl;
import java.io.IOException;
import javax.annotation.Resource;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.task.TaskExecutor;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import com.zhangjihao.bean.Email;
import com.zhangjihao.service.MailService;
import com.zhangjihao.util.StringUtil;
/**
 * 说明:<br>
 * 
 * @author 张纪豪
 * @version
 * Build Time Jul 24, 2009
 */
@Service("mailService")
public class MailServiceImpl implements MailService {
 @Resource JavaMailSender mailSender;//注入Spring封装的javamail,Spring的xml中已让框架装配
 @Resource TaskExecutor taskExecutor;//注入Spring封装的异步执行器
 
 private Log log = LogFactory.getLog(getClass());
 private StringBuffer message = new StringBuffer();
 
 public void sendMail(Email email) throws MessagingException, IOException {
  if(email.getAddress() == null || email.getAddress().length == 0){
   this.message.append("没有收件人");
   return;
  }
  if(email.getAddress().length > 5){//收件人大于5封时,采用异步发送
   sendMailByAsynchronousMode(email);
   this.message.append("收件人过多,正在采用异步方式发送...<br/>");
  }else{
   sendMailBySynchronizationMode(email);
   this.message.append("正在同步方式发送邮件...<br/>");
  }
 }
 
 /** 
  * 异步发送
  * @see com.zhangjihao.service.MailService#sendMailByAsynchronousMode(com.zhangjihao.bean.Email)
  */
 public void sendMailByAsynchronousMode(final Email email){
  taskExecutor.execute(new Runnable(){
   public void run(){
    try {
     sendMailBySynchronizationMode(email);
    } catch (Exception e) {
     log.info(e);
    }
   }
  });
 }
 
 /** 
  * 同步发送
  * @throws IOException 
  * @see com.zhangjihao.service.MailServiceMode#sendMail(com.zhangjihao.bean.Email)
  */
 public void sendMailBySynchronizationMode(Email email) throws MessagingException, IOException {
  MimeMessage mime = mailSender.createMimeMessage();
  MimeMessageHelper helper = new MimeMessageHelper(mime, true, "utf-8");
  helper.setFrom("cs@chinaptp.com");//发件人
  helper.setTo(email.getAddress());//收件人
  helper.setBcc("administrator@chinaptp.com");//暗送
  if(StringUtil.hasLength(email.getCc())){
   String cc[] = email.getCc().split(";");
   helper.setCc(cc);//抄送
  }
  helper.setReplyTo("cs@chinaptp.com");//回复到
  helper.setSubject(email.getSubject());//邮件主题
  helper.setText(email.getContent(), true);//true表示设定html格式
  
  //内嵌资源,这种功能很少用,因为大部分资源都在网上,只需在邮件正文中给个URL就足够了.
  //helper.addInline("logo", new ClassPathResource("logo.gif"));
  
  //处理附件
  for(MultipartFile file : email.getAttachment()){
   if(file == null || file.isEmpty()){
    continue;
   }
   String fileName = file.getOriginalFilename();
   try {
    fileName = new String(fileName.getBytes("utf-8"),"ISO-8859-1");
   } catch (Exception e) {}
   helper.addAttachment(fileName, new ByteArrayResource(file.getBytes()));
  }
  mailSender.send(mime);
 }
 public StringBuffer getMessage() {
  return message;
 }
 public void setMessage(StringBuffer message) {
  this.message = message;
 }
}

    此类实现了MailService接口,该接口仅三个方法(接口文件代码省略):一个发送分流器、一个同步发送方法、一个异步发送方法。通过其实现者MailServiceImpl的代码可以看出,邮件发送仅在同步发送这个方法中,当需要异步执行的时候,只需要将其扔进taskExecutor异步执行器中,就这么简单。这三个方法都是public修饰的,所以在上层随意调用哪个都行。以下看一个简单的调用代码。

    调用之前,为让初学者能更好地接受,先列出Email.java代码:


Email.java:


package com.zhangjihao.bean;
import java.io.Serializable;
import org.springframework.web.multipart.MultipartFile;
import com.zhangjihao.util.StringUtil;
/**
 * 说明:<br>
 * 
 * @author 张纪豪
 * @version
 * Build Time Jul 24, 2009
 */
public class Email implements Serializable {
 private static final long serialVersionUID = 9063903350324510652L;
 
 /**用户组:可以按用户组来批量发送邮件**/
 private UserGroups userGroups;
 /**收件人**/
 private String addressee;
 
 /**抄送给**/
 private String cc;
 
 /**邮件主题**/
 private String subject;
 
 /**邮件内容**/
 private String content;
 
 /**附件**/
 private MultipartFile[] attachment = new MultipartFile[0];
 
 //解析邮件地址//
 
 public String[] getAddress() {
  if(!StringUtil.hasLength(this.addressee)) {
   return null;
  }
  addressee = addressee.trim();
  addressee.replaceAll(";", ";");
  addressee.replaceAll(" ", ";");
  addressee.replaceAll(",", ";");
  addressee.replaceAll(",", ";");
  addressee.replaceAll("|", ";");
  return addressee.split(";");
 }

 /Getter && Setter///
 ...... 
}


    这个类就是一个简单的JavaBean,用于封装邮件数据,对于习惯使用Struts框架的读者,完全可以把它理解为一个ActionForm。但对于MultipartFile类型且是数组的attachment属性可能较难理解,熟悉Struts框架的可以看作是FormFile,在Struts2中可能好理解些。笔者使用的是Spring MVC,框架中内置了这种属性编辑器,因此很容易地将form表单上传的文件进行转换成这个字段。

    我们来看看WEB层调用,其实到此为止,就已经完成本文的主题了,因此WEB怎么调用都是围绕MailService中的三个方法,为便有全面的认识,将代码列出,不过最好需要了解Spring @MVC的一些知识。

MailController.java:


package com.zhangjihao.web.controller.system;
import java.util.List;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import com.zhangjihao.bean.Email;
import com.zhangjihao.domain.user.User;
import com.zhangjihao.service.MailService;
import com.zhangjihao.service.UserService;
import com.zhangjihao.util.StringUtil;
import com.zhangjihao.web.controller.MasterController;
import com.zhangjihao.web.validator.EmailValidator;
/**
 * 说明:<br>
 * 邮件发送处理器
 * @author 张纪豪
 * @version
 * Build Time Jul 24, 2009
 */
@Controller
public class MailController extends MasterController {
 @Resource MailService mailService;
 @Resource UserService userService;
 @RequestMapping(value = "/sendEmail", method=RequestMethod.GET)
 public String sendEmail(@RequestParam(value="email",required=false) String singleEmailAddress , HttpServletRequest request){
  Email email = new Email();
  if(StringUtil.hasLength(singleEmailAddress)){
   email.setAddressee(singleEmailAddress);
  }
  request.setAttribute("email", email);
  return "system/sendMail";
 }
 
 @RequestMapping(value = "/sendEmail", method=RequestMethod.POST)
 public String send(
   @ModelAttribute Email email,  //Spring MVC将form表单的数据封装到这个对象中
   BindingResult result,
   ModelMap model,
   HttpServletRequest request){
  try {
   new EmailValidator().validate(email, result);
   if(result.hasErrors()){
    throw new RuntimeException("数据填写不正确");
   }
   if(email.getEmailGroup()!=null){
    List<User> users = userService.getUserByUserGroups(email.getEmailGroup(), "userName,email", null, null);
    StringBuffer sb = new StringBuffer(StringUtil.hasLengthBytrim(email.getAddressee()) ? (email.getAddressee().trim() + ";") : "");
    for(User user : users){
     sb.append(user.getEmail()).append(";");
    }
    email.setAddressee(sb.toString());
   }
   if(email.getAddress()==null || email.getAddress().length==0){
    request.setAttribute("message", "没有收件人!");
    return "message";
   }
   
   mailService.sendMail(email);  //大于5个收件人时,分流器会自动选择异步方式发送
   request.setAttribute("message", mailService.getMessage().append("本次共发送了 ").append(email.getAddress().length).append(" 封邮件.").toString());
   
  } catch (Exception e) {
   request.setAttribute("message", "Has errors! The info by "+e.getMessage()+"<br/>Can log in to view more detailed information on abnormalities.");
   log.error(this.getClass().getName()+"中发生异常---------------:\n", e);
  }
  return BACK + "message";
 }
}


    当一个get方法请求的连接进来,此控制器会转向一个html页面,其页面中有form表单,表单中的字段与Email.java对应,当post方法过来后,Spring MVC会把表单中的数据填充到Email对象中,交给MailService处理就ok了。

    最后讲述下最容易出现的错误:

    网上很多人都说J2EE5兼容性不好,例如典型的javamail1.4中包与J2EE5中包接口包引起冲突,导致单元测试经常报如下错误:

java.lang.NoClassDefFoundError: com/sun/mail/util/BEncoderStream
    当然这个错误是没有将javamail的实现者引进工程(没有导包),但导包后,就会出现另外一个错误:
    java.lang.NoClassDefFoundError: com/sun/mail/util/LineInputStream

    此时甚至web容器都无法启动,经常会有网友们为这两个异常搞得焦头烂额,如此更换J2EE1.4,会对工程造成影响。但是一定要把概念弄清楚,问题就好解决。J2EE5中mail.jar包定义的只是接口,没有实现,是不能真正发送邮件的,但开发编译肯定是可以过去的,因为我们是针对J2EE规范编的程序。而运行期用Sun公司的JavaMail1.4的实现才可以开始发送邮件,但老大为什么把这两个弄冲突了?

    笔者的解决办法是:

    开发期不要导包,运行期将javamail1.4压缩文件中的mail.jar包放入到tomcat\lib目录下,这样完全可以通过开发和运行。若要做单元测试则新开一个Java Project,注意,不是web工程,此时可以将javamail1.4压缩包中的mail.jar放入到工程的classpath下。