当前位置: 首页 > news >正文

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

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信息内容并发送邮件, 并不是你想想中的那么简单

Springboot 读取模板excel信息内容并发送邮件 背景技术选型搭建过程数据加密隐藏问题暴露背景追溯解决背景 在我们日常开发中, 会遇到这样一种场景, 就是读取表格中的数据, 并将数据以附件的形式通过邮箱发送到表格中的每个人 即: excel 读取 excel 写入 发送邮件(携带附件), 例…...

蓝桥杯真题31日冲刺 |第一天

蓝桥杯真题31日冲刺 |第一天 一&#xff1a;完全平方数 题目&#xff1a;[链接](完全平方数 - 蓝桥云课 (lanqiao.cn)) 思路&#xff1a; 将 每个 完全平方数都 消掉&#xff0c;剩下的就是 不能构成平方的数 以12 为例&#xff1a; 所以 12 只要再 乘个三 即可满足 代…...

STM32开发(18)----CubeMX配置RTC

CubeMX配置RTC前言一、什么是RTC&#xff1f;RTC时钟源RTC备份域二、实验过程1.CubeMX配置2.代码实现3.实验结果总结前言 本章介绍使用STM32CubeMX对RTC进行配置的方法&#xff0c;RTC的原理、概念和特点&#xff0c;配置各个步骤的功能&#xff0c;并通过实验方式验证。 一、…...

Qt 单例模式第一次尝试

文章目录摘要单例模式如何使用Qt 的属性系统总结关键字&#xff1a; Qt、 单例、 的、 Q_GLOBAL_STATIC、 女神节摘要 世界上第一位电脑程序设计师是名女性&#xff1a;Ada Lovelace (1815-1852)是一位英国数学家兼作家&#xff0c;她是第一位主张计算机不只可以用来算数的人…...

C语言--一维数组

数组概念 数组&#xff1a;是一种构造数据类型&#xff0c;用以处理批量的同种类型的数据。 主要特点&#xff1a;数据量大 &#xff0c;类型相同 一维数组的定义 语法&#xff1a; 类型说明符 数组名[整型常量表达式]&#xff1b; 注意&#xff1a; 方括号里面的内容用于指…...

DataGear 4.5.1 发布,数据可视化分析平台

DataGear 4.5.1 发布&#xff0c;严重 BUG 修复&#xff0c;具体更新内容如下&#xff1a; 修复&#xff1a;修复SQL数据集对于DB2、SQLite等数据源预览时会报错的BUG&#xff1b;修复&#xff1a;修复系统对于MySQL、MariaDB等数据源中无符号数值类型有时报错的BUG&#xff1…...

Springboot——@valid 做字段校验和自定义注解

文章目录前言注意实现测试环境验证自带的注解自定义valid注解自定义注解和处理类创建参数接收类&#xff0c;并增加字段注解接口中使用自测环节正常测试异常测试自定义全局异常监听扩展递归参数下valid不识别的坑前言 再项目开发中&#xff0c;针对前端传递的参数信息&#xf…...

c语言基础练习题详解

&#x1f49e;&#x1f49e; 1.C语言程序的基本单位是&#xff08;C&#xff09;。 A&#xff0e;程序行 B&#xff0e; 语句 C&#xff0e; 函数 D&#xff0e;字符 &#x1f49e;&#x1f49e; 2.已知各变量的类型说明如下&#xff1a; int m6,n,a,b; unsigned long w8;…...

C语言设计模式:实现简单工厂模式和工程创建

目录 一&#xff0c;设计模式概念引入 ① 什么是设计模式 ② 什么是类和对象 ③ 什么是工厂模式 二&#xff0c;C语言工厂模式的实现 ① 普通类和对象的代码实现 ② 工厂模式代码实现 ● cat.c ● dog.c ● person.c ● animal.h ● mainpro.c ● 完善mainpro.c …...

3.6日报

今天进行3.0信号整理工作 做官网后台技术文档 了解grpc gRPC是rpc框架中的一种&#xff0c;是rpc中的大哥 是一个高性能&#xff0c;开源和通用的RPC框架&#xff0c;基于Protobuf序列化协议开发&#xff0c;且支持众多开发语言。 面向服务端和协议端&#xff0c;基于http…...

中文代码88

PK 嘚釦 docProps/PK 嘚釦|,g z docProps/app.xml漅AN??駠(髂v诖m岼侸 魣,g踃$秂D廋Qvf漶x莗笳w?:瘜^?俍欶辇2}?睧汎 t#:?效7治XtA鏊?羄鈋嫿饄攗Tv契"D桷撵vJ鉂?闌 Jg??浱?樱沲gic鋹峡?sū窛葻?]迾?9卑{艏 rk\?洺萹啰N?W??2&quo…...

ElasticSearch 基础(五)之 映射

目录前言一、映射&#xff08;Mapping&#xff09;简介二、动态映射&#xff08;Dynamic mapping&#xff09;1、动态字段映射1.1、日期检测1.1.1、禁用日期检测1.1.2、自定义检测到的日期格式1.2、数值检测2、动态模板三、显示映射&#xff08;Explicit mapping&#xff09;1、…...

【C语言督学训练营 第二天】C语言中的数据类型及标准输入输出

文章目录一、前言二、数据类型1.基本数据类型①.整形②.浮点型③.字符型2.高级数据类型3.数据分类①.常量②.变量三、标准输入输出1.scanf2.printf四、进制转换1.进制转换简介2.十进制转其他进制3.其他进制转换五、OJ网站的使用一、前言 王道2024考研408C语言督学营第二天&…...

重资产模式和物流网络将推动京东第四季度利润率增长

来源&#xff1a;猛兽财经 作者&#xff1a;猛兽财经 强劲的2022年第三季度财务业绩 2022年11月18日&#xff0c;京东&#xff08;JD&#xff09;公布了2022年第三季度财务业绩&#xff0c;净收入为2435亿元人民币&#xff0c;增长了11.4%。净服务收入为465亿元人民币&#xf…...

【新】EOS至MES的假捻报工数据导入-V2.0版本

假捻自动线的数据和MES没有进行对接,直接入库至EOS。 因此可信平台上缺少这部分的报工数据,需要把EOS的入库数据导出,整理成报工数据,导入到MES,然后通过定时任务集成到可信平台。 MES这边的报工数据整理,主要是添加订单明细ID,和完工单号。 订单明细ID(根据批次号和…...

python甜橙歌曲音乐网站平台源码

wx供重浩&#xff1a;创享日记 对话框发送&#xff1a;python音乐 获取完整源码源文件说明文档配置教程等 在虚拟环境下输入命令“python manage.py runserver”启动项目&#xff0c;启动成功后&#xff0c;访问“http://127.0.0.1:5000”进入甜橙音乐网首页&#xff0c;如图1所…...

docker imageID计算

Image ID是在本地由Docker根据镜像的描述文件计算的&#xff0c;并用于imagedb的目录名称 docker镜像id都保存在/var/lib/docker/image/overlay2/imagedb/content/sha256下面&#xff0c;都是一些以sha256sum计算文件内容得出的哈希值的文件。 #ls /var/lib/docker/image/ove…...

借助媛如意让ROS机器人turtlesim画出美丽的曲线-云课版本

首先安装并打开猿如意其次打开蓝桥云课ROS并加入课程在猿如意输入问题得到答案在蓝桥云课ROS验证如何通过turtlesim入门ROS机器人您可以通过以下步骤入门ROS机器人&#xff1a;安装ROS&#xff1a;您需要安装ROS&#xff0c;可以在ROS官网上找到安装指南。安装turtlesim&#x…...

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

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

【C++知识点】异常处理

✍个人博客&#xff1a;https://blog.csdn.net/Newin2020?spm1011.2415.3001.5343 &#x1f4da;专栏地址&#xff1a;C/C知识点 &#x1f4e3;专栏定位&#xff1a;整理一下 C 相关的知识点&#xff0c;供大家学习参考~ ❤️如果有收获的话&#xff0c;欢迎点赞&#x1f44d;…...

网络编程(Modbus进阶)

思维导图 Modbus RTU&#xff08;先学一点理论&#xff09; 概念 Modbus RTU 是工业自动化领域 最广泛应用的串行通信协议&#xff0c;由 Modicon 公司&#xff08;现施耐德电气&#xff09;于 1979 年推出。它以 高效率、强健性、易实现的特点成为工业控制系统的通信标准。 包…...

日语学习-日语知识点小记-构建基础-JLPT-N4阶段(33):にする

日语学习-日语知识点小记-构建基础-JLPT-N4阶段(33):にする 1、前言(1)情况说明(2)工程师的信仰2、知识点(1) にする1,接续:名词+にする2,接续:疑问词+にする3,(A)は(B)にする。(2)復習:(1)复习句子(2)ために & ように(3)そう(4)にする3、…...

MongoDB学习和应用(高效的非关系型数据库)

一丶 MongoDB简介 对于社交类软件的功能&#xff0c;我们需要对它的功能特点进行分析&#xff1a; 数据量会随着用户数增大而增大读多写少价值较低非好友看不到其动态信息地理位置的查询… 针对以上特点进行分析各大存储工具&#xff1a; mysql&#xff1a;关系型数据库&am…...

OkHttp 中实现断点续传 demo

在 OkHttp 中实现断点续传主要通过以下步骤完成&#xff0c;核心是利用 HTTP 协议的 Range 请求头指定下载范围&#xff1a; 实现原理 Range 请求头&#xff1a;向服务器请求文件的特定字节范围&#xff08;如 Range: bytes1024-&#xff09; 本地文件记录&#xff1a;保存已…...

React19源码系列之 事件插件系统

事件类别 事件类型 定义 文档 Event Event 接口表示在 EventTarget 上出现的事件。 Event - Web API | MDN UIEvent UIEvent 接口表示简单的用户界面事件。 UIEvent - Web API | MDN KeyboardEvent KeyboardEvent 对象描述了用户与键盘的交互。 KeyboardEvent - Web…...

自然语言处理——Transformer

自然语言处理——Transformer 自注意力机制多头注意力机制Transformer 虽然循环神经网络可以对具有序列特性的数据非常有效&#xff0c;它能挖掘数据中的时序信息以及语义信息&#xff0c;但是它有一个很大的缺陷——很难并行化。 我们可以考虑用CNN来替代RNN&#xff0c;但是…...

【论文阅读28】-CNN-BiLSTM-Attention-(2024)

本文把滑坡位移序列拆开、筛优质因子&#xff0c;再用 CNN-BiLSTM-Attention 来动态预测每个子序列&#xff0c;最后重构出总位移&#xff0c;预测效果超越传统模型。 文章目录 1 引言2 方法2.1 位移时间序列加性模型2.2 变分模态分解 (VMD) 具体步骤2.3.1 样本熵&#xff08;S…...

大学生职业发展与就业创业指导教学评价

这里是引用 作为软工2203/2204班的学生&#xff0c;我们非常感谢您在《大学生职业发展与就业创业指导》课程中的悉心教导。这门课程对我们即将面临实习和就业的工科学生来说至关重要&#xff0c;而您认真负责的教学态度&#xff0c;让课程的每一部分都充满了实用价值。 尤其让我…...

Redis数据倾斜问题解决

Redis 数据倾斜问题解析与解决方案 什么是 Redis 数据倾斜 Redis 数据倾斜指的是在 Redis 集群中&#xff0c;部分节点存储的数据量或访问量远高于其他节点&#xff0c;导致这些节点负载过高&#xff0c;影响整体性能。 数据倾斜的主要表现 部分节点内存使用率远高于其他节…...

Spring Cloud Gateway 中自定义验证码接口返回 404 的排查与解决

Spring Cloud Gateway 中自定义验证码接口返回 404 的排查与解决 问题背景 在一个基于 Spring Cloud Gateway WebFlux 构建的微服务项目中&#xff0c;新增了一个本地验证码接口 /code&#xff0c;使用函数式路由&#xff08;RouterFunction&#xff09;和 Hutool 的 Circle…...