Java使用阿里邮箱生成excle邮件附件发送

需求背景

  • 日常工作中,对于一个码农来说,熟练使用框架之外,还需要灵活使用各类工具类,类似于导入导出,上传下载,邮件短信,语音视频等常用功能也是非常常见的需求,网上搜索的相关资料也是非常的多,今天就描述一个需求的场景,难度不是太难,只是需要注意的小细节还是挺多的,故借此契机,二狗在此将我个人实现的过程全程记录下来,希望有类似需求的朋友可以参考,但是话说到底,编程这个东西可以看,可以摘录,可以背诵,但是只有自己手敲出来的才是自己的,希望二狗的文章可以帮助更多的朋友把时间花在研究技术原理以及各类底层实现的设计艺术上,而对于业务类的需求,永远千变万化,只需要理解清楚,想顺畅了,就可以动手开干~以上一些个人理解,如有

实现功能:

  • 每周二上午10点,以当天时间为参照时间节点,到当天之前的一周内的这个时间段,把后台的律师信息表中的律师信息,进行汇总,然后通过excel附件的形式,使用阿里邮箱发送到市场运营人员的邮箱;

需求分析:

  1. 每周二上午10点–定时任务解决(需要注意线上多台服务器跑批,执行定时任务需要添加分布锁,避免重复跑批)
  2. 时间段为:(当天-当天前一周]
  3. 通过Excel 附件形式,考虑使用POI生成Excel模板文件,后端读取数据进行填充
  4. 使用阿里邮箱,需要准备好连接邮箱的Host,端口,协议等参数,数据上准备好收件人邮箱账号+发件人邮箱账号,密码

项目技术架构选型描述

  • 本项目使用的是Springboot框架,一些固定参数,比如邮箱的Host,端口,协议等参数,数据上准备好收件人邮箱账号发件人邮箱账号,密码等均参数化的配置在boot框架的yml配置文件中
  • 定时任务使用注解@Scheduled(cron="")方式进行跑批,当然启动类Application类需要添加@EnableScheduling 注解以便于执行定是脚本,在此不再赘述,因为不是本文探讨的重点;
  • 本本文描述的代码基于Idea+ jdk1.8+ 数据库mysql+ Mybatis+ Gradle导包+ Redis缓存

代码过程展示
1、build.gradle 文件 的dependencies { }中先导包,如果你使用的是Maven原理也是一样,可以去 Maven中央仓库搜索你想要的包的版本的Maven写法,在此就不再赘述

//poi
	implementation('org.apache.poi:poi:3.16')
	implementation('org.apache.poi:poi-ooxml:3.14')
	implementation('org.apache.poi:poi-scratchpad:3.15')
	//email
	implementation('org.springframework:spring-context-support')
	implementation('javax.mail:javax.mail-api:1.5.1')
	implementation('com.sun.mail:javax.mail:1.5.4')

2、yml文件内配置邮箱相关参数

email:
  host: smtpdm.aliyun.com
  port: 80
  senderMail: [email protected]
  senderPassword: Wahaha2019
  sendTo: [email protected],[email protected]

3、律师信息实体类–使用的Lombok插件通过@Data生成setter 和getter

@Data
public class LawyerAuthenticate {

    /**
     * 主键
     * */
    private Long id;
    /**
     * 律师姓名
     * */
    private String  lawName;
    /**
     * 所属律所
     * */
    private String lawFirm;
    /**
     * 律师手机号
     * */
    private String phone;
    /**
     * 律师常用邮箱
     * */
    private String  mail;
    /**
     * 律师执业证号
     * */
    private String licenseNumber;
    /**
     * 律师执业证照片名字
     * */
    private String  licenseName;
    /**
     * 律师执业证照片url
     * */
    private String licenseUrl;
    /**
     * 身份证照片名字
     * */
    private String idcardName;
    /**
     * 身份证照片url
     * */
    private String idcardUrl;
    /**
     * 创建日期
     * */
    private Date createDate;
    /**
     * 起始时间--不存库
     * */
    private Date startTime;
    /**
     * 截止时间--不存库
     * */
    private Date endTime;
    }

4、日期工具类–在本功能内仅仅用到了:nextDay(int num) 这个方法,其他日期常用方法,为了篇幅的简洁,故在此就不一一列举了

/**
 * 日期相关工具
 */
public class DateUtils  {

    /**
     * 获取当前日期指定天数之后的日期.
     * @param num   相隔天数
     * @return Date 日期
     * @since 1.0
     */
    public static Date nextDay(int num) {
        Calendar curr = Calendar.getInstance();
        curr.set(Calendar.DAY_OF_MONTH, curr.get(Calendar.DAY_OF_MONTH) + num);
        return curr.getTime();
    }
}

4、定时任务脚本代码
*注意该接口类下的方法,为本次需求的核心方法:sendMailLawyerAuthService.sendMail();

@Slf4j
@Component
public class SendMailOfLawyerAuthJob {

    @Autowired
    private SendMailLawyerAuthService sendMailLawyerAuthService;


    @Autowired
    private RedisTemplate redisTemplate;


    @Scheduled(cron = "0 0 10 ? * TUE")
    protected synchronized void executeInternal() throws InterruptedException {
        Thread.sleep((int) (Math.random() * 8000));
        Object judgementCount = this.getJudgementCount();
        if (null == judgementCount) {
            // 分布式锁
            redisTemplate.execute(new RedisCallback() {
                @Override
                public Object doInRedis(RedisConnection connection) throws DataAccessException {

                    byte[] key = redisTemplate.getStringSerializer().serialize("SendMailOfLawyerAuthJob");

                    byte[] value = SerializationHelper.serialize("SendMailOfLawyerAuthJob");

                    connection.setEx(key, 120, value);
                    return null;
                }
            });
            Date now = new Date();

            //新版律师信息汇总定期发送邮件成功!
            try {
                sendMailLawyerAuthService.sendMail();
            } catch (Exception e){
                log.error("ErrorMessage: "+ e.getMessage());
            }
            //Test测试打印
            log.info("新版律师信息汇总定期发送邮件任务:SendMailOfLawyerAuthJob 执行完毕!");
        }
    }

    //分布式锁
    public Object getJudgementCount() {
        return redisTemplate.execute(new RedisCallback() {
            @Override
            public Object doInRedis(RedisConnection connection) throws DataAccessException {
                byte[] key = redisTemplate.getStringSerializer().serialize("SendMailOfLawyerAuthJob");
                byte[] value = connection.get(key);
                return SerializationHelper.unSerialize(value);
            }
        });
    }

}

5、核心方法sendMail() 代码示例

/**发送邮件核心方法*/
public interface SendMailLawyerAuthService {

    void sendMail() throws IOException;
}

实现类方法—此处非常重要,此处非常重要,此处非常重要(重要的是说三遍!)

/**
 * Created by 陈二狗 on 2019-09-18.
 * Desc: 新版律师信息汇总定期发送邮件
 */
@Service
public class SendMailLawyerAuthServiceImpl implements SendMailLawyerAuthService {

    @Autowired
    private LawyerAuthenticateMapper lawyerAuthenticateMapper;

    private static final Logger log = LoggerFactory.getLogger(SendMailLawyerAuthServiceImpl.class);

    @Value("${email.host}")
    private String emailHost;
    @Value("${email.port}")
    private int emailPort;
    @Value("${email.senderMail}")
    private String senderMail;
    @Value("${email.senderPassword}")
    private String senderPassword;
    @Value("${email.sendTo}")
    private List sendTo;

    /**发送邮件的附件标题后缀*/
    private static final String FILENAME_END_PREFIX = "新版律师信息汇总.xls";

    /**
     * 发送带Excel附件的阿里企业邮件
     * @throws IOException
     */
    @Override
    public void sendMail() throws IOException {

        //获取当前日期的前一周日期
        Date startTime = DateUtils.nextDay(-7);
        Date endTime = new Date();
        //首先查库获取周期范围内的数据信息
        List  lists = lawyerAuthenticateMapper.sendMailOfLawyerAuth(startTime, endTime);
        if (CollectionUtils.isEmpty(lists)) {
            log.info("没有需要发送的认证律师相关的邮件!");
            return ;
        }

        //1、调用创建邮件对象辅助方法
        Map resultMap = createSendMailHelper();
        Properties props = (Properties)resultMap.get("props");
        Authenticator authenticator = (Authenticator)resultMap.get("authenticator");

        Session session = Session.getInstance(props,authenticator);
        MimeMessage message = new MimeMessage(session);
        try {
            /**
             * 2、设置发件人
             * 其中 InternetAddress 的三个参数分别为: 邮箱, 显示的昵称(只用于显示, 没有特别的要求), 昵称的字符集编码
             */
            message.setFrom(new InternetAddress(senderMail, "wusong", "UTF-8"));

            /**
             * 3、设置收件人--支持多个,基于收件人肯定存在的情况下哈,因为发邮件如果一个收件人都没有,我·····
             * To收件人   CC 抄送  BCC密送
             */
            if (sendTo.size() > 0) {
                InternetAddress[] sendToInfo = new InternetAddress[sendTo.size()];
                for (int i = 0; i < sendTo.size(); i++) {
                    sendToInfo[i] = new InternetAddress(sendTo.get(i), "", "UTF-8");
                }
                message.addRecipients(MimeMessage.RecipientType.TO, sendToInfo);
            }
            
            /**
             * 4、设置标题
             */
            log.info("导出到Excel");
            message.setSubject("新版律师认证信息汇总表","UTF-8");

            HSSFWorkbook workbook = new HSSFWorkbook();
            CreationHelper helper = workbook.getCreationHelper();
            HSSFSheet sheet = workbook.createSheet("新版律师认证信息汇总表");

            //调用设置文件以及填充对象数据方法,具象模板文件
            setFileFromLawyerInfolHelper(sheet, lists);

            /****workBook写入输出流******/
            log.info("workBook写入输出流");
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            workbook.write(baos);
            baos.flush();
            baos.close();

            DataSource fds = new ByteArrayDataSource(baos.toByteArray(), "application/vnd.ms-excel");

            MimeBodyPart mbp1 = new MimeBodyPart();
            //此处设置的是邮件内容的body部分,可以对比效果图查看
            mbp1.setText("你好:\n                 律师信息邮件汇总,请注意查收!");

            MimeBodyPart mbp2 = new MimeBodyPart();
            mbp2.setDataHandler(new DataHandler(fds));

            //5、调用格式化日期方法,生成附件标题
            String fileNameStr = formatDateHelper();
            //Javamail源码中MimeBodyPart在setFileName 时候会对文件名进行强制转码,So,针对此进行强制设置编码,避免出现乱码现象
            mbp2.setFileName(new String(fileNameStr.getBytes(), "ISO8859-1"));

            Multipart mp = new MimeMultipart();
            mp.addBodyPart(mbp1);
            mp.addBodyPart(mbp2);
            message.setContent(mp);

            /**
             * 6、保存邮件并发送
             */
            log.info("保存邮件并发送");
            message.saveChanges();
            Transport transport = session.getTransport("smtp");
            transport.connect(emailHost, senderMail, senderPassword);
            transport.sendMessage(message,message.getAllRecipients());
            transport.close();
        } catch (MessagingException e) {
           log.error("异常信息为:"+e.getMessage());
        } catch (UnsupportedEncodingException e) {
           log.error("异常信息为:"+e.getMessage());
        }
    }

    /**
     *
     * 创建邮件相关对象辅助方法-001
     * */
    private Map createSendMailHelper(){
        Properties props = new Properties();
        /**表示SMTP发送邮件,需要进行身份验证*/
        props.put("mail.smtp.auth", "true");
        /**是否启用调试*/
        props.put("mail.debug", "false");
        /**设置链接超时*/
        props.put("mail.smtp.timeout", "50000");
        /**设置端口*/
        props.put("mail.smtp.port", Integer.toString(emailPort));
        /**设置ssl端口*/
        props.put("mail.smtp.socketFactory.port", Integer.toString(emailPort));
        props.put("mail.smtp.socketFactory.fallback", "false");
        props.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");

        // 发件人的账号
        props.put("mail.user", senderMail);
        // 访问SMTP服务时需要提供的密码(邮箱密码)
        props.put("mail.password", senderPassword);

        // 构建授权信息,用于进行SMTP进行身份验证
        Authenticator authenticator = new Authenticator() {
            @Override
            protected PasswordAuthentication getPasswordAuthentication() {
                // 用户名、密码
                String userName = props.getProperty("mail.user");
                String password = props.getProperty("mail.password");
                return new PasswordAuthentication(userName, password);
            }
        };

        //封装返回对象参数数据
        Map  mailMap = new HashMap<>();

        mailMap.put("props",props);
        mailMap.put("authenticator",authenticator);

        return mailMap;
    }



    /**
     * 设置文件以及填充数据辅助方法-002
     * */
    private void  setFileFromLawyerInfolHelper(HSSFSheet sheet, List lists){

        //设置列宽度
        sheet.setColumnWidth(0, 20 * 256);
        sheet.setColumnWidth(1, 20 * 256);
        sheet.setColumnWidth(2, 20 * 256);
        sheet.setColumnWidth(3, 20 * 256);
        sheet.setColumnWidth(4, 20 * 256);
        sheet.setColumnWidth(5, 20 * 256);
        sheet.setColumnWidth(6, 80 * 256);
        sheet.setColumnWidth(7, 80 * 256);
        //新增数据行,并且设置单元格数据
        int rowNum = 1;
        String[] headers = { "序号","律师姓名","手机号","所属律所","邮箱","执业证号","律师执照照片","手持身份证照片"};
        //headers表示excel表中第一行的表头
        HSSFRow row = sheet.createRow(0);
        //在excel表中添加表头
        for(int i=0;i

邮件发送代码说明:
1)附件主题乱码,通过读取源码的setFilename()方法可以看到,源码底层对字符串进行了转码,所以我们在代码中,进行了处理,需要注意的是,不同的邮箱可能处理的方式不尽相同,此处我这里使用的是阿里企业邮箱,以下为我的处理方式:

 String fileNameStr = formatDateHelper();
 //Javamail源码中MimeBodyPart在setFileName 时候会对文件名进行强制转码,So,针对此进行强制设置编码,避免出现乱码现象
mbp2.setFileName(new String(fileNameStr.getBytes(), "ISO8859-1"));

网上很多资料说的方式是在发送邮件之前,添加修改编码代码,如果大家感兴趣可以自行百度,此处不再过多赘述;

2)附件的excel 内各列的宽度可以自定义设置调整,各位看官根据实际需要变更即可

附上效果图:红色1:发件人;红色2:多个收件人
Java使用阿里邮箱生成excle邮件附件发送_第1张图片
附件详情效果图:
上图是附件excel详情

  • 综上所述,整体的功能实现到此基本全部完毕,代码虽然进行了一定的抽离但是届于自身能力以及工作技术架构选型的的局限,解决方案不一定是最优解,但是可以作为有类似需求的看官浏览使用,不足之处,欢迎各位看官提出宝贵的意见,我也会积极响应,长路漫漫,其修远兮,共勉!

你可能感兴趣的:(日常搬砖烫手之随笔,定时任务发,生成Excel附件,阿里邮箱定时发送带附件邮件,分布锁)