在线邮件项目:SpringBoot + 消息队列
在线邮件是很多OA系统必备功能,作为OA系统内嵌式的重要模块,采用SpringBoot + 消息队列技术模块实现。
项目架构:
邮件发送人作为消息生产者,触发sendMail()方法后,将消息放入消息队列中,等待邮件接收者接收邮件:
生产者发送邮件:
import com.itstyle.mail.common.model.Email;
import com.itstyle.mail.common.model.Result;
public interface IMailService {
void send(Email mail) throws Exception;
void sendHtml(Email mail) throws Exception;
void sendFreemarker(Email mail) throws Exception;
void sendThymeleaf(Email mail) throws Exception;
void sendQueue(Email mail) throws Exception;
void sendRedisQueue(Email mail) throws Exception;
Result listMail(Email mail);
}
具体实现如下:
import java.io.File;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.mail.internet.MimeMessage;
import org.springframework.beans.factory.annotation.Service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.ui.freemarker.FreeMarkerTemplateUtils;
import org.springframework.util.ResourceUtils;
import org.thymeleaf.context.Context;
import org.thymeleaf.spring5.SpringTemplateEngine;
import com.itstyle.mail.common.dynamicquery.DynamicQuery;
import com.itstyle.mail.common.model.Email;
import com.itstyle.mail.common.model.Result;
import com.itstyle.mail.common.queue.MailQueue;
import com.itstyle.mail.common.util.Constants;
import com.itstyle.mail.entity.OaEmail;
import com.itstyle.mail.repository.MailRepository;
import com.itstyle.mail.service.IMailService;
import freemarker.template.Configuration;
import freemarker.template.Template;
@Service(version = "1.0.0")
public class MailServiceImpl implements IMailService {
private static final Logger logger = LoggerFactory.getLogger(MailServiceImpl.class);
@Autowired
private DynamicQuery dynamicQuery;
@Autowired
private MailRepository mailRepository;
@Autowired
private JavaMailSender mailSender;//执行者
@Autowired
public Configuration configuration;//freemarker
@Autowired
private SpringTemplateEngine templateEngine;//thymeleaf
@Value("${spring.mail.username}")
public String USER_NAME;//发送者
@Value("${server.path}")
public String PATH;//发送者
@Autowired
private RedisTemplate<String, String> redisTemplate;
static {
System.setProperty("mail.mime.splitlongparameters","false");
//编码后的文件名长度如果大于60并且splitLongParameters的值为true,encodeParameters的值为true;
//文件名就会被截取,截取后导致出现未命名或者奇怪命名,因此设置为false;
//或者添加System.setProperty("mail.mime.charset","UTF-8");
}
@Override
public void send(Email mail) throws Exception {
logger.info("发送邮件:{}",mail.getContent());
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(USER_NAME);
message.setTo(mail.getEmail());
message.setSubject(mail.getSubject());
message.setText(mail.getContent());
mailSender.send(message);
}
@Override
public void sendHtml(Email mail) throws Exception {
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true);
//这里可以自定义发信名称
helper.setFrom(USER_NAME,"笔记");
helper.setTo(mail.getEmail());
helper.setSubject(mail.getSubject());
helper.setText(
"<html><body><img src=\"cid:springcloud\" ></body></html>",
true);
// 发送图片
File file = ResourceUtils.getFile("classpath:static"
+ Constants.SF_FILE_SEPARATOR + "image"
+ Constants.SF_FILE_SEPARATOR + "springcloud.png");
helper.addInline("springcloud", file);
// 发送附件
file = ResourceUtils.getFile("classpath:static"
+ Constants.SF_FILE_SEPARATOR + "file"
+ Constants.SF_FILE_SEPARATOR + "笔记.zip");
helper.addAttachment("笔记", file);
mailSender.send(message);
}
@Override
public void sendFreemarker(Email mail) throws Exception {
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true);
//这里可以自定义发信名称比如:笔记
helper.setFrom(USER_NAME,"笔记");
helper.setTo(mail.getEmail());
helper.setSubject(mail.getSubject());
Map<String, Object> model = new HashMap<>();
model.put("mail", mail);
model.put("path", PATH);
Template template = configuration.getTemplate(mail.getTemplate());
String text = FreeMarkerTemplateUtils.processTemplateIntoString(
template, model);
helper.setText(text, true);
mailSender.send(message);
mail.setContent(text);
OaEmail oaEmail = new OaEmail(mail);
mailRepository.save(oaEmail);
}
//弃用
@Override
public void sendThymeleaf(Email mail) throws Exception {
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setFrom(USER_NAME);
helper.setTo(mail.getEmail());
helper.setSubject(mail.getSubject());
Context context = new Context();
context.setVariable("email", mail);
String text = templateEngine.process(mail.getTemplate(), context);
helper.setText(text, true);
mailSender.send(message);
}
@Override
public void sendQueue(Email mail) throws Exception {
MailQueue.getMailQueue().produce(mail);
}
@Override
public void sendRedisQueue(Email mail) throws Exception {
redisTemplate.convertAndSend("mail",mail);
}
@Override
public Result listMail(Email mail) {
List<OaEmail> list = mailRepository.findAll();
return Result.ok(list);
}
}
从而实现具体邮件发送逻辑。
消息队列:
使用LinkedBlockingQueue来实现邮件发送队列,LinkedBlockingQueue作为一个阻塞队列是线程安全的,同时具有先进先出等特性,是作为生产者消费者的首选。用到put和take方法,put方法在队列满的时候会阻塞直到有队列成员被消费,take方法在队列空的时候会阻塞,直到有队列成员被放进来,从而实现生产入队,消费出队。
说明:
(1) LinkedBlockingQueue继承于AbstractQueue,它本质上是一个FIFO(先进先出)的队列。
(2) LinkedBlockingQueue实现了BlockingQueue接口,它支持多线程并发。当多线程竞争同一个资源时,某线程获取到该资源之后,其它线程需要阻塞等待。
(3) LinkedBlockingQueue是通过单链表实现的。
3.a) head是链表的表头。取出数据时,都是从表头head处插入。
3.b) last是链表的表尾。新增数据时,都是从表尾last处插入。
3.c) count是链表的实际大小,即当前链表中包含的节点个数。
3.d) capacity是列表的容量,它是在创建链表时指定的。
3.e) putLock是插入锁,takeLock是取出锁;notEmpty是“非空条件”,notFull是“未满条件”。通过它们对链表进行并发控制。
java实现
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import com.itstyle.mail.common.model.Email;
public class MailQueue {
//队列大小
static final int QUEUE_MAX_SIZE = 1000;
static BlockingQueue<Email> blockingQueue = new LinkedBlockingQueue<Email>(QUEUE_MAX_SIZE);
/**
* 私有的默认构造子,保证外界无法直接实例化
*/
private MailQueue(){};
/**
* 类级的内部类,也就是静态的成员式内部类,该内部类的实例与外部类的实例
* 没有绑定关系,而且只有被调用到才会装载,从而实现了延迟加载
*/
private static class SingletonHolder{
/**
* 静态初始化器,由JVM来保证线程安全
*/
private static MailQueue queue = new MailQueue();
}
//单例队列
public static MailQueue getMailQueue(){
return SingletonHolder.queue;
}
//生产入队
public void produce(Email mail) throws InterruptedException {
blockingQueue.put(mail);
}
//消费出队
public Email consume() throws InterruptedException {
return blockingQueue.take();
}
// 获取队列大小
public int size() {
return blockingQueue.size();
}
}
消费队列
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import org.apache.dubbo.config.annotation.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import com.itstyle.mail.common.model.Email;
import com.itstyle.mail.service.IMailService;
/**
* 消费队列
*/
@Component
public class ConsumeMailQueue {
private static final Logger logger = LoggerFactory.getLogger(ConsumeMailQueue.class);
@Reference(check = false,version = "1.0.0")
IMailService mailService;
@PostConstruct
public void startThread() {
ExecutorService e = Executors.newFixedThreadPool(2);// 两个大小的固定线程池
e.submit(new PollMail(mailService));
e.submit(new PollMail(mailService));
}
class PollMail implements Runnable {
IMailService mailService;
public PollMail(IMailService mailService) {
this.mailService = mailService;
}
@Override
public void run() {
while (true) {
try {
Email mail = MailQueue.getMailQueue().consume();
if (mail != null) {
logger.info("剩余邮件总数:{}",MailQueue.getMailQueue().size());
mailService.send(mail);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
@PreDestroy
public void stopThread() {
logger.info("destroy");
}
}
消费者:
配置监听器来接收消息
1.消费者监听到消息,触发sendmail()方法。
2.根据邮件号唯一标识判断是否第一次发送,如果是第一次发送,先入数据库;否则直接发送。
3.是否发送成功,如果成功,更新发送成功这种状态;失败则再向队列中生产消息,重新发送。
java实现:
import java.util.concurrent.CountDownLatch;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
import org.springframework.stereotype.Component;
/**
* 注意 扫描监听 否则无法接收消息
*
*/
@Component
public class RedisListener {
private static final Logger LOGGER = LoggerFactory.getLogger(RedisListener.class);
@Bean
RedisMessageListenerContainer container(
RedisConnectionFactory connectionFactory,
MessageListenerAdapter listenerAdapter) {
LOGGER.info("启动监听");
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.addMessageListener(listenerAdapter, new PatternTopic("mail"));
return container;
}
@Bean
MessageListenerAdapter listenerAdapter(Receiver receiver) {
return new MessageListenerAdapter(receiver, "receiveMessage");
}
@Bean
Receiver receiver(CountDownLatch latch) {
return new Receiver(latch);
}
@Bean
CountDownLatch latch() {
return new CountDownLatch(1);
}
@Bean
StringRedisTemplate template(RedisConnectionFactory connectionFactory) {
return new StringRedisTemplate(connectionFactory);
}
}
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
import com.alibaba.dubbo.config.annotation.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.itstyle.mail.common.model.Email;
import com.itstyle.mail.service.IMailService;
public class Receiver {
private static final Logger LOGGER = LoggerFactory.getLogger(Receiver.class);
@Reference(check = false)
private IMailService mailService;
private CountDownLatch latch;
@Autowired
public Receiver(CountDownLatch latch) {
this.latch = latch;
}
public void receiveMessage(String message) {
LOGGER.info("接收email消息 <{}>",message);
if(message == null){
LOGGER.info("接收email消息 <" + null + ">");
}else {
ObjectMapper mapper = new ObjectMapper();
try {
Email email = mapper.readValue(message, Email.class);
mailService.send(email);
LOGGER.info("接收email消息内容 <{}>",email.getContent());
} catch (JsonParseException e) {
e.printStackTrace();
} catch (JsonMappingException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
latch.countDown();
}
}
异步+多实例功能实现:
import java.util.Properties;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.atomic.AtomicInteger;
import javax.mail.internet.MimeMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.JavaMailSenderImpl;
/**
* 异步发送
*
*/
public class MailUtil {
private Logger logger = LoggerFactory.getLogger(MailUtil.class);
private ScheduledExecutorService service = Executors.newScheduledThreadPool(6);
private final AtomicInteger count = new AtomicInteger(1);
public void start(final JavaMailSender mailSender,final SimpleMailMessage message) {
service.execute(new Runnable() {
@Override
public void run() {
try {
if (count.get() == 2) {
service.shutdown();
logger.info("the task is down");
}
logger.info("start send email and the index is " + count);
mailSender.send(message);
logger.info("send email success");
}catch (Exception e){
logger.error("send email fail" , e);
}
}
});
}
public void startHtml(final JavaMailSender mailSender,final MimeMessage message) {
service.execute(new Runnable() {
@Override
public void run() {
try {
if (count.get() == 2) {
service.shutdown();
logger.info("the task is down");
}
logger.info("start send email and the index is " + count);
mailSender.send(message);
logger.info("send email success");
}catch (Exception e){
logger.error("send email fail" , e);
}
}
});
}
/**
* 获取 Sender 多实例发送
*/
public static JavaMailSenderImpl createMailSender(){
JavaMailSenderImpl sender = new JavaMailSenderImpl();
sender.setHost("smtp.mxhichina.com");
sender.setPort(25);
sender.setUsername("admin@52itstyle.com");
sender.setPassword("123456");
sender.setDefaultEncoding("Utf-8");
Properties p = new Properties();
p.setProperty("mail.smtp.timeout",1000+"");
p.setProperty("mail.smtp.auth","true");
sender.setJavaMailProperties(p);
return sender;
}
public static void main(String[] args) {
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom("admin@52itstyle.com");
message.setTo("345849402@qq.com");
message.setSubject("测试");
message.setText("测试");
createMailSender().send(message);
}
}
故障与解决:
1.SSLHandShakeException异常
JDK中有一个jce包,版本不同会有影响,建议替换JDK1.7中的jce包。
2.乱码
- 编码方式不统一,建议统一使用UTF-8编码,兼容中文。
- 截断问题
static {
System.setProperty("mail.mime.splitlongparameters","false");
//编码后的文件名长度如果大于60并且splitLongParameters的值为true,encodeParameters的值为true;
//文件名就会被截取,截取后导致出现未命名或者奇怪命名,因此设置为false;
//或者添加System.setProperty("mail.mime.charset","UTF-8");
}