springboot 邮件发送太慢的问题定位及解决方案

最近有个小功能,需要发送邮件。
使用了spring-boot-starter-mail组件进行邮件操作,却发现速度特别慢,每次发送都要20秒左右。为了解决这个问题,针对源码进行了一些研究和优化

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-mail</artifactId>
        </dependency>

问题定位

通过分析,发现主要在以下两部分比较耗时

  1. 创建连接:查看源码发现org.springframework.mail.javamail.JavaMailSenderImpldoSend方法 每次发送邮件都会创建新的连接。每次创建连接都要10秒左右
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);
					}
					...
  1. 生成消息ID: 这里耗时其实是没想到的,通过查看源码发现在生成消息ID的时候,代码中会尝试去DNS服务查找跟自身IP符合的域名,这个操作特别耗时

解决方案

定位到了问题,解决方案自然也就有了

缓存TCP连接或池化

针对第一个问题,最常见的方式就是将Transport对象池化。感兴趣的可以自己去实现,通过common-pool就可以轻松实现了,
由于我的功能更简单,单个资源就够用了,所有就做了下面这种直接缓存连接对象即可最简单的优化,代码如下

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);
        }
    }
}

配置类如下

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参数就行,如下

java -Dmail.mime.address.usecanonicalhostname=false -Dfile.encoding=UTF8 -Duser.timezone=GMT+8 -jar app.jar

总结

经过以上两手优化,邮件发送速度,从原来的20秒,现在只需要0.5秒左右

  • 4
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
以下是使用SpringBoot发送邮件的示例代码,其中包括了如何发送带附件的邮件和如何开启TLS验证: ```java import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.mail.MailProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.core.io.FileSystemResource; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mail.javamail.MimeMessageHelper; import javax.mail.MessagingException; import javax.mail.internet.MimeMessage; import java.io.File; @SpringBootApplication @EnableConfigurationProperties(MailProperties.class) public class MailApplication { @Autowired private JavaMailSender mailSender; @Autowired private MailProperties mailProperties; public static void main(String[] args) { SpringApplication.run(MailApplication.class, args); } public void sendMailWithAttachment() throws MessagingException { MimeMessage message = mailSender.createMimeMessage(); MimeMessageHelper helper = new MimeMessageHelper(message, true); helper.setFrom(mailProperties.getUsername()); helper.setTo("recipient@example.com"); helper.setSubject("Test email with attachment"); // 添加附件 FileSystemResource file = new FileSystemResource(new File("attachment.txt")); helper.addAttachment("attachment.txt", file); // 发送邮件 mailSender.send(message); } } ``` 在application.properties文件中添加以下配置: ``` spring.mail.username=xxxxxxx@outlook.com spring.mail.password=xxxxxxxxx spring.mail.port=587 spring.mail.host=smtp-mail.outlook.com spring.mail.properties.mail.smtp.starttls.required=true ``` 注意:在使用Outlook发送邮件时,需要开启TLS验证,否则会显示匿名用户无法通过验证。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值