Springboot 读取模板excel信息内容并发送邮件, 并不是你想想中的那么简单

Springboot 读取模板excel信息内容并发送邮件

    • 背景
    • 技术选型
    • 搭建过程
    • 数据加密
    • 隐藏问题暴露
      • 背景
      • 追溯
      • 解决

背景

在我们日常开发中, 会遇到这样一种场景, 就是读取表格中的数据, 并将数据以附件的形式通过邮箱发送到表格中的每个人 即: excel 读取+ excel 写入+ 发送邮件(携带附件), 例如: 公司在做工资单发送功能时, 财务将所有人的工资单excel上传, 后台通过excel 读取, 然后将每个人的工资信息写入到一个excel, 最后以邮件的形式发送. 为了应对这一场景, 我们来进行技术选型. 然而功能实现了, 使用就没有问题吗? 通过对后续暴露问题的分析来体会下利用技术实现功能往往是开发的第一步, 后面仍需要我们根据具体的软硬件情况对代码进行优化.

技术选型

  • excel文件读取和写入: easyexcel 社区活跃度, 可写入数据条数以及可并发量都不错, 因此采用easy
  • 邮箱发送: spring-boot-starter-mail Spring官方集成的, 底层是jakarta-mail, 与Springboot兼容性较好
  • 信息加密: jasypt 隐藏需求, 需要对邮箱的pop3密码进行加密

搭建过程

首先以无加密方式搭建

相关jar

代码语言:javascript
复制
        <!--EasyExcel-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>easyexcel</artifactId>
            <version>${easyexcel.version}</version>
        </dependency>
        <!--开启邮箱验证 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-mail</artifactId>
        </dependency>
        <!--jasypt加密字符串-->
        <dependency>
            <groupId>com.github.ulisesbocchio</groupId>
            <artifactId>jasypt-spring-boot</artifactId>
            <version>${jasypt-spring-boot.version}</version>
        </dependency>

配置文件进行配置

代码语言:javascript
复制
#邮箱配置
spring.mail.host=邮箱所在服务器域名/ip地址
spring.mail.username=邮箱账号
spring.mail.password=邮箱密码
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.starttls.required=true

Excel 数据列列名实体 @ExcelPropertyindex 属性用于文件读取时, 指定读取的列, 而 value 用于在列写入时, 指定列的表头. 采取 value = {"序号", "序号"} 是因为存在复合表头, 这里需要根据自己业务具体情况去编写

在这里插入图片描述
代码语言:javascript
复制
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.format.DateTimeFormat;
import lombok.Data;

import java.io.Serializable;

/**

  • info: 工资单实体

  • @Author chy
    */
    @Data
    public class WagesDTO {

    @ExcelProperty(value = {"序号", "序号"}, index = 0)
    private Integer id;

    @ExcelProperty(value = {"月份", "月份"}, index = 1)
    private Integer mounth;

    @ExcelProperty(value = {"部门", "部门"}, index = 2)
    private String deptName;

    @ExcelProperty(value = {"工号", "工号"}, index = 3)
    private String jobNumber;

    @ExcelProperty(value = {"姓名", "姓名"}, index = 4)
    private String name;
    /**

    • 入职时间
      */
      @DateTimeFormat("yyyy-MM-dd HH:mm:ss")
      @ExcelProperty(value = {"入职时间", "入职时间"}, index = 5)
      private String entryTime;

    @ExcelProperty(value = {"岗位", "岗位"}, index = 6)
    private String position;
    /**

    • 出勤
      */
      @ExcelProperty(value = {"出勤", "出勤"}, index = 7)
      private String attendance;

    @ExcelProperty(value = {"基本工资", "固定工资"}, index = 8)
    private Double fixedSalary;

    @ExcelProperty(value = {"基本工资", "工龄"}, index = 9)
    private Double workAge;
    /**

    • 岗位绩效
      /
      @ExcelProperty(value = {"岗位绩效", "岗位绩效"},index = 10)
      private Double achievements;
      /
      *
    • 考核评分
      /
      @ExcelProperty(value = {"考核评分", "考核评分"}, index = 11)
      private Integer assessmentScore;
      /
      *
    • 考评绩效
      /
      @ExcelProperty(value = {"考评绩效", "考评绩效"}, index = 12)
      private Double evaluatePerformance;
      /
      *
    • 转正
      /
      @ExcelProperty(value = {"转正", "转正"}, index = 13)
      private Double become;
      /
      *
    • 补贴
      /
      @ExcelProperty(value = {"补贴", "补贴"}, index = 14)
      private Double subsidy;
      /
      *
    • 加班
      /
      @ExcelProperty(value = {"加班", "加班"}, index = 15)
      private Double workExtra;
      /
      *
    • 津贴及其他
      /
      @ExcelProperty(value = {"津贴及其他","津贴及其他"}, index = 16)
      private Double otherSalary;
      /
      *
    • 缺勤及其他
      /
      @ExcelProperty(value = {"缺勤及其他", "缺勤及其他"}, index = 17)
      private Double absenceFromDuty;
      /
      *
    • 应得工资
      /
      @ExcelProperty(value = {"应得工资", "应得工资"}, index = 18)
      private Double observeSalary;
      /
      *
    • 养老
      /
      @ExcelProperty(value = {"扣除款项", "养老"}, index = 19)
      private Double elderlyCare;
      /
      *
    • 医保
      /
      @ExcelProperty(value = {"扣除款项", "医保"}, index = 20)
      private Double medicalInsurance;
      /
      *
    • 失业
      /
      @ExcelProperty(value = {"扣除款项", "失业"}, index = 21)
      private Double lossWork;
      /
      *
    • 大病
      /
      @ExcelProperty(value = {"扣除款项", "大病"}, index = 22)
      private Double seriousIllness;
      /
      *
    • 公积金
      /
      @ExcelProperty(value = {"扣除款项", "公积金"}, index = 23)
      private Double accumulationFund;
      /
      *
    • 累计专项附加扣除
      /
      @ExcelProperty(value = {"扣除款项", "累计专项附加扣除"}, index = 24)
      private Double accumulatedSpecialAdditionalDeduction;
      /
      *
    • 所得税
      /
      @ExcelProperty(value = {"扣除款项", "所得税"}, index = 25)
      private Double incomeTax;
      /
      *
    • 公款
      /
      @ExcelProperty(value = {"扣除款项", "公款"}, index = 26)
      private Double publicFunds;
      /
      *
    • 其他
      /
      @ExcelProperty(value = {"扣除款项", "其他"}, index = 27)
      private Double other;
      /
      *
    • 实发工资
      */
      @ExcelProperty(value = {"实发工资", "实发工资"}, index = 28)
      private Double netSalary;
      }

业务代码

代码语言:javascript
复制
	//==========controller方法
@ApiOperation("文件上传")
@PostMapping("/upload")
public RpcServiceResult upload(@RequestParam("file") MultipartFile file) throws IOException {
return RpcServiceResult.getSuccessResult(wagesService.handle(file));
}

//==========sevice接口
/**

  • 处理
  • @param file
  • @return
    */
    List<WagesDTO> handle(MultipartFile file) throws IOException;

//===========业务实现类
@Service
@Slf4j
public class WagesServiceImpl implements WagesService {

 @Resource
 private JavaMailSender mailSender;
 /**
  *	这里需要在redis中构建, 员工工号和邮箱的联系. 如果用户表中有, 那么直接查询出来即可
  */
 @Resource
 private RedisUtils redisUtils;

 /**
  *
  * 1. 创建excel对应的实体对象 参照{@link WagesDTO}
  * 2. 由于默认一行行的读取excel,所以需要创建excel一行一行的回调监听器,参照{@link EasyExcelStudentListener}
  * 3. 直接读即可
  */
 @Override
 public List&lt;WagesDTO&gt; handle(MultipartFile file) throws IOException {
     //发送人员列表
     List&lt;WagesDTO&gt; dataList = new ArrayList&lt;&gt;();
     //发送失败人员列表
     List&lt;WagesDTO&gt; failuresList = new ArrayList&lt;&gt;();
     AtomicInteger result = new AtomicInteger();
     // 读取excel
     EasyExcel.read(file.getInputStream(), WagesDTO.class, new EasyExcelStudentListener(dataList))
             .sheet()
             .headRowNumber(3)
             .doRead();
     System.out.println(JSONArray.toJSONString(dataList));
     if (CollectionUtils.isEmpty(dataList)) {
         throw new ExcelUploadException(&#34;上传Excel表格内容为空, 请核对后再次上传!&#34;);
     }
     /**
      * 邮件发送失败的三种情况:
      * 1. 找不到工号
      * 2. 找不到邮箱
      * 3. 网络原因导致邮件发送失败
      */
     dataList.forEach(item -&gt; {
         String empName = item.getName();
         Integer mounth = item.getMounth();
         String jobNumber = item.getJobNumber();
         //获取对应邮箱
         String emailName = &#34;&#34;;
         if (StringUtils.isNotBlank(item.getJobNumber()) &amp;&amp; StringUtils.isNotBlank(redisUtils.getCacheObject(BusinessConstant.JOB_NUMBER_EMAIL+&#34;:&#34;+jobNumber))) {
             emailName = redisUtils.getCacheObject(BusinessConstant.JOB_NUMBER_EMAIL + &#34;:&#34; + jobNumber);
             String fileName = empName + &#34;-&#34; + mounth + &#34;月份工资表&#34; + &#34;.xlsx&#34;;
             List&lt;WagesDTO&gt; wagesTempList = new ArrayList(1);
             wagesTempList.add(item);
             try {
                 org.springframework.core.io.Resource resource = new ClassPathResource(&#34;static/&#34; + &#34;工资表模板.xlsx&#34;);
                 //excel文件写入
                 EasyExcel.write(fileName, WagesDTO.class).needHead(false).
                         withTemplate(resource.getInputStream()).sheet().doWrite(wagesTempList);
             } catch (IOException e) {
                 e.printStackTrace();
             }
             //邮箱发送
             MimeMessage mimeMessage = mailSender.createMimeMessage();
             try {
                 MimeMessageHelper messageHelper = new MimeMessageHelper(mimeMessage, true);
                 //接受者邮箱
                 messageHelper.setTo(emailName);
                 //邮箱主题
                 messageHelper.setSubject(fileName.substring(0, fileName.lastIndexOf(&#34;.&#34;)));
                 //发送文字内容
                 messageHelper.setText(empName+&#34;: 您&#34;+ Calendar.getInstance().get(Calendar.YEAR)+&#34;年&#34;+mounth+&#34;月份的工资单已到, 请查收!&#34;);
                 //发送附件
                 messageHelper.addAttachment(fileName, new File(fileName));
                 //发送者邮箱
                 messageHelper.setFrom(&#34;发件人邮箱&#34;);
                 mailSender.send(mimeMessage);
                 result.incrementAndGet();
             } catch (MessagingException e) {
                 failuresList.add(item);
                 e.printStackTrace();
             }
             //发送结束后删除文件对应文件
             FileUtils.delete(new File(fileName));
         }else {
         	//统计失败人员信息
             failuresList.add(item);
         }
     });
     log.info(&#34;\n成功给{}人发送工资单&#34;, result.get());
     log.info(&#34;\n发送失败人数: {}, \n发送失败人员信息{}&#34;, failuresList.size(), failuresList);
     return failuresList;
 }

}

附: redisUtils工具类代码

代码语言:javascript
复制
package com.sxd.mis.util;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.sxd.mis.constant.BusinessConstant;
import com.sxd.mis.entity.dto.UserDTO;
import com.sxd.mis.entity.po.UserPO;
import com.sxd.mis.exception.UselessTokenException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.*;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;

import javax.annotation.Resource;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;

/**

  • @author cyy

  • 使用此工具类时使用 @Autowired 注解

  • 保存实体类时 实体类需要实现implements Serializable 接口 不然会报序列化错误
    */
    @Component
    public class RedisUtils {

    @Resource
    private RedisTemplate redisTemplate;
    @Value("${ding.params.appkey}")
    public String appKey;

    /**

    • 缓存基本的对象,Integer、String、实体类等
    • @param key 缓存的键值
    • @param value 缓存的值
    • @return 缓存的对象
      */
      public <T> ValueOperations<String, T> setCacheObject(String key, T value)
      {
      ValueOperations<String, T> operation = redisTemplate.opsForValue();
      operation.set(key, value);
      return operation;
      }

    /**

    • 缓存基本的对象,Integer、String、实体类等
    • @param key 缓存的键值
    • @param value 缓存的值
    • @param timeout 时间
    • @param timeUnit 时间颗粒度
    • @return 缓存的对象
      */
      public <T> ValueOperations<String, T> setCacheObject(String key, T value, Integer timeout, TimeUnit timeUnit)
      {
      ValueOperations<String, T> operation = redisTemplate.opsForValue();
      operation.set(key, value, timeout, timeUnit);
      return operation;
      }

    /**

    • 获得缓存的基本对象。
    • @param key 缓存键值
    • @return 缓存键值对应的数据
      */
      public <T> T getCacheObject(String key)
      {
      ValueOperations<String, T> operation = redisTemplate.opsForValue();
      return operation.get(key);
      }

    /**

    • 删除单个对象
    • @param key
      */
      public void deleteObject(String key)
      {
      redisTemplate.delete(key);
      }

    /**

    • 根据key前缀批量删除
    • @param keyPrefix 键前缀字符串
    • @return 结果
      /
      public boolean delAll(String keyPrefix) {
      if (keyPrefix != null) {
      Set<String> keys = redisTemplate.keys(Pattern.matches("\
      $", keyPrefix) ? keyPrefix : keyPrefix + "*");
      redisTemplate.delete(keys);
      return true;
      }
      return false;
      }

    /**

    • 删除集合对象
    • @param collection
      */
      public void deleteObject(Collection collection)
      {
      redisTemplate.delete(collection);
      }

    /**

    • 缓存List数据
    • @param key 缓存的键值
    • @param dataList 待缓存的List数据
    • @return 缓存的对象
      */
      public <T> ListOperations<String, T> setCacheList(String key, List<T> dataList)
      {
      ListOperations listOperation = redisTemplate.opsForList();
      if (null != dataList)
      {
      int size = dataList.size();
      for (int i = 0; i < size; i++)
      {
      listOperation.leftPush(key, dataList.get(i));
      }
      }
      return listOperation;
      }

    /**

    • 获得缓存的list对象

    • @param key 缓存的键值

    • @return 缓存键值对应的数据
      */
      public <T> List<T> getCacheList(String key)
      {
      List<T> dataList = new ArrayList<T>();
      ListOperations<String, T> listOperation = redisTemplate.opsForList();
      Long size = listOperation.size(key);

      for (int i = 0; i < size; i++)
      {
      dataList.add(listOperation.index(key, i));
      }
      return dataList;
      }

    /**

    • 缓存Set
    • @param key 缓存键值
    • @param dataSet 缓存的数据
    • @return 缓存数据的对象
      */
      public <T> BoundSetOperations<String, T> setCacheSet(String key, Set<T> dataSet)
      {
      BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
      Iterator<T> it = dataSet.iterator();
      while (it.hasNext())
      {
      setOperation.add(it.next());
      }
      return setOperation;
      }

    /**

    • 获得缓存的set
    • @param key
    • @return
      */
      public <T> Set<T> getCacheSet(String key)
      {
      Set<T> dataSet = new HashSet<T>();
      BoundSetOperations<String, T> operation = redisTemplate.boundSetOps(key);
      dataSet = operation.members();
      return dataSet;
      }

    /**

    • 缓存Map
    • @param key
    • @param dataMap
    • @return
      */
      public <T> HashOperations<String, String, T> setCacheMap(String key, Map<String, T> dataMap)
      {
      HashOperations hashOperations = redisTemplate.opsForHash();
      if (null != dataMap)
      {
      for (Map.Entry<String, T> entry : dataMap.entrySet())
      {
      hashOperations.put(key, entry.getKey(), entry.getValue());
      }
      }
      return hashOperations;
      }

    /**

    • 获得缓存的Map
    • @param key
    • @return
      */
      public <T> Map<String, T> getCacheMap(String key)
      {
      Map<String, T> map = redisTemplate.opsForHash().entries(key);
      return map;
      }

    /**

    • 获得缓存的基本对象列表
    • @param pattern 字符串前缀
    • @return 对象列表
      */
      public Collection<String> keys(String pattern)
      {
      return redisTemplate.keys(pattern);
      }
      }

//========================需要添加的pom文件
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

数据加密

利用jasypt 对项目配置文件中, 敏感信息进行加密.
Jasypt 是一个 Java 库,它允许开发人员以最小的努力为项目添加基本的加密功能,而无需深入了解密码学的工作原理.

使用步骤

引入jar

代码语言:javascript
复制
        <!--jasypt加密字符串-->
        <dependency>
            <groupId>com.github.ulisesbocchio</groupId>
            <artifactId>jasypt-spring-boot</artifactId>
            <version>2.0.0</version>
        </dependency>

启动类使用 @EnableEncryptableProperties

敏感信息加密 引入jar坐标之后, 找到所下载的位置, 如果使用的是idea, 默认jar存储路径在 C:\Users\Administrator\.m2\repository\org\jasypt\jasypt\1.9.2

利用jar进行加密 进入命令行, 输入java -cp命令

代码语言:javascript
复制
java -cp jasypt-1.9.2.jar  org.jasypt.intf.cli.JasyptPBEStringEncryptionCLI input="test" password=salt algorithm=PBEWithMD5AndDES

-- input参数:你想要加密的密码
-- password参数:jasypt用来加密你的密码的密码
-- output: 输出的参数就是你用于替代原明文密码的字符串!!!

在这里插入图片描述

对配置文件中的邮箱密码(pop3)进行加密

代码语言:javascript
复制
spring.mail.host=邮箱所在服务器域名/ip地址
spring.mail.username=邮箱账号
spring.mail.password=ENC(xcGyDdk8DOlDMOW0ij3k5A==)
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.starttls.required=true
#jasypt加密配置
jasypt.encryptor.password=salt

隐藏问题暴露

背景

在测试上述技术时, 由于当初使用的是腾讯企业邮箱, 在开发自测以及测试小规模测试之后并未发现问题. 但是在项目发布到生产环境之后问题方才暴露出来. 那是一个周五的晚上. 收到消息的我真的是血压突突上涨…

在这里插入图片描述

追溯

  1. 好在我也是老鸟了, 马上就冷静下来, 询问发送情况, 当时成功人数未知且前端服务一直没有获取到后端的响应. 由于涉及到生产环境日志, 只能初步判断应该是邮箱那边的限制. 在周一的时候, 在相关人员的帮忙下拿到了生产环境的日志.
在这里插入图片描述
  1. 从日志这里可以判断出连接被smtp服务器关闭了. 我第一反应就是为什么会关闭? 然后去搜索相关相关内容未果. 因此问题又回到我之前的推测上. 而和腾讯邮箱那边的客服佐证了我的推测
在这里插入图片描述
在这里插入图片描述
  1. 通过和客服的对话我们可以知道, 腾讯的发送邮箱是有限制的, 也就是说: 单个邮箱账号发送邮件需要满足频率不超过 10封/min, 1000封/天. 而上面那种写法是通过spring自带的邮箱api建立连接之后, 一直发送邮件直到超过每分钟发送数限制后smtp服务端阻塞线程, 待下一分钟继续发送, 当超过smtp服务器规定的最大连接时间(推测大概为120s左右)之后就会强制断开连接.最终导致邮件发送失败.
  2. 分析到这里, 我们就可以对现有业务进行优化, 首先针对业务长时间未返回, 我们可以将同步操作改为异步操作. 读取Excel表格并验证邮箱之后, 直接进行返回. 然后针对smtp服务器超时断开连接的情况, 我的处理是: 开启多线程, 用于专门处理邮件发送操作, 并且每次发送邮件都手动开启和断开连接, 每次发送之后休眠6秒, 保证一分钟最多发10封邮件. 因此, 基于以上逻辑改造原有代码如下:

解决

同步改异步, 长连接改为短连接

修改主业务流程类

代码语言:javascript
复制
    @Resource
    private SendmailUtil sendMailUtils;
@Override
public Map&lt;String, Object&gt; handle(MultipartFile file, String content) throws IOException {
    String suffix = file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf(&#34;.&#34;) + 1);
    if (!(suffix.equals(&#34;xlsx&#34;) || suffix.equals(&#34;xls&#34;))) {
        throw new BusinessException(&#34;上传文件格式有误!&#34;);
    }
    Map&lt;String, Object&gt; resultMap = Maps.newHashMap();
    //发送人员列表
    List&lt;WagesDTO&gt; dataList = new LinkedList&lt;&gt;();
    //发送失败人员列表
    List&lt;WagesDTO&gt; failDtoList = new LinkedList&lt;&gt;();
    // 读取excel
    EasyExcel.read(file.getInputStream(), WagesDTO.class, new EasyExcelStudentListener(dataList))
            .sheet()
            .headRowNumber(3)
            .doRead();
    if (CollectionUtils.isEmpty(dataList)) {
        throw new ExcelUploadException(&#34;上传Excel表格内容为空, 请核对后再次上传!&#34;);
    }
    //验证邮箱是否存在, 存在则返回给前端, 不存在则提示失败
    AtomicInteger successCount = new AtomicInteger(0);
    Map&lt;String, WagesDTO&gt; emailAndWagesInfoMap = Maps.newLinkedHashMap();
    for (WagesDTO item : dataList) {
        String empName = item.getName();
        String jobNumber = item.getJobNumber();
        //获取对应邮箱
        String emailName = &#34;&#34;;
        if (StringUtils.isNotBlank(item.getJobNumber()) &amp;&amp; StringUtils.isNotBlank(redisUtils.getCacheObject(BusinessConstant.JOB_NUMBER_EMAIL + &#34;:&#34; + jobNumber + empName))) {
            emailName = redisUtils.getCacheObject(BusinessConstant.JOB_NUMBER_EMAIL + &#34;:&#34; + jobNumber + empName);
            if (StringUtils.isNotBlank(emailName)) {
                emailAndWagesInfoMap.put(emailName, item);
                successCount.incrementAndGet();
                }
            }else {
            failDtoList.add(item);
        }
    }
    //将邮箱发送给对应人员
    sendMailToEmployees(content, emailAndWagesInfoMap);
    log.info(&#34;\n成功给{}人发送&#34;, successCount.get());
    log.info(&#34;\n发送失败人数: {}, \n发送失败人员信息{}&#34;, failDtoList.size(), failDtoList);
    resultMap.put(&#34;successCount&#34;, successCount.get());
    resultMap.put(&#34;failList&#34;, failDtoList);
    return resultMap;
}</code></pre></div></div><p>异步线程类</p><blockquote><p> 用于发送邮件

代码语言:javascript
复制
  /**
     *
     * @param content   邮箱内容说明
     * @param emailAndWagesInfoMap   发送邮件的集合体
     * @param
     */
    private void sendMailToEmployees(String content, Map<String, WagesDTO> emailAndWagesInfoMap) {
        ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("thread-pool-sendMailToEmployees-%d").build();
        ExecutorService singleThreadPool = new ThreadPoolExecutor(1, 1, 60L, TimeUnit.MINUTES,
                new LinkedBlockingQueue<Runnable>(16), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());
        singleThreadPool.execute(() -> {
            //邮件发送失败的列表
            Map<String, WagesDTO> failMap = Maps.newLinkedHashMap();
            /**
             * 邮件发送失败的三种情况:
             * 1. 找不到工号
             * 2. 找不到邮箱
             * 3. 网络原因导致邮件发送失败
             */
            AtomicInteger successCount = new AtomicInteger(0);
            emailAndWagesInfoMap.forEach((email,wagesDto)->{
                String empName = wagesDto.getName();
                Integer mounth = wagesDto.getMounth();
                //获取对应邮箱
                if (StringUtils.isNotBlank(wagesDto.getJobNumber())) {
                    String fileName = empName + "-" + mounth + "月份数据" + ".xlsx";
                    List<WagesDTO> wagesTempList = new ArrayList(1);
                    wagesTempList.add(wagesDto);
                    try {
                        org.springframework.core.io.Resource resource = new ClassPathResource("static/" + "模板.xlsx");
                        EasyExcel.write(fileName, WagesDTO.class).needHead(false).
                                withTemplate(resource.getInputStream()).sheet().doWrite(wagesTempList);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                    /**
                     * 邮件单发
                     * @param toEmailAddress 收件箱地址
                     * @param emailTitle 邮件主题
                     * @param emailContent 邮件内容
                     * @param fileName   附件名称
                     */
                    String emailTitle = fileName.substring(0, fileName.lastIndexOf("."));
                    String emailContent = empName + ": 您" + mounth + "月份数据已发送, 请查收! " + content;
                    try {
                        sendMailUtils.sendEmail(email, emailTitle, emailContent, fileName);
                        successCount.incrementAndGet();
                        log.info("step" + successCount.get() + ": 向" + empName + "发送邮件");
                        Thread.sleep(6);
                    } catch (Exception e) {
                        failMap.put(email, wagesDto);
                        e.printStackTrace();
                    }
                    FileUtils.delete(new File(fileName));
                } else {
                    failMap.put(email, wagesDto);
                }
            });
            if (!CollectionUtils.isEmpty(failMap)) {
                log.info("存在发送人间失败的人,重新进行发送");
				//这里可以丢给redis或者消息队列进行处理
            }
        });
        singleThreadPool.shutdown();
    }

邮件发送工具类

实现手动创建连接, 发送邮件, 关闭连接操作

代码语言:javascript
复制
import javax.activation.DataHandler;
import javax.activation.DataSource;
import javax.activation.FileDataSource;
import javax.mail.Address;
import javax.mail.Message;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.*;
import com.sun.mail.util.MailSSLSocketFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.io.File;
import java.util.Properties;

/**

  • info:

  • @Author caoHaiYang

  • @Date 2023/2/21 19:18
    */
    @Component
    public class SendmailUtil {

    /**

    • 邮件服务器主机名
      /
      @Value("${spring.mail.host}")
      private String myEmailSMTPHost;
      /
      *
    • 发件人邮箱
      /
      @Value("${spring.mail.username}")
      private String myEmailAccount;
      /
      *
    • 在开启SMTP服务时会获取到一个授权码,把授权码填在这里
      */
      @Value("${spring.mail.password}")
      private String myEmailPassword;

    /**

    • 邮件单发

    • @param toEmailAddress 收件箱地址

    • @param emailTitle 邮件主题

    • @param emailContent 邮件内容

    • @param fileName 附件名称

    • @throws Exception
      */
      public void sendEmail(String toEmailAddress, String emailTitle, String emailContent, String fileName) throws Exception {
      Properties props = new Properties();
      // 开启debug调试(如果遇到邮箱发送失败时可开启)
      // props.setProperty("mail.debug", "true");
      // 发送服务器需要身份验证
      props.setProperty("mail.smtp.auth", "true");
      // 端口号
      props.put("mail.smtp.port", 465);
      //设置邮件服务器主机名
      props.setProperty("mail.smtp.host", myEmailSMTPHost);
      // 发送邮件协议名称
      props.setProperty("mail.transport.protocol", "smtp");
      /SSL认证,注意腾讯邮箱是基于SSL加密的,所以需要开启才可以使用/
      MailSSLSocketFactory sf = new MailSSLSocketFactory();
      sf.setTrustAllHosts(true);
      //设置是否使用ssl安全连接(一般都使用)
      props.put("mail.smtp.ssl.enable", "true");
      props.put("mail.smtp.ssl.socketFactory", sf);

      //创建会话
      Session session = Session.getInstance(props);
      //获取邮件对象
      //发送的消息,基于观察者模式进行设计的
      Message msg = new MimeMessage(session);
      //设置邮件标题
      msg.setSubject(emailTitle);

      //向multipart对象中添加邮件的各个部分内容,包括文本内容和附件
      MimeMultipart multipart = new MimeMultipart();
      //设置邮件的文本内容
      MimeBodyPart contentPart = new MimeBodyPart();
      contentPart.setContent(emailContent, "text/html;charset=UTF-8");
      multipart.addBodyPart(contentPart);
      //添加附件
      MimeBodyPart filePart = new MimeBodyPart();
      DataSource source = new FileDataSource(fileName);
      //添加附件的内容
      filePart.setDataHandler(new DataHandler(source));
      //添加附件的标题
      filePart.setFileName(MimeUtility.encodeText(fileName));
      multipart.addBodyPart(filePart);
      multipart.setSubType("mixed");
      //将multipart对象放到message中
      msg.setContent(multipart);

      //设置发件人邮箱
      // InternetAddress 的三个参数分别为: 发件人邮箱, 显示的昵称(只用于显示, 没有特别的要求), 昵称的字符集编码
      String nickName = myEmailAccount.split("@")[0];
      msg.setFrom(new InternetAddress(myEmailAccount, nickName, "UTF-8"));
      //得到邮差对象
      Transport transport = session.getTransport();
      //连接自己的邮箱账户
      //密码不是自己QQ邮箱的密码,而是在开启SMTP服务时所获取到的授权码
      //connect(host, user, password)
      transport.connect(myEmailSMTPHost, myEmailAccount, myEmailPassword);
      //发送邮件
      transport.sendMessage(msg, new Address[]{new InternetAddress(toEmailAddress)});
      transport.close();
      }
      }


通过对问题的深入挖掘和分析最终解决了问题, 由此可见在不少场景下, 仅仅实现功能是不够的,
还需要我们结合实际情况对业务交互方式进行修改. 例如同步改异步, 串行改并行, 立即执行与延迟执行, 长短连接的取舍等等…
让用户体验良好, 就需要后端同学多做功课, 给予前端快速响应. 无论是异步执行还是接口性能优化, 都需要我们具体情况具体分析.
学无止境, 我们下次再见!!!

更多jasypt的配置可见 小白入门之 Jasypt 加密和解密