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

news2024/11/25 15:57:49

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

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

背景

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

技术选型

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

搭建过程

首先以无加密方式搭建

  1. 相关jar

            <!--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>
    
  2. 配置文件进行配置

    #邮箱配置
    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
    
  3. Excel 数据列列名实体
    @ExcelPropertyindex 属性用于文件读取时, 指定读取的列, 而 value 用于在列写入时, 指定列的表头.
    采取 value = {"序号", "序号"} 是因为存在复合表头, 这里需要根据自己业务具体情况去编写
    在这里插入图片描述

    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;
    }
    
    
  4. 业务代码

    	//==========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<WagesDTO> handle(MultipartFile file) throws IOException {
    	        //发送人员列表
    	        List<WagesDTO> dataList = new ArrayList<>();
    	        //发送失败人员列表
    	        List<WagesDTO> failuresList = new ArrayList<>();
    	        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("上传Excel表格内容为空, 请核对后再次上传!");
    	        }
    	        /**
    	         * 邮件发送失败的三种情况:
    	         * 1. 找不到工号
    	         * 2. 找不到邮箱
    	         * 3. 网络原因导致邮件发送失败
    	         */
    	        dataList.forEach(item -> {
    	            String empName = item.getName();
    	            Integer mounth = item.getMounth();
    	            String jobNumber = item.getJobNumber();
    	            //获取对应邮箱
    	            String emailName = "";
    	            if (StringUtils.isNotBlank(item.getJobNumber()) && StringUtils.isNotBlank(redisUtils.getCacheObject(BusinessConstant.JOB_NUMBER_EMAIL+":"+jobNumber))) {
    	                emailName = redisUtils.getCacheObject(BusinessConstant.JOB_NUMBER_EMAIL + ":" + jobNumber);
    	                String fileName = empName + "-" + mounth + "月份工资表" + ".xlsx";
    	                List<WagesDTO> wagesTempList = new ArrayList(1);
    	                wagesTempList.add(item);
    	                try {
    	                    org.springframework.core.io.Resource resource = new ClassPathResource("static/" + "工资表模板.xlsx");
    	                    //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(".")));
    	                    //发送文字内容
    	                    messageHelper.setText(empName+": 您"+ Calendar.getInstance().get(Calendar.YEAR)+"年"+mounth+"月份的工资单已到, 请查收!");
    	                    //发送附件
    	                    messageHelper.addAttachment(fileName, new File(fileName));
    	                    //发送者邮箱
    	                    messageHelper.setFrom("发件人邮箱");
    	                    mailSender.send(mimeMessage);
    	                    result.incrementAndGet();
    	                } catch (MessagingException e) {
    	                    failuresList.add(item);
    	                    e.printStackTrace();
    	                }
    	                //发送结束后删除文件对应文件
    	                FileUtils.delete(new File(fileName));
    	            }else {
    	            	//统计失败人员信息
    	                failuresList.add(item);
    	            }
    	        });
    	        log.info("\n成功给{}人发送工资单", result.get());
    	        log.info("\n发送失败人数: {}, \n发送失败人员信息{}", failuresList.size(), failuresList);
    	        return failuresList;
    	    }
    	
    	   
    	}
    
    
  5. 附: redisUtils工具类代码

    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 库,它允许开发人员以最小的努力为项目添加基本的加密功能,而无需深入了解密码学的工作原理.

使用步骤

  1. 引入jar

            <!--jasypt加密字符串-->
            <dependency>
                <groupId>com.github.ulisesbocchio</groupId>
                <artifactId>jasypt-spring-boot</artifactId>
                <version>2.0.0</version>
            </dependency>
    
  2. 启动类使用 @EnableEncryptableProperties

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

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

    java -cp jasypt-1.9.2.jar  org.jasypt.intf.cli.JasyptPBEStringEncryptionCLI input="test" password=salt algorithm=PBEWithMD5AndDES
    
    -- input参数:你想要加密的密码
    -- password参数:jasypt用来加密你的密码的密码
    -- output: 输出的参数就是你用于替代原明文密码的字符串!!!
    

    在这里插入图片描述

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

    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. 好在我也是老鸟了, 马上就冷静下来, 询问发送情况, 当时成功人数未知且前端服务一直没有获取到后端的响应. 由于涉及到生产环境日志, 只能初步判断应该是邮箱那边的限制. 在周一的时候, 在相关人员的帮忙下拿到了生产环境的日志.
    在这里插入图片描述

  2. 从日志这里可以判断出连接被smtp服务器关闭了. 我第一反应就是为什么会关闭? 然后去搜索相关相关内容未果. 因此问题又回到我之前的推测上. 而和腾讯邮箱那边的客服佐证了我的推测
    在这里插入图片描述
    在这里插入图片描述

  3. 通过和客服的对话我们可以知道, 腾讯的发送邮箱是有限制的, 也就是说: 单个邮箱账号发送邮件需要满足频率不超过 10封/min, 1000封/天. 而上面那种写法是通过spring自带的邮箱api建立连接之后, 一直发送邮件直到超过每分钟发送数限制后smtp服务端阻塞线程, 待下一分钟继续发送, 当超过smtp服务器规定的最大连接时间(推测大概为120s左右)之后就会强制断开连接.最终导致邮件发送失败.

  4. 分析到这里, 我们就可以对现有业务进行优化, 首先针对业务长时间未返回, 我们可以将同步操作改为异步操作. 读取Excel表格并验证邮箱之后, 直接进行返回. 然后针对smtp服务器超时断开连接的情况, 我的处理是: 开启多线程, 用于专门处理邮件发送操作, 并且每次发送邮件都手动开启和断开连接, 每次发送之后休眠6秒, 保证一分钟最多发10封邮件. 因此, 基于以上逻辑改造原有代码如下:

解决

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

  1. 修改主业务流程类

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

    用于发送邮件

      /**
         *
         * @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();
        }
    
  3. 邮件发送工具类

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

    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 加密和解密

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/393773.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Volsdf Sampling algorithm

l论文作者开发一个算法计算抽样S方程中使用 I(c,v)≈I^S(c,v)∑i1m−1τ^iLiI(\boldsymbol{c}, \boldsymbol{v}) \approx \hat{I}_{\mathcal{S}}(\boldsymbol{c}, \boldsymbol{v})\sum_{i1}^{m-1} \hat{\tau}_{i} L_{i} I(c,v)≈I^S​(c,v)i1∑m−1​τ^i​Li​ 首先是通过利用…

小区业主入户安检小程序开发

小区业主入户安检小程序开发 可针对不同行业自定义安检项目&#xff0c;线下安检&#xff0c;线上留存&#xff08;安检拍照/录像&#xff09;&#xff0c;提高安检人员安检效率 功能特性&#xff0c;为你介绍小区入户安检系统的功能特性。 小区管理;后台可添加需要安检的小区…

LeetCode-96. 不同的二叉搜索树

题目来源 96. 不同的二叉搜索树 递归 1.我们要知道二叉搜索树的性质&#xff0c;对于一个二叉搜索树&#xff0c;其 【左边的节点值 < 中间的节点值 < 右边的节点值】&#xff0c;也就是说&#xff0c;对于一个二叉搜索树&#xff0c;其中序遍历之后形成的数组应该是一…

分布式系统中的补偿机制设计问题

我们知道&#xff0c;应用系统在分布式的情况下&#xff0c;在通信时会有着一个显著的问题&#xff0c;即一个业务流程往往需要组合一组服务&#xff0c;且单单一次通信可能会经过 DNS 服务&#xff0c;网卡、交换机、路由器、负载均衡等设备&#xff0c;而这些服务于设备都不一…

C++:初识函数模板和类模板

目录 一. 泛型编程 二. 函数模板 2.1 什么是函数模板 2.2 函数模板的实例化 2.2.1 函数模板的隐式实例化 2.2.1 函数模板的显示实例化 2.3 函数模板实例化的原理 2.4 模板函数调用实例化原则 三. 类模板 3.1 什么是类模板 3.2 类模板的实例化 一. 泛型编程 泛型编程…

Qt广告机客户端(下位机)

目录功能结构adClient.promain.cppadclient.h 客户端adclient.cpp 客户端addate.h 时间处理addate.cpp 时间处理adsocket.h 客户端Socket处理adsocket.cpp 客户端Socket处理weather.h 天气信息处理weather.cpp 天气信息处理rollmassege.h 滚动信息处理rollmassege.cpp 滚动信息…

DCC数字管护生命周期模型解读

实话说&#xff0c;对于Digital Curation笔者真心不知道应该怎么翻译。本文借用了钱毅老师的观点&#xff0c;姑且翻译成“数字管护”&#xff0c;详见《从保护到管护&#xff1a;对象变迁视角下的档案保管思想演变》&#xff08;《档案学通讯》&#xff0c;2022年第2期&#x…

数据库基本功之SQL的数据类型

1.四种基本的常用数据类型 1.1 字符型 char # 固定字符,最长2000个 varchar2 # 可变长字符,最长4000个,最小值是1 nchar/nvarchar2 # 类型的列使用国家字符集 raw & long raw # 固定/可变长度的二进制数据长度 最2G,可存放多媒体图象声音等.(老类型,逐步淘汰) LONG …

浅谈CSRF跨域读取型漏洞之JSONP劫持

目录 前提知识 CSRF JSONP jsonp漏洞 原理 过程 复现 漏洞挖掘思路 漏洞防御 前提知识 CSRF 提起CSRF&#xff0c;可能很多人都会想到修改个人资料、授权登陆等攻击场景&#xff0c;可以发现这两个场景都是写入型的CSRF漏洞&#xff0c;通常会忽视更常见的读取型的CS…

MP与IP-Trunk技术讲解

目录 PPP MP技术 将PPP链路直接绑定到VT上实现MP 按照PPP链路用户名查找VT实现MP IP-Trunk技术 PPP MP技术 MP&#xff08;MultiLink PPP&#xff09;将多个PPP链路捆绑使用的技术&#xff08;Serial接口、POS接口等&#xff09; 实现方式 可以采用虚拟VT接口实现MP PPP链路…

通过canvas画出爱心图案,表达你的爱意!

通过canvas画出爱心图案&#xff0c;浏览器可以使用以下js代码&#xff0c;新建对象时&#xff0c;会自动呈现动画效果&#xff0c;代码文末可下载。 点击免费下载源码 let HeartCanvas new HeartCanvas() /*** 爱心* Heart Canvas*/class HeartCanvas {/*** param hMin 颜…

怎么恢复本地磁盘里的数据?电脑本地磁盘数据恢复7种方案

演示机型&#xff1a;技嘉 H310M HD22.0系统版本&#xff1a;Windows 10 专业版软件版本&#xff1a;云骑士数据恢复软件3.21.0.17本地磁盘是什么意思&#xff1f;所谓的本地磁盘是指安装在电脑主板上&#xff0c;不能随便拔插的硬盘&#xff0c;通俗易懂的讲就是电脑内部安装的…

Spring Cloud融合gateway构建的API网关服务 | Spring Cloud 12

一、Spring Cloud Gateway 1.1 概述 所谓的网关就是指系统的统一入口&#xff0c;它封装了运用程序的内部结构&#xff0c;为客户端提供统一的服务&#xff0c;一些与业务功能无关的公共逻辑可以在这里实现&#xff0c;诸如认证、鉴权、监控、路由转发等。 Spring Cloud Gat…

北斗导航 | PPP-RTK:CLASLIB 0.7.2 版本中文手册(CLASLIB ver. 0.7.2 Manual)

===================================================== github:https://github.com/MichaelBeechan CSDN:https://blog.csdn.net/u011344545 ===================================================== CLASLIB ver. 0.7.2 Manual

Hevc变换系数扫描

量化后变换系数的熵编码在整个熵编码中占有举足轻重的地位&#xff0c;由于量化后变换系数大多为零值或者幅度较小的值&#xff0c;如何有效利用这一特性是熵编码的关键环节&#xff0c;H265/HEVC标准中&#xff0c;亮度数据和色度数据均以变换块TB为单位&#xff0c;通过编码非…

Compose 动画 (四) : AnimatedVisibility 各种入场和出场动画效果

AnimatedVisibility中的EnterTransition 和 ExitTransition &#xff0c;用来配置入场/出场时候的动画效果。 默认的入场效果是 fadeIn() expandVertically() 默认的出场效果是 fadeOut() shrinkVertically() 1. EnterTransition和ExitTransition支持的动画 enter的参数类…

【VUE】vue3.0后台常用模板

vue3.0后台常用模板&#xff1a; 1、vue-admin-perfect 在线预览 gitee国内访问地址&#xff1a;https://yuanzbz.gitee.io/vue-admin-perfect/#/home github site : https://zouzhibin.github.io/vue-admin-perfect/ 基础功能版本预览&#xff1a;https://yuanzbz.gitee.io/…

上海亚商投顾:沪指失守3300点 两市上涨股不足500只

上海亚商投顾前言&#xff1a;无惧大盘涨跌&#xff0c;解密龙虎榜资金&#xff0c;跟踪一线游资和机构资金动向&#xff0c;识别短期热点和强势个股。市场情绪沪指今日冲高回落&#xff0c;午后跌幅扩大至1%&#xff0c;失守3300点关口&#xff0c;深成指、创业板指跌近2%。通…

Springboot 定时任务注入FeignClient

问题引入: 在springboot 项目写了个定时任务,里面有段代码通过Feign 调用远程服务,发现通过接口调用可以程序正常执行, 通过配置定时任务发现定时任务没执行,看日志是报了NP.问题跟踪: 写了个demo 重现以上错误:Api(tags "XXX控制器") RestController RequestMapp…

认识代码之前,请先认识你自己 |《编程人生》

这是我的湛庐课程《给技术人的职场突围课》 &#xff08;链接&#xff09; 的一部分。 这篇文章也是 IT 女神征文活动 的一部分。 《编程人生》是一本优秀程序员的采访集&#xff0c;里面记录了15位世界级编程大师的故事。 我在 发刊词 里面说过&#xff0c;在这个书单课里&am…