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@Slf4jpublic class WagesServiceImpl implements WagesService {@Resourceprivate JavaMailSender mailSender;/*** 这里需要在redis中构建, 员工工号和邮箱的联系. 如果用户表中有, 那么直接查询出来即可*/@Resourceprivate RedisUtils redisUtils;/**** 1. 创建excel对应的实体对象 参照{@link WagesDTO}* 2. 由于默认一行行的读取excel,所以需要创建excel一行一行的回调监听器,参照{@link EasyExcelStudentListener}* 3. 直接读即可*/@Overridepublic List<WagesDTO> handle(MultipartFile file) throws IOException {//发送人员列表List<WagesDTO> dataList = new ArrayList<>();//发送失败人员列表List<WagesDTO> failuresList = new ArrayList<>();AtomicInteger result = new AtomicInteger();// 读取excelEasyExcel.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 {@Resourceprivate 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. 修改主业务流程类

        @Resourceprivate SendmailUtil sendMailUtils;@Overridepublic 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<>();// 读取excelEasyExcel.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 加密和解密

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

  1. layui 读取本地excel内容_Python操作Excel基础(1)

    本节利用包openpyxl,实现Excel文件的读取.写入,以及创建Excel文件的基础操作. 操作逻辑 首先,生成一个Workbook对象,这个对象相当于是一个在内存中的Excel文件,它可以包含多 ...

  2. 组策略中的审核策略提示 Windows无法读取模板信息的解决方法

    今天在帮客户配置2003的时候,进入组策略提示Windows无法读取模板信息,原来是因为安装设置过程中删除了guest账号,下面脚本之家小编为大家分享下解决方法吧 若组策略出现"window ...

  3. SpringBoot读取excel表格

    文章目录 SpringBoot读取excel表格 pom.xml依赖 POIUtils工具类 controller测试 注意问题 SpringBoot读取excel表格 共同探讨,向各位大佬学习 走向 ...

  4. jxls读取模板导出Excel学习笔记

    jxls读取模板导出Excel学习笔记 ​ jxls是一个简单的.轻量级的excel导出库,使用特定的标记在excel模板文件中来定义输出格式和布局.除此以外,java中成熟的excel导出工具有po ...

  5. springboot项目导出excel 合并单元格表格

    springboot项目导出excel 合并单元格表格 导出效果 业务controller 业务数据 业务实体类 注解MyExcel.java 注解 MyExcels 导出工具类MyExcelUtil ...

  6. python拆分excel的sheet为单文件_python拆分Excel表格并发送邮件

    工作中经常会出现需要将数据按一定的条件拆分并分发给不同的收件人的情况,今天就来给大家分享一下如何使用python拆分Excel表格并分发邮件. 以下表(2019年下半年销量数据表)数据为例: 首先我们 ...

  7. SpringBoot图文教程9—SpringBoot 导入导出 Excel 「Apache Poi」(亲测)

    有天上飞的概念,就要有落地的实现 概念十遍不如代码一遍,朋友,希望你把文中所有的代码案例都敲一遍 先赞后看,养成习惯 来源:Springboot使用POI实现导出Excel文件示例的搜索结果-阿里云开 ...

  8. Java POI SXSSFWorkbook 读取模板,输出

    Java POI 用 SXSSFWorkbook 读取模板,输出 公司的项目,用户投诉,说只要点击模板下载功能,就会导致整个服务全部停掉. 之前这个功能由于数据量并没有那么多,所以一直都没有发生过这种 ...

  9. SpringBoot图文教程9—SpringBoot 导入导出 Excel 「Apache Poi」

    有天上飞的概念,就要有落地的实现 概念十遍不如代码一遍,朋友,希望你把文中所有的代码案例都敲一遍 先赞后看,养成习惯 SpringBoot 图文教程系列文章目录 SpringBoot图文教程1「概念+ ...

最新文章

  1. Unity接入安卓sdk查看应用内存占用
  2. 刷脸支付问题多,亚马逊选择刷「手掌」,在无人超市正式商用
  3. 总有些物理现象颠覆你的想象
  4. Ubuntu 安装Jdk(apt-get)
  5. php 分布式数据库查询,分布式数据库
  6. 刘庆敏 博客linux,Linux内核源码分析--zImage出生实录(Linux-3.0 ARMv7)
  7. Js中Number对象
  8. 万测试验机软件,万测TestStar®新秀®100kN微机控制电子万能试验机
  9. Kotlin协程简介(一) Hello,coroutines!
  10. docker——容器(container)
  11. 一篇关于蓝牙SDP和L2CAP协议的文章
  12. PHP自定义数组转Json字符串函数
  13. 计蒜客 挑战难题 整数转换成罗马数字
  14. android qq输入法表情,QQ输入法-问题反馈
  15. android抠图软件,手机抠图软件
  16. 使用POI编译word—删除WORD空白段落
  17. 目标检测入门实战:贪吃蛇小游戏
  18. TCGA 亚型突变负荷代码
  19. stardict词典下载
  20. rd640服务器引导,联想RD640服务器内部介绍

热门文章

  1. UP Squared Board评测——毫无疑问,这是全球性能最强的创客板了
  2. CSS实现微信对话框
  3. html5微信超链接对话窗口,HTML5高仿微信聊天、微信聊天表情|对话框|编辑器功能...
  4. Python-自制背题脚本!你们背过毛概吗?
  5. Eclipse安装、使用及卸载
  6. 网络协议-DNS协议
  7. 干货|初学者怎么学习ZBrush
  8. c语言窗口程序暂停,C语言中的时间和程序暂停
  9. JDK 11主要特性一览
  10. 解决springboot + quartz的分布式问题以及dolphinscheduler