最近有个小功能,需要发送邮件。
使用了spring-boot-starter-mail
组件进行邮件操作,却发现速度特别慢,每次发送都要20秒左右。为了解决这个问题,针对源码进行了一些研究和优化
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
问题定位
通过分析,发现主要在以下两部分比较耗时
- 创建连接 :查看源码发现
org.springframework.mail.javamail.JavaMailSenderImpl
的doSend
方法 每次发送邮件都会创建新的连接。每次创建连接都要10秒左右
java
protected void doSend(MimeMessage[] mimeMessages, @Nullable Object[] originalMessages) throws MailException {
Map<Object, Exception> failedMessages = new LinkedHashMap<>();
Transport transport = null;
try {
for (int i = 0; i < mimeMessages.length; i++) {
// Check transport connection first...
if (transport == null || !transport.isConnected()) {
if (transport != null) {
try {
transport.close();
}
catch (Exception ex) {
// Ignore - we're reconnecting anyway
}
transport = null;
}
try {
transport = connectTransport();
}
catch (AuthenticationFailedException ex) {
throw new MailAuthenticationException(ex);
}
...
- 生成消息ID: 这里耗时其实是没想到的,通过查看源码发现在生成消息ID的时候,代码中会尝试去DNS服务查找跟自身IP符合的域名,这个操作特别耗时
解决方案
定位到了问题,解决方案自然也就有了
缓存TCP连接或池化
针对第一个问题,最常见的方式就是将Transport
对象池化。感兴趣的可以自己去实现,通过common-pool就可以轻松实现了,
由于我的功能更简单,单个资源就够用了,所有就做了下面这种直接缓存连接对象即可最简单的优化,代码如下
java
package com.leewan.server.service.impl;
import jakarta.mail.Address;
import jakarta.mail.AuthenticationFailedException;
import jakarta.mail.Transport;
import jakarta.mail.internet.MimeMessage;
import org.springframework.mail.MailAuthenticationException;
import org.springframework.mail.MailException;
import org.springframework.mail.MailSendException;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
public class MailSenderImpl extends JavaMailSenderImpl {
private static final String HEADER_MESSAGE_ID = "Message-ID";
private Transport transport;
@Override
protected synchronized void doSend(MimeMessage[] mimeMessages, Object[] originalMessages) throws MailException {
Map<Object, Exception> failedMessages = new LinkedHashMap<>();
for (int i = 0; i < mimeMessages.length; i++) {
// Check transport connection first...
if (transport == null || !transport.isConnected()) {
try {
transport = connectTransport();
}
catch (AuthenticationFailedException ex) {
throw new MailAuthenticationException(ex);
}
catch (Exception ex) {
// Effectively, all remaining messages failed...
for (int j = i; j < mimeMessages.length; j++) {
Object original = (originalMessages != null ? originalMessages[j] : mimeMessages[j]);
failedMessages.put(original, ex);
}
throw new MailSendException("Mail server connection failed", ex, failedMessages);
}
}
// Send message via current transport...
MimeMessage mimeMessage = mimeMessages[i];
try {
if (mimeMessage.getSentDate() == null) {
mimeMessage.setSentDate(new Date());
}
String messageId = mimeMessage.getMessageID();
mimeMessage.saveChanges();
if (messageId != null) {
// Preserve explicitly specified message id...
mimeMessage.setHeader(HEADER_MESSAGE_ID, messageId);
}
Address[] addresses = mimeMessage.getAllRecipients();
transport.sendMessage(mimeMessage, (addresses != null ? addresses : new Address[0]));
}
catch (Exception ex) {
Object original = (originalMessages != null ? originalMessages[i] : mimeMessage);
failedMessages.put(original, ex);
}
}
if (!failedMessages.isEmpty()) {
throw new MailSendException(failedMessages);
}
}
}
配置类如下
java
package com.leewan.server.context.mail;
import com.leewan.server.service.impl.MailSenderImpl;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.mail.MailProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import java.util.Map;
import java.util.Properties;
@Configuration
@EnableConfigurationProperties(MailProperties.class)
public class MailConfiguration {
@Bean
@ConditionalOnMissingBean(JavaMailSender.class)
MailSenderImpl mailSender(MailProperties properties) {
MailSenderImpl sender = new MailSenderImpl();
applyProperties(properties, sender);
return sender;
}
private void applyProperties(MailProperties properties, JavaMailSenderImpl sender) {
sender.setHost(properties.getHost());
if (properties.getPort() != null) {
sender.setPort(properties.getPort());
}
sender.setUsername(properties.getUsername());
sender.setPassword(properties.getPassword());
sender.setProtocol(properties.getProtocol());
if (properties.getDefaultEncoding() != null) {
sender.setDefaultEncoding(properties.getDefaultEncoding().name());
}
if (!properties.getProperties().isEmpty()) {
sender.setJavaMailProperties(asProperties(properties.getProperties()));
}
}
private Properties asProperties(Map<String, String> source) {
Properties properties = new Properties();
properties.putAll(source);
return properties;
}
}
设置属性避免DNS查找
通过源码发现可以设置mail.mime.address.usecanonicalhostname
去控制是否去使用DNS查询域名。
那么这种情况 我们只需要在启动命令加上-D参数就行,如下
shell
java -Dmail.mime.address.usecanonicalhostname=false -Dfile.encoding=UTF8 -Duser.timezone=GMT+8 -jar app.jar
总结
经过以上两手优化,邮件发送速度,从原来的20秒,现在只需要0.5秒左右