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

最近有个小功能,需要发送邮件。

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

xml 复制代码
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-mail</artifactId>
        </dependency>

问题定位

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

  1. 创建连接 :查看源码发现org.springframework.mail.javamail.JavaMailSenderImpldoSend方法 每次发送邮件都会创建新的连接。每次创建连接都要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);
					}
					...
  1. 生成消息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秒左右

相关推荐
悟空码字19 小时前
Spring Boot 整合 MongoDB 最佳实践:CRUD、分页、事务、索引全覆盖
java·spring boot·后端
皮皮林5512 天前
拒绝写重复代码,试试这套开源的 SpringBoot 组件,效率翻倍~
java·spring boot
用户908324602735 天前
Spring AI 1.1.2 + Neo4j:用知识图谱增强 RAG 检索(上篇:图谱构建)
java·spring boot
用户8307196840826 天前
Spring Boot 集成 RabbitMQ :8 个最佳实践,杜绝消息丢失与队列阻塞
spring boot·后端·rabbitmq
Java水解6 天前
Spring Boot 视图层与模板引擎
spring boot·后端
Java水解6 天前
一文搞懂 Spring Boot 默认数据库连接池 HikariCP
spring boot·后端
洋洋技术笔记6 天前
Spring Boot Web MVC配置详解
spring boot·后端
初次攀爬者7 天前
Kafka 基础介绍
spring boot·kafka·消息队列
用户8307196840827 天前
spring ai alibaba + nacos +mcp 实现mcp服务负载均衡调用实战
spring boot·spring·mcp
Java水解7 天前
SpringBoot3全栈开发实战:从入门到精通的完整指南
spring boot·后端