当前位置: 首页 > 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;…...

【FATE联邦学习debug】 No module named ‘federatedml‘

直接pip install federatedml是无法找得到这个库的。 这个的原因是环境变量的事情&#xff0c;因为在部署文档中&#xff0c;本身提示我们要更新一些环境变量&#xff0c;如果不export那些变量&#xff0c;下面的fate_test其实也是无法测试成功的。 打开bin/init_env.sh&#x…...

【Git】P1 Git 基础

Git 基础Git 基本概念集中式版本控制工具 与 分布式版本控制工具Git 下载与安装Bash 初始设置创建本地仓库Git 三区概念一个简单的提交流程更改文件后再次提交git 实现版本切换查看提交日志设置 git 快捷键版本切换&#xff08;一&#xff09;版本切换&#xff08;二&#xff0…...

智能交通数据集Rope3D(仅限科研使用)

Rope3D Dataset 官网&#xff1a;https://thudair.baai.ac.cn/index &#xff01;&#xff01;&#xff01;如想要使用Rope3D数据集进行2D检测&#xff0c;最后有我们处理完的数据集链接。 &#xff01;&#xff01;&#xff01; 介绍&#xff1a; DAIR-V2X数据集是首个用于…...

Java虚拟机JVM-面试题

1、Java 虚拟机是如何捕获异常的&#xff1f; 答&#xff1a; 在编译生成的字节码中&#xff0c;每个方法都附带一个异常表。异常表中的每一个条目代表一个异常处理器&#xff0c;并且由 from 指针、to 指针、target 指针以及所捕获的异常类型构成。这些指针的值是字节码索引…...

详细的说说Redis的数据类型

Redis是一个开源的内存数据库&#xff0c;它可以用作缓存、消息代理、实时数据处理和许多其他用途。Redis是一个key-value存储系统&#xff0c;其中数据存储在内存中&#xff0c;并通过网络进行访问。与传统的关系型数据库不同&#xff0c;Redis支持多种数据结构&#xff0c;包…...

798.差分矩阵

输入一个 n行 m列的整数矩阵&#xff0c;再输入 q个操作&#xff0c;每个操作包含五个整数 x1,y1,x2,y2,c&#xff0c;其中 (x1,y1)和 (x2,y2) 表示一个子矩阵的左上角坐标和右下角坐标。每个操作都要将选中的子矩阵中的每个元素的值加上 c。 请你将进行完所有操作后的矩阵输出…...

InfluxDB 2 介绍与使用 flux查询 数据可视化

一、关键概念 相比V1 移除了database 和 RP&#xff0c;增加了bucket。 V2具有以下几个概念&#xff1a; timestamp、field key、field value、field set、tag key、tag value、tag set、measurement、series、point、bucket、bucket schema、organization 新增的概念&…...

Qt QTreeView简单使用

QT-QTreeView使用方法 QTreeView: 用于显示树状结构数据&#xff0c;适用于树状结构数据的操作。 一、初始化 ​ 利用QStandardlternModel来初始化数据&#xff0c;标准的基于项数据的数据模型类&#xff0c; 每个项数据可以是任何数据类型。 // 初始化model QStandardItem…...

Wannacrypt蠕虫老树开花?又见Wannacrypt

Wannacrypt蠕虫是一个在2017年就出现的远古毒株&#xff0c;其利用永恒之蓝漏洞降维打击用户服务器&#xff0c;而后进行扩散勒索&#xff0c;曾经一度风靡全球&#xff0c;可谓是闻者伤心&#xff0c;听着落泪&#xff0c;因为这玩意解密是不可能 解密的。 而2023年的今天&am…...

结合基于规则和机器学习的方法构建强大的混合系统

经过这些年的发展&#xff0c;我们都确信ML即使不能表现得更好&#xff0c;至少也可以在几乎所有地方与前ML时代的解决方案相匹配。比如说一些规则约束&#xff0c;我们都会想到能否把它们替换为基于树的ml模型。但是世界并不总是黑白分明的&#xff0c;虽然机器学习在解决问题…...

广州web网站开发培训班/百度用户服务中心人工24小时电话

小信号分析法重点笔记开关电源的反馈环路设计是开关电源设计的一个非常重要的部分&#xff0c;它关系到一个电源性能的好坏。要设计一个好的环路&#xff0c;必须要知道主回路的数学模型&#xff0c;然后根据主回路的数学模型&#xff0c;设计反馈补偿环路。开关电源是一个非线…...

wordpress hide/seo优化外包

摘要&#xff1a;进入21世纪以来,互联网的快速发展以及带宽的提升使得通过互联网获取各种对带宽要求较高的多媒体服务提供了可能.以往的单播方案,采取C/S模型,在特定的服务器和每位用户之间都提供一条单独的数据传输通道.当用户数量快速增长时,不得不增加更多专用服务器和提高带…...

免费动态网站成品作业/惊艳的网站设计

隐写篇0x01. 通过进制转换隐藏信息0x02. 在图片中隐藏压缩包(图种)加密篇0x03. 伪加密0x04. 爆破/字典/掩码攻击0x05. 明文攻击0x06. CRC32碰撞格式篇0x07. 修改格式0x01. 通过进制转换隐藏信息这种方法比较简单&#xff0c;直接拿一道题讲解(题目来自ISCC 2017 Basic-04)。题目…...

opencms wordpress/宁波seo公司哪家好

阅读原文&#xff1a;技术面试中的软技能 作为求职者时&#xff0c;只听别人说如何面试&#xff0c;按部就班的做&#xff0c;没有特别的体会。如今作为面试官&#xff0c;面试别人时&#xff0c;才发现什么才是重要的事情。 形象 形象包括衣着发型和五官。衣着不必太正式&…...

淘客网站如何做推广/seo招聘要求

1、将所有的.enw文件放在一个文件夹中&#xff1b; 2、利用dos命令copy *.enw new.enw将该文件夹中所有的.enw文件合并为一个new.enw文件&#xff1b; 3、用notepad打开&#xff0c;先用<br>%全部替换%&#xff0c;再用<br>%0全部替换%0&#xff0c;然后把文件另…...

做歌手的网站/色盲测试图片

$(#add_submit_ajax).click(function(){$.ajax({url: /ajax_add_app,// data: {user: 123,host_list: [1,2,3,4]},data: $(#add_form).serialize(),type: "POST",dataType: JSON, // 内部 让jQuery把返回的 json字符串 转成json对象 traditional: true, //当发送的…...