项目第9天

复习:
一、分页
1、MyBatis —PageHelper
2、使用的是一个公开源代码的工具
导入该导入的内容 tld,Page.java NavigationTag.java
使用就是在需要的jsp页面上,导入taglib 地址,
最重要的就是Servlet如何编写:
封装一个Page对象。
学习目的:动手能力,学习思路。
二、爬虫 —- Jsoup

今日内容

一、定时任务 — Quartz (只是其中一种实现方案)

Quartz是OpenSymphony开源组织在Job scheduling领域又一个开源项目,它完全由 Java 写成,并设计用于 J2SE 和 J2EE 应用中。
Quartz可以用来创建简单或为运行十个,百个,甚至是好几万个Jobs这样复杂的程序。它提供了巨大的灵活性而不牺牲简单性。
你能够用它来为执行一个作业而创建简单的或复杂的调度。
1)Job
表示一个任务(工作),要执行的具体内容。
2)JobDetail
JobDetail 表示一个具体的可执行的调度程序,Job 是这个可执行程调度程序所要执行的内容,另外 JobDetail 还包含了这个任务调度的方案和策略。
告诉调度容器,将来执行哪个类(job)的哪个方法
3)Trigger 是一个类,代表一个调度参数的配置,描述触发Job执行的时间触发规则。一个Job可以对应多个Trigger,但一个Trigger只能对应一个Job
4)Scheduler 代表一个调度容器,一个调度容器中可以注册多个 JobDetail 和Trigger。 Scheduler可以将Trigger绑定到某一JobDetail中,这样当Trigger触发时,对应的Job就被执行。
注意事项: 当JobDetail和Trigger在scheduler容器上注册后,形成了装配好的作业(JobDetail和Trigger所组成的一对儿),就可以伴随容器启动而调度执行了。

由上面总结:
1、有一个总管家—Scheduler
2、每一个具体的任务都交给 Job 和 JobDetail —任务就是要干的具体的事儿
3、定时任务,就必须有定时才可以,如何定时—>Trigger 类
Quartz 在不同的项目中,不同的技术中释放方式略有不同。最简单的方式就是跟Spring进行整合使用。目前学习的是Servlet,所以我们目前演示的是:Servlet和Quartz整合的代码
第一步:导入需要的jar包
项目第9天 - 图1

先编写一个任务:

  1. public class SendEmailTaskJob implements Job {
  2. // 只有一个方法名,叫执行
  3. @Override
  4. public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
  5. System.out.println("此处编写定时发邮件的代码");
  6. }
  7. }

遇到一个问题:
如何启动这个任务呢,以及我将来项目如何运行定时任务的代码?
先编写一个Main方法,测试一下定时任务是否可以正常使用:

  1. package com.qfedu.job;
  2. import org.quartz.*;
  3. import org.quartz.impl.StdSchedulerFactory;
  4. public class TestJob {
  5. public static void main(String[] args) throws Exception {
  6. // 任务的具体描述
  7. // 给任务添加定时
  8. // 管理任务的管家
  9. Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
  10. // 根据代码执行的类和一些描述,组合成JodDetail对象
  11. JobDetail jobDetail = JobBuilder.newJob(SendEmailTaskJob.class).withIdentity("定时发送邮件", "行情组").build();
  12. // 定义一个任务调度的时间对象
  13. CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule("0/1 * * * * ?");
  14. CronTrigger cronTrigger = TriggerBuilder.newTrigger().withIdentity("此处定义任务执行的时间", "发送邮件时机组").withSchedule(scheduleBuilder).build();
  15. // 通过管家,将具体的任务和调度时间,绑定在一起
  16. scheduler.scheduleJob(jobDetail,cronTrigger);
  17. scheduler.start();// 绑定完成后,点击开启摁钮
  18. }
  19. }

以上代码总结来讲: 有一个任务,有一个任务的执行时间,然后通过一个调度器,将任务和执行时间绑定在一起,start()即可。

思考一个问题:将来我的项目如何部署呢?一方面是tomcat的启动,一方面是java的启动
解决办法:
1、将任务编写成一个独立的java项目,但是缺点是代码太少,搁不住,而且改动有点大。
2、可以在Servlet的init方法中,定义定时任务,loadon-start-up = 1 ,tomcat一启动,servlet就被创建,init方法就被执行。
3、我们可以编写一个ServletContextListener ,服务器一启动,定时任务代码执行。
通过Listener 进行定时任务的执行

  1. @WebListener
  2. public class LoadOnListener implements ServletContextListener {
  3. @Override
  4. public void contextInitialized(ServletContextEvent servletContextEvent) {
  5. // tomcat启动,该方法执行,我们在此时调用我们的定时任务
  6. // 管理任务的管家
  7. // ctrl + alt + t 环绕代码提示
  8. try {
  9. Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
  10. // 根据代码执行的类和一些描述,组合成JodDetail对象
  11. JobDetail jobDetail = JobBuilder.newJob(SendEmailTaskJob.class).withIdentity("定时发送邮件", "行情组").build();
  12. // 定义一个任务调度的时间对象
  13. CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule("0/1 * * * * ?");
  14. CronTrigger cronTrigger = TriggerBuilder.newTrigger().withIdentity("此处定义任务执行的时间", "发送邮件时机组").withSchedule(scheduleBuilder).build();
  15. // 通过管家,将具体的任务和调度时间,绑定在一起
  16. scheduler.scheduleJob(jobDetail, cronTrigger);
  17. scheduler.start();// 绑定完成后,点击开启摁钮
  18. } catch (SchedulerException e) {
  19. e.printStackTrace();
  20. }
  21. }
  22. @Override
  23. public void contextDestroyed(ServletContextEvent servletContextEvent) {
  24. }
  25. }

Cron表达式:

Cron 表达式是一个字符串,字符串以 5 或 6 个空格隔开,分为 6 或 7 个域,每一个域代表一个含义。也叫七子表达式。
项目中出现: 0/1 ? 每隔一秒钟执行一次
1)Seconds(秒):可出现”, - /“四个字符,有效范围为 0-59 的整数
2)Minutes(分钟):可出现”, -
/“四个字符,有效范围为 0-59 的整数
3)Hours(小时):可出现”, - /“四个字符,有效范围为 0-23 的整数
4)DayofMonth(日 of 月):可出现”, -
/ ? LW C”八个字符,有效范围为 1-31 的整数
5)Month(月):可出现”, - /“四个字符,有效范围为 1-12 的整数
6)DayofWeek(日 of 星期):可出现”, -
/ ? L C #”八个字符,有效范围为 1-7 的整数
1 表示星期天,2 表示星期一, 依次类推
7)Year(年):可出现”, - * /“四个字符,有效范围为 1970-2099 年

总结: 0/数字 表示每隔多少单位执行一次
知道了该表达式有几位,每个位置上代表什么含义之外,我们还要学习 特殊字符的含义:
1) 表示匹配该域的任意值,假如在 Minutes 域使用, 即表示每分钟都会触发事件。
2) ? 表示不指定值。只能用在 DayofMonth 和 DayofWeek 两个域。因为DayofMonth 和 DayofWeek 会相互影响。例如想在每月的 20 日触发调度,不管20 日到底是星期几,则只能使用如下写法: 13 13 15 20 ?, 其中最后一位只能用?,而不能使用,如果使用*表示不管星期几都会触发。
3) - 表示范围,例如在 Minutes 域使用 5-20,表示从 5 分到 20 分钟每分钟触发一次
4) / 表示起始时间开始触发,然后每隔固定时间触发一次,例如在 Minutes 域使用 5/20,则意味着 5 分钟触发一次,而 25,45 等分别触发一次
5) , 表示列出枚举值值。例如:在 Minutes 域使用 5,20,则意味着在 5 和 20 分每分钟触发一次。
6)L 表示最后,只能出现在 DayofMonth 和 DayofWeek 域。如果在 DayofMonth写 L 表示这个月的最后一天,如果在 DayofWeek 写 L 表示每个星期的最后一天(星期六) 。如果在 DayofWeek 域使用 5L,意味着在最后的一个星期四触发。
7)W 表示最近有效工作日(周一到周五),只能出现在 DayofMonth 域,系统将在离指定日期的最近的有效工作日触发事件。例如:在 DayofMonth 使用 5W,如果 5 日是星期六,则将在最近的工作日:星期五,即 4 日触发。如果 5 日是星期天,则在 6 日(周一)触发;如果 5 日在星期一到星期五中的一天,则就在 5 日触发。另外一点,W 的最近寻找不会跨过月份
8)LW:这两个字符可以连用,表示在某个月最后一个工作日,即最后一个星期五
9)#:用于确定每个月第几个星期几,只能出现在 DayofWeek 域。例如在 4#2,表示某月的第二个星期三。

还是不会写怎么办?借助工具:
https://www.matools.com/cron
填写你想执行的时间,会出现一个表达式,我们根据执行的10次记录,查看记录是否正确。
0 0 22 ? — 代表的意思是每天晚上10点,执行该代码
0 0 22 ?
6 * — 每个月的星期五晚上10点执行。
在使用表达式的时候,一定不要让其报错。
我们发送邮件,什么时候执行呢?
分析:因为这个网站中的数据,我们不知道什么时候更新,我们需要一直去访问并抓取网站上的数据,如果数据有变化,说明数据更新了,更新了就发送邮件。
因为这个网站(https://www.abuquant.com/)数据更新不频繁,所以,我们也没必要一直刷新。每隔1分钟执行一次数据抓取即可。
一些例子,便于理解:

  1. "0 0 12 * * ?" 每天中午 12 点触发
  2. "0 15 10 ? * *" 每天上午 10:15 触发
  3. "0 15 10 * * ?" 每天上午 10:15 触发
  4. "0 15 10 * * ? *" 每天上午 10:15 触发
  5. "0 15 10 * * ? 2005" 2005 年的每天上午 10:15 触发
  6. "0 * 14 * * ?" 在每天下午 2 点到下午 2:59 期间的每 1 分钟触发
  7. "0 0/5 14 * * ?" 在每天下午 2 点到下午 2:55 期间的每 5 分钟触发
  8. "0 0/5 14,18 * * ?" 在每天下午 2 点到 2:55 期间和下午 6 点到 6:55 期间的每 5 分钟触发
  9. "0 0-5 14 * * ?" 在每天下午 2 点到下午 2:05 期间的每 1 分钟触发
  10. "0 10,44 14 ? 3 4" 每年三月的星期三的下午 2:10 2:44 触发
  11. "0 15 10 ? * MON-FRI" 周一至周五的上午 10:15 触发
  12. "0 15 10 15 * ?" 每月 15 日上午 10:15 触发
  13. "0 15 10 L * ?" 每月最后一日的上午 10:15 触发
  14. "0 15 10 ? * 6L" 每月的最后一个星期五上午 10:15 触发
  15. "0 15 10 ? * 6L 2002-2005" 2002 年至 2005 年的每月的最后一个星期五上午 10:15触发
  16. "0 15 10 ? * 6#3" 每月的第三个星期五上午 10:15 触发

二、发送邮件

第一步:导入jar包
第二步:需要配置发送的邮箱
项目第9天 - 图2
项目第9天 - 图3
项目第9天 - 图4
每次生成的授权码都不一样,所以妥善保管。
第三步:复制一个工具类EmailUtils.java
修改 发送邮件的邮箱地址以及授权码。测试

  1. package com.qfedu.utils;
  2. import javax.mail.*;
  3. import javax.mail.internet.InternetAddress;
  4. import javax.mail.internet.MimeMessage;
  5. import java.util.Properties;
  6. /**
  7. * 发邮件工具类
  8. */
  9. public final class MailUtils {
  10. private static final String USER = "838700991@qq.com"; // 发件人称号,同邮箱地址
  11. private static final String PASSWORD = "evlduwgrzsyxbgad"; // 如果是qq邮箱可以使户端授权码,或者登录密码
  12. /**
  13. *
  14. * @param to 收件人邮箱
  15. * @param text 邮件正文
  16. * @param title 标题
  17. */
  18. /* 发送验证信息的邮件 */
  19. public static boolean sendMail(String to, String text, String title){
  20. try {
  21. final Properties props = new Properties();
  22. props.put("mail.smtp.auth", "true");
  23. props.put("mail.smtp.host", "smtp.qq.com");
  24. // 发件人的账号
  25. props.put("mail.user", USER);
  26. //发件人的密码
  27. props.put("mail.password", PASSWORD);
  28. // 构建授权信息,用于进行SMTP进行身份验证
  29. Authenticator authenticator = new Authenticator() {
  30. @Override
  31. protected PasswordAuthentication getPasswordAuthentication() {
  32. // 用户名、密码
  33. String userName = props.getProperty("mail.user");
  34. String password = props.getProperty("mail.password");
  35. return new PasswordAuthentication(userName, password);
  36. }
  37. };
  38. // 使用环境属性和授权信息,创建邮件会话
  39. Session mailSession = Session.getInstance(props, authenticator);
  40. // 创建邮件消息
  41. MimeMessage message = new MimeMessage(mailSession);
  42. // 设置发件人
  43. String username = props.getProperty("mail.user");
  44. InternetAddress form = new InternetAddress(username);
  45. message.setFrom(form);
  46. // 设置收件人
  47. InternetAddress toAddress = new InternetAddress(to);
  48. message.setRecipient(Message.RecipientType.TO, toAddress);
  49. // 设置邮件标题
  50. message.setSubject(title);
  51. // 设置邮件的内容体
  52. message.setContent(text, "text/html;charset=UTF-8");
  53. // 发送邮件
  54. Transport.send(message);
  55. return true;
  56. }catch (Exception e){
  57. e.printStackTrace();
  58. }
  59. return false;
  60. }
  61. public static void main(String[] args) throws Exception { // 做测试用
  62. MailUtils.sendMail("18137884406@163.com","你好,这是一封测试邮件,无需回复。","测试邮件");
  63. System.out.println("发送成功");
  64. }
  65. }

你作为一个系统的开发者,你需要准备一个发邮件的邮箱,客户的邮箱是用来接收邮件的。
所以你会看到两个邮箱,一个是你们公司的(就是你的),还有一个是客户的。

163邮箱操作截图:
项目第9天 - 图5
项目第9天 - 图6
项目第9天 - 图7
项目第9天 - 图8

三、编写业务逻辑

需求分析:每当网站数据有更新之后,我就给我的订阅用户发送邮件。
拆分步骤:
1、创建一个定时任务,每隔1分钟去解析一下我要抓取的网站内容

  1. private List<Message> getNewCoinMessage() throws IOException {
  2. List<Message> list =new ArrayList<Message>();
  3. // 获取一个文档对象,一个网页就是一个文档
  4. Document document = Jsoup.connect("https://www.abuquant.com/rankDetail/final_score_rank/coin/day#selectExchange").get();
  5. // 获取该文档下面的,所有被class="newList" 修饰的元素
  6. Elements lists = document.getElementsByClass("newsList");
  7. // 获取newsList元素中的一个即可,在这个元素的第一个中,获取里面所有的li标签对象
  8. Elements lis = lists.first().getElementsByTag("li");
  9. System.out.println(lis.size());
  10. for (int i = 0; i < lis.size() ; i++) {
  11. Message message =new Message();
  12. Element element = lis.get(i);
  13. Element h3 = element.getElementsByTag("h3").first();
  14. String coinName= h3.text();
  15. System.out.println(coinName);
  16. message.setCoinName(coinName);
  17. Element xs8 = element.getElementsByClass("layui-col-xs8").first();
  18. Elements ps= xs8.getElementsByTag("p");
  19. for (int j = 0; j < ps.size(); j++) {
  20. Element p = ps.get(j);
  21. if(j==0){
  22. message.setLevel(p.text());
  23. }
  24. if(j==3){
  25. message.setScore(p.text());
  26. }
  27. if(j==4){
  28. message.setDate(p.text());
  29. }
  30. System.out.println(p.text());
  31. }
  32. list.add(message);
  33. }
  34. System.out.println(list);
  35. return list;
  36. }

2、比对网站上的数据,跟上一次信息的数据是否相同,如果不同,就给我的订阅用户发送邮件(我们只需要关心这一次抓取的数据的时间跟上一次是否一样即可)

  1. try {
  2. List<Message> messageList = getNewCoinMessage();
  3. // 是否发送邮件呢?
  4. String date = messageList.get(0).getDate();
  5. if(!date.equals(flagDate)){
  6. // 发送邮件
  7. sendEmail(messageList);
  8. flagDate = date;
  9. }else{
  10. System.out.println("数据无更新,无需发送邮件");
  11. }
  12. } catch (IOException e) {
  13. e.printStackTrace();
  14. }

3、将网站上的内容抓取出来封装成邮件的内容

  1. private void sendEmail(List<Message> messageList) {
  2. CustomerService customerService=new CustomerServiceImpl();
  3. // 先整理出要发送的邮件的内容
  4. // 数据库中找出符合条件的邮箱
  5. // 循环发送邮件
  6. StringBuffer stringBuffer = new StringBuffer();
  7. stringBuffer.append("感谢您订阅行情来了交易系统,本次的财富密码是:<br/>");
  8. for (int i = 0; i < messageList.size(); i++) {
  9. Message message = messageList.get(i);
  10. stringBuffer.append("<font color='red'>"+message.getCoinName()+"</font><br/>");
  11. stringBuffer.append(message.getLevel()+"<br/>");
  12. stringBuffer.append(message.getScore()+"<br/>");
  13. stringBuffer.append(message.getDate()+"<br/>");
  14. stringBuffer.append("<br/>");
  15. }
  16. stringBuffer.append("<h6>本交易系统提供的信息仅供参考,不作为任何投资理财的操作标准,币圈风险大,投资需谨慎.</h6>");
  17. String content=stringBuffer.toString();
  18. // 操作数据库,然后获取满足条件的邮箱地址
  19. List<String> listEmail = customerService.getSendEmailData(new Date());
  20. for (int i = 0; i < listEmail.size(); i++) {
  21. MailUtils.sendMail(listEmail.get(i),content,"行情来了");
  22. }
  23. }

4、每个邮箱,每天只能发送200~500条数据,为了便于拓展业务,我们可以多准备几个邮箱,每次发送的时候分均分配。
本次我需要给100个客户发送邮件,如果我 准备了2个邮箱,每个邮箱发送50个客户。
项目第9天 - 图9
发送邮件的逻辑:
现在比如有100条需要发送的邮件,3个发送邮件的邮箱,每个邮箱发送33封邮件,最后一封邮件,交给第一个邮箱。

  1. // 操作数据库,然后获取满足条件的邮箱地址
  2. List<String> listEmail = customerService.getSendEmailData(new Date());
  3. // 比如现在要发送100条数据,有两个邮箱 ,每个邮箱发送50条
  4. int count = listEmail.size();
  5. // 获取能发送邮件的所有邮箱:
  6. List<SendEmail> sendEmails =sendEmailService.getAllSendEmail();
  7. int useEmail = sendEmails.size();
  8. int num = count/useEmail;// 比如要发送100条数/ 3个邮箱= 33
  9. /**
  10. * 100 条数据 3 个邮箱
  11. * 第一个邮箱 i=0 0~32 begin=33*i 33
  12. * 第二个邮箱 i=1 33~65 66
  13. * 第三个邮箱 i=2 66~98 99
  14. */
  15. // 考虑 整除以及不能整除
  16. for (int i = 0; i < sendEmails.size(); i++) {
  17. SendEmail sendEmail = sendEmails.get(i);
  18. String user = sendEmail.getEmailName();
  19. String pass = sendEmail.getEmailPass();
  20. String smtp = sendEmail.getEmailType();
  21. // 必须训练到,这就是业务
  22. for (int j = num * i ; j < (i+1)* num; j++) {
  23. MailUtils.sendMail2(listEmail.get(i),content,"行情来了",user,pass,smtp);
  24. }
  25. }
  26. // 如果整除怎么办?
  27. if(count%useEmail != 0){
  28. int syNum = count % useEmail; // 1
  29. // 可以指定第一个邮箱给其发送几次即可
  30. SendEmail sendEmail = sendEmails.get(0);
  31. String user = sendEmail.getEmailName();
  32. String pass = sendEmail.getEmailPass();
  33. String smtp = sendEmail.getEmailType();
  34. for(int m= listEmail.size();m > (listEmail.size()-syNum);m--){
  35. MailUtils.sendMail2(listEmail.get(m),content,"行情来了",user,pass,smtp);
  36. }
  37. }

为我们最后一个阶段腾出一些时间。

如果这是一个标准的项目:
1、一般不会使用邮箱给用户发送
用短信通知客户,有新的行情,请注意查收。 接着使用微信公众号推送一个模板信息。
2、需要开发一个微信公众号,注册功能,登录功能
用户登录进来之后,展示一个非常详细的行情说明
3、用户可以在微信公众号页面,进行付款,更改自己的基本信息
4、后台可以统计业绩。查看支付明细等信息
5、推荐奖励 A用户推荐给B用户注册,并支付,可以得到现金奖励。用户可以体现。
没必要告诉你的你面试官,这个数据是爬的,而是你们公司的大数据开发人员设计的,交易模型,数据整理出来以后,你来处理后续的工作。