今天把之前写的使用JavaMail异步发送邮件的demo程序贴出来。
最近一段时间,发现新浪微博手机客户端也开始支持异步发送信息了。不管是发微博,还是评论微博,点击过“发送”按钮之后,马上会被告知“已经进入发送队列”,我觉得这明显增加了用户体验,并且这个提升也不存在任何技术困难。这样一种情况,比如我发一个带图的微博消息,在不使用wifi的情况下,上传一个稍大些的图片可能会耗费不少时间。假如微博客户端不支持异步发送,也许就因为图片的上传,这个客户端得卡上好半天,直到上传完成为止。这种完全阻塞的方式,对用户来说可不是种好的体验。
发送邮件的时候同样存在着类似上面的情况。整个邮件的发送过程是比较耗时的,假如使用普通的单线程串行处理方式,当并发量大时,必然带来灾难性的后果。在下面的例子中,我使用多线程的方式来解决这个问题,使得邮件支持异步发送。
要支持新浪微博的异步发送,可以使用多线程方式,也可以使用消息服务。我本身对于JMS的方式不太了解,因此选择了一种相对熟悉和容易实现的方式,即每个邮件发送请求都作为一个线程任务,由线程池中的线程来处理每一个邮件发送任务。
首先,介绍邮件的JavaBean对象Mail。很简单,无需赘言。
package org.tang.financial.domain; import java.util.List; public class Mail { /** * 发送人 */ private String sender; /** * 收件人 */ private List<String> recipientsTo; /** * 抄送人 */ private List<String> recipientsCc; /** * 密送人 */ private List<String> recipientsBcc; /** * 主题 */ private String subject; /** * 正文 */ private String body; /** * 附件列表 */ private List<String> attachments; public String getSender() { return sender; } public void setSender(String sender) { this.sender = sender; } public List<String> getRecipientsTo() { return recipientsTo; } public void setRecipientsTo(List<String> recipientsTo) { this.recipientsTo = recipientsTo; } public List<String> getRecipientsCc() { return recipientsCc; } public void setRecipientsCc(List<String> recipientsCc) { this.recipientsCc = recipientsCc; } public List<String> getRecipientsBcc() { return recipientsBcc; } public void setRecipientsBcc(List<String> recipientsBcc) { this.recipientsBcc = recipientsBcc; } public String getSubject() { return subject; } public void setSubject(String subject) { this.subject = subject; } public String getBody() { return body; } public void setBody(String body) { this.body = body; } public List<String> getAttachments() { return attachments; } public void setAttachments(List<String> attachments) { this.attachments = attachments; } }
其次,是邮件发送程序当中需要用到的常量。各个常量的含义都已经有说明,也无需赘言。
package org.tang.financial.mail; public abstract class MailProperties { /** * SMTP服务器 */ public static final String MAIL_SMTP_HOST = "mail.smtp.host"; /** * SMTP服务器端口号 */ public static final String MAIL_SMTP_PORT = "mail.smtp.port"; /** * 登录SMTP服务器是否需要通过授权。可选值为true和false */ public static final String MAIL_SMTP_AUTH = "mail.smtp.auth"; /** * 登录SMTP服务器默认邮箱账号 */ public static final String MAIL_SMTP_USER = "mail.smtp.user"; /** * 登录SMTP服务器默认邮箱账号对应密码 */ public static final String MAIL_SMTP_PASSWORD = "mail.smtp.password"; /** * 是否打开程序调试。可选值包括true和false */ public static final String MAIL_DEBUG = "mail.debug"; }
接着,是邮件发送程序需要使用到得properties属性配置文件。各个键值的含义参考上面的说明。
mail.smtp.host = smtp.example.com mail.smtp.port = 25 mail.smtp.auth = true mail.smtp.user = [email protected] mail.smtp.password = password mail.debug = true
最后,邮件发送的处理程序。
package org.tang.financial.service; import java.io.IOException; import java.io.InputStream; import java.util.Date; import java.util.List; import java.util.Properties; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import javax.mail.Authenticator; import javax.mail.Message; import javax.mail.MessagingException; import javax.mail.Multipart; import javax.mail.PasswordAuthentication; import javax.mail.Session; import javax.mail.Transport; import javax.mail.internet.AddressException; import javax.mail.internet.InternetAddress; import javax.mail.internet.MimeBodyPart; import javax.mail.internet.MimeMessage; import javax.mail.internet.MimeMultipart; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.stereotype.Component; import org.tang.financial.domain.Mail; import org.tang.financial.mail.MailProperties; import org.tang.financial.util.CollectionUtils; import org.tang.financial.util.StringUtils; @Component public class MailService { private static Log logger = LogFactory.getLog(MailService.class); private static final String MAIL_PROPERTIE_NAME = "JavaMail.properties"; private static Properties mailPro = new Properties(); private static Executor executor = Executors.newFixedThreadPool(10); static { //初始化,读取属性文件的过程 InputStream in = null; try { in = MailService.class.getResourceAsStream(MAIL_PROPERTIE_NAME); mailPro.load(in); } catch (IOException e) { if (logger.isErrorEnabled()) { logger.error(e); } } finally { if (in != null) { try { in.close(); } catch (IOException e) { if (logger.isErrorEnabled()) { logger.error(e); } } } } } public boolean sendMail(final Mail mail) { if(mail == null){ return false; } //创建邮件发送任务 Runnable task = new Runnable() { @Override public void run() { final String username = mailPro.getProperty(MailProperties.MAIL_SMTP_USER); final String password = mailPro.getProperty(MailProperties.MAIL_SMTP_PASSWORD); //创建发送邮件的会话 Session session = Session.getDefaultInstance(mailPro, new Authenticator() { protected PasswordAuthentication getPasswordAuthentication() { return new PasswordAuthentication(username, password); } }); try { //创建邮件消息 MimeMessage msg = new MimeMessage(session); //设置邮件发送人 msg.setFrom(new InternetAddress(StringUtils.isEmpty(mail .getSender()) ? mailPro .getProperty(MailProperties.MAIL_SMTP_USER) : mail .getSender())); //分别设置邮件的收件人、抄送人和密送人 msg.setRecipients(Message.RecipientType.TO, strListToInternetAddresses(mail.getRecipientsTo())); msg.setRecipients(Message.RecipientType.CC, strListToInternetAddresses(mail.getRecipientsCc())); msg.setRecipients(Message.RecipientType.BCC, strListToInternetAddresses(mail.getRecipientsBcc())); //设置邮件主题 msg.setSubject(mail.getSubject()); Multipart mp = new MimeMultipart(); //创建邮件主体内容 MimeBodyPart mbp1 = new MimeBodyPart(); mbp1.setText(mail.getBody()); mp.addBodyPart(mbp1); if(!CollectionUtils.isEmpty(mail.getAttachments())){ //循环添加邮件附件 MimeBodyPart attach = null; for(String path : mail.getAttachments()){ attach = new MimeBodyPart(); try { attach.attachFile(path); mp.addBodyPart(attach); } catch (IOException e) { if (logger.isErrorEnabled()) { logger.error(e); } } } } msg.setContent(mp); msg.setSentDate(new Date()); //邮件开始发送 Transport.send(msg); } catch (AddressException e) { if (logger.isErrorEnabled()) { logger.error(e); } } catch (MessagingException e) { if (logger.isErrorEnabled()) { logger.error(e); } } } }; //使用Executor框架的线程池执行邮件发送任务 executor.execute(task); return true; } /** * 将列表中的字符串转换成InternetAddress对象 * @param list 邮件字符串地址列表 * @return InternetAddress对象数组 */ private InternetAddress[] strListToInternetAddresses(List<String> list) { if (list == null || list.isEmpty()) { return null; } int size = list.size(); InternetAddress[] arr = new InternetAddress[size]; for (int i = 0; i < size; i++) { try { arr[i] = new InternetAddress(list.get(i)); } catch (AddressException e) { e.printStackTrace(); } } return arr; } }