在日常生活中,郵件已經被聊天軟體、簡訊等更便捷的資訊傳送方式代替。但在日常工作中,我們的重要的資訊通知等非常有必要去歸檔追溯,那麼郵件就是不可或缺的資訊傳送渠道。對於我們工作中經常用到的系統,裡面也基本都整合了郵件傳送功能。
SpringBoot提供了基於JavaMail的starter,我們只要按照官方的說明設定郵件伺服器資訊,即可使我們的系統擁有傳送電子郵件的功能。但是,在我們GitEgg開發框架的實際業務開發過程中,有兩個問題需要解決:一個是SpringBoot郵箱伺服器的設定是設定在組態檔中的,不支援靈活的介面設定。另外一個是我們的開發框架需要支援多租戶,那麼此時需要對SpringBoot提供的郵件傳送功能進行擴充套件,以滿足我們的需求。
那麼,基於以上需求和問題,我們對GitEgg框架進行擴充套件,增加以下功能:
同一個租戶可以設定多個電子郵件伺服器,但只可以設定一個伺服器為啟用狀態。預設情況下,系統通知類的功能只使用啟用狀態的伺服器進行郵件傳送。在有客製化化需求的情況下,比如從頁面直接指定某個伺服器進行郵件傳送,那麼提供可以選擇的介面,指定某個伺服器進行郵件傳送。
<dependencies>
<!-- gitegg Spring Boot自定義及擴充套件 -->
<dependency>
<groupId>com.gitegg.platform</groupId>
<artifactId>gitegg-platform-boot</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
<!-- 去除springboot預設的logback設定-->
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class GitEggMailProperties extends MailProperties {
/**
* 設定id
*/
private Long id;
/**
* 租戶id
*/
private Long tenantId;
/**
* 渠道id
*/
private String channelCode;
/**
* 狀態
*/
private Integer channelStatus;
/**
* 設定的md5值
*/
private String md5;
}
@Data
public class GitEggJavaMailSenderImpl extends JavaMailSenderImpl {
/**
* 設定id
*/
private Long id;
/**
* 租戶id
*/
private Long tenantId;
/**
* 渠道編碼
*/
private String channelCode;
/**
* 設定的md5值
*/
private String md5;
}
@Slf4j
public class JavaMailSenderFactory {
private RedisTemplate redisTemplate;
private JavaMailSenderImpl javaMailSenderImpl;
/**
* 是否開啟租戶模式
*/
private Boolean enable;
/**
* JavaMailSender 快取
* 儘管存在多個微服務,但是隻需要在每個微服務初始化一次即可
*/
private final static Map<String, GitEggJavaMailSenderImpl> javaMailSenderMap = new ConcurrentHashMap<>();
public JavaMailSenderFactory(RedisTemplate redisTemplate, JavaMailSenderImpl javaMailSenderImpl, Boolean enable) {
this.redisTemplate = redisTemplate;
this.javaMailSenderImpl = javaMailSenderImpl;
this.enable = enable;
}
/**
* 指定郵件傳送渠道
* @return
*/
public JavaMailSenderImpl getMailSender(String... channelCode){
if (null == channelCode || channelCode.length == GitEggConstant.COUNT_ZERO
|| null == channelCode[GitEggConstant.Number.ZERO])
{
return this.getDefaultMailSender();
}
// 首先判斷是否開啟多租戶
String mailConfigKey = JavaMailConstant.MAIL_TENANT_CONFIG_KEY;
if (enable) {
mailConfigKey += GitEggAuthUtils.getTenantId();
} else {
mailConfigKey = JavaMailConstant.MAIL_CONFIG_KEY;
}
// 從快取獲取郵件設定資訊
// 根據channel code獲取設定,用channel code時,不區分是否是預設設定
String propertiesStr = (String) redisTemplate.opsForHash().get(mailConfigKey, channelCode[GitEggConstant.Number.ZERO]);
if (StringUtils.isEmpty(propertiesStr))
{
throw new BusinessException("未獲取到[" + channelCode[GitEggConstant.Number.ZERO] + "]的郵件設定資訊");
}
GitEggMailProperties properties = null;
try {
properties = JsonUtils.jsonToPojo(propertiesStr, GitEggMailProperties.class);
} catch (Exception e) {
log.error("轉換郵件設定資訊異常:{}", e);
throw new BusinessException("轉換郵件設定資訊異常:" + e);
}
return this.getMailSender(mailConfigKey, properties);
}
/**
* 不指定郵件傳送渠道,取預設設定
* @return
*/
public JavaMailSenderImpl getDefaultMailSender(){
// 首先判斷是否開啟多租戶
String mailConfigKey = JavaMailConstant.MAIL_TENANT_CONFIG_KEY;
if (enable) {
mailConfigKey += GitEggAuthUtils.getTenantId();
} else {
mailConfigKey = JavaMailConstant.MAIL_CONFIG_KEY;
}
// 獲取所有郵件設定列表
Map<Object, Object> propertiesMap = redisTemplate.opsForHash().entries(mailConfigKey);
Iterator<Map.Entry<Object, Object>> entries = propertiesMap.entrySet().iterator();
// 如果沒有設定取哪個設定,那麼獲取預設的設定
GitEggMailProperties properties = null;
try {
while (entries.hasNext()) {
Map.Entry<Object, Object> entry = entries.next();
// 轉為系統設定物件
GitEggMailProperties propertiesEnable = JsonUtils.jsonToPojo((String) entry.getValue(), GitEggMailProperties.class);
if (propertiesEnable.getChannelStatus().intValue() == GitEggConstant.ENABLE) {
properties = propertiesEnable;
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
return this.getMailSender(mailConfigKey, properties);
}
private JavaMailSenderImpl getMailSender(String mailConfigKey, GitEggMailProperties properties) {
// 根據最新設定資訊判斷是否從本地獲取mailSender,在設定儲存時,計算實體設定的md5值,然後進行比較,不要在每次對比的時候進行md5計算
if (null != properties && !StringUtils.isEmpty(properties.getMd5()))
{
GitEggJavaMailSenderImpl javaMailSender = javaMailSenderMap.get(mailConfigKey);
if (null == javaMailSender || !properties.getMd5().equals(javaMailSender.getMd5()))
{
// 如果沒有設定資訊,那麼直接返回系統預設設定的mailSender
javaMailSender = new GitEggJavaMailSenderImpl();
this.applyProperties(properties, javaMailSender);
javaMailSender.setMd5(properties.getMd5());
javaMailSender.setId(properties.getId());
// 將MailSender放入快取
javaMailSenderMap.put(mailConfigKey, javaMailSender);
}
return javaMailSender;
}
else
{
return this.javaMailSenderImpl;
}
}
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(this.asProperties(properties.getProperties()));
}
}
private Properties asProperties(Map<String, String> source) {
Properties properties = new Properties();
properties.putAll(source);
return properties;
}
}
@Configuration
public class MailThreadPoolConfig {
@Value("${spring.mail-task.execution.pool.core-size}")
private int corePoolSize;
@Value("${spring.mail-task.execution.pool.max-size}")
private int maxPoolSize;
@Value("${spring.mail-task.execution.pool.queue-capacity}")
private int queueCapacity;
@Value("${spring.mail-task.execution.thread-name-prefix}")
private String namePrefix;
@Value("${spring.mail-task.execution.pool.keep-alive}")
private int keepAliveSeconds;
/**
* 郵件傳送的執行緒池
* @return
*/
@Bean("mailTaskExecutor")
public Executor mailTaskExecutor(){
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
//最大執行緒數
executor.setMaxPoolSize(maxPoolSize);
//核心執行緒數
executor.setCorePoolSize(corePoolSize);
//任務佇列的大小
executor.setQueueCapacity(queueCapacity);
//執行緒字首名
executor.setThreadNamePrefix(namePrefix);
//執行緒存活時間
executor.setKeepAliveSeconds(keepAliveSeconds);
// 設定裝飾器,父子執行緒共用request header變數
executor.setTaskDecorator(new RequestHeaderTaskDecorator());
/**
* 拒絕處理策略
* CallerRunsPolicy():交由呼叫方執行緒執行,比如 main 執行緒。
* AbortPolicy():直接丟擲異常。
* DiscardPolicy():直接丟棄。
* DiscardOldestPolicy():丟棄佇列中最老的任務。
*/
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 執行緒初始化
executor.initialize();
return executor;
}
}
public enum MailResultCodeEnum {
/**
* 預設
*/
SUCCESS("success", "郵件傳送成功"),
/**
* 自定義
*/
ERROR("error", "郵件傳送失敗");
public String code;
public String message;
MailResultCodeEnum(String code, String message) {
this.code = code;
this.message = message;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
public class JavaMailConstant {
/**
* Redis JavaMail設定config key
*/
public static final String MAIL_CONFIG_KEY = "mail:config";
/**
* 當開啟多租戶模式時,Redis JavaMail設定config key
*/
public static final String MAIL_TENANT_CONFIG_KEY = "mail:tenant:config:";
}
@Slf4j
@Configuration
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class GitEggJavaMailConfiguration {
private final JavaMailSenderImpl javaMailSenderImpl;
private final RedisTemplate redisTemplate;
/**
* 是否開啟租戶模式
*/
@Value("${tenant.enable}")
private Boolean enable;
@Bean
public JavaMailSenderFactory gitEggAuthRequestFactory() {
return new JavaMailSenderFactory(redisTemplate, javaMailSenderImpl, enable);
}
}
郵箱伺服器的設定,實際就是不同郵箱渠道的設定,這裡我們將表和欄位設計好,然後使用GitEgg自帶程式碼生成器,生成業務的CRUD程式碼即可。
CREATE TABLE `t_sys_mail_channel` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
`tenant_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '租戶id',
`channel_code` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '渠道編碼',
`channel_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '渠道名稱',
`host` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'SMTP伺服器地址',
`port` int(11) NULL DEFAULT NULL COMMENT 'SMTP伺服器埠',
`username` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '賬戶名',
`password` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '密碼',
`protocol` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'smtp' COMMENT '協定',
`default_encoding` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '預設編碼',
`jndi_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '對談JNDI名稱',
`properties` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'JavaMail 設定',
`channel_status` tinyint(2) NOT NULL DEFAULT 0 COMMENT '渠道狀態 1有效 0禁用',
`md5` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT 'MD5',
`comments` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '描述',
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '建立時間',
`creator` bigint(20) NULL DEFAULT NULL COMMENT '建立者',
`update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新時間',
`operator` bigint(20) NULL DEFAULT NULL COMMENT '更新者',
`del_flag` tinyint(2) NULL DEFAULT 0 COMMENT '是否刪除',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '郵件渠道' ROW_FORMAT = DYNAMIC;
SET FOREIGN_KEY_CHECKS = 1;
郵件模板資料庫表設計:
CREATE TABLE `t_sys_mail_template` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
`tenant_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '租戶id',
`template_code` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '模板編碼',
`template_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '模板名稱',
`sign_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '模板簽名',
`template_status` tinyint(2) NOT NULL DEFAULT 1 COMMENT '模板狀態',
`template_type` tinyint(2) NULL DEFAULT NULL COMMENT '模板型別',
`template_content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '模板內容',
`cache_code_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '快取key',
`cache_time_out` bigint(20) NULL DEFAULT 0 COMMENT '快取有效期 值',
`cache_time_out_unit` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '快取有效期 單位',
`send_times_limit` bigint(20) NULL DEFAULT 0 COMMENT '傳送次數限制',
`send_times_limit_period` bigint(20) NULL DEFAULT 0 COMMENT '限制時間間隔',
`send_times_limit_period_unit` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '限制時間間隔 單位',
`comments` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '描述',
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '建立時間',
`creator` bigint(20) NULL DEFAULT NULL COMMENT '建立者',
`update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新時間',
`operator` bigint(20) NULL DEFAULT NULL COMMENT '更新者',
`del_flag` tinyint(2) NULL DEFAULT 0 COMMENT '是否刪除',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '郵件模板' ROW_FORMAT = DYNAMIC;
SET FOREIGN_KEY_CHECKS = 1;
郵件紀錄檔資料庫表設計:
CREATE TABLE `t_sys_mail_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
`tenant_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '租戶id',
`channel_id` bigint(20) NULL DEFAULT NULL COMMENT 'mail渠道id',
`template_id` bigint(20) NULL DEFAULT NULL COMMENT 'mail模板id',
`mail_subject` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '郵件主題',
`mail_from` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '傳送人',
`mail_to` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '收件人',
`mail_cc` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '抄送',
`mail_bcc` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '密抄送',
`mail_content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '郵件內容',
`attachment_name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '0' COMMENT '附件名稱',
`attachment_size` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '0' COMMENT '附件大小',
`send_time` datetime(0) NULL DEFAULT NULL COMMENT '傳送時間',
`send_result_code` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '1' COMMENT '傳送結果碼',
`send_result_msg` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '傳送結果訊息',
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '建立日期',
`creator` bigint(20) NULL DEFAULT NULL COMMENT '建立者',
`update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新日期',
`operator` bigint(20) NULL DEFAULT NULL COMMENT '更新者',
`del_flag` tinyint(2) NOT NULL DEFAULT 0 COMMENT '是否刪除 1:刪除 0:不刪除',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '郵件記錄' ROW_FORMAT = DYNAMIC;
SET FOREIGN_KEY_CHECKS = 1;
上面的基本功能開發完成之後,那麼我們就需要進行測試,這裡選擇兩種型別的郵箱進行測試,一種是QQ郵箱,還有一種是阿里雲企業郵箱。
QQ郵箱在設定的時候不能使用QQ的登入密碼,需要單獨設定QQ郵箱的授權碼,下面是操作步驟:
阿里雲企業郵箱的設定相比較而言就簡單一些,設定的密碼就是企業郵箱登入的密碼。
mail:
username: XXXXXXXXXXX
password: XXXXXXXXXX
default-encoding: UTF-8
host: smtp.mxhichina.com
port: 25
protocol: smtp
properties:
mail:
smtp:
auth: true
ssl:
enable: false
# 非同步傳送郵件,核心執行緒池數設定
mail-task:
execution:
pool:
core-size: 5
max-size: 10
queue-capacity: 5
keep-alive: 60
thread-name-prefix: mail-send-task-
Gitee: https://gitee.com/wmz1930/GitEgg
GitHub: https://github.com/wmz1930/GitEgg