Spring Quartz「终于解决」

Spring Quartz「终于解决」Quartz是由Java编写的开源作业调度框架,Quartz可以定时或周期性的执行一些任务。

大家好,欢迎来到IT知识分享网。

Quartz是由Java编写的开源作业调度框架

Quartz可以定时或周期性的执行一些任务,比如:

  • 定时清除系统的缓存;
  • 定时更新热帖(微博热搜);
  • 定时发送系统邮件;
  • 再或者每天12点从网址扒拉一些每日更新的爱情动作片(狗头);

总结:在某一个有规律的时间点干某件事。并且时间的触发的条件可以非常复杂

一、Quartz 组件

Quartz实现,首先要理解Schedule(任务调度器),Job(作业任务)和Trigger(触发器)三者的关系

1. Job 任务

  • 我们要定时执行某件事情,然后我们把它写成一个接口,这就是 Job;
  • 只有一个方法void execute(JobExecutionContext context)。

2. JobDetail 任务详情

  • 具体任务类有了,还需要任务详情类( JobDetail )去实现 Job;
  • Quartz在执行Job时,需接收一个Job实现类,以便运行时通过反射机制实例化Job;
  • 描述Job的实现类及其它相关的静态信息—–JobDetail
  • 真正执行的任务并不是Job接口的实例,而是用反射的方式实例化的一个JobDetail实例

3. Trigger 触发器

  • 是一个类,描述触发Job执行的时间触发规则;
  • 主要有SimpleTriggerCronTrigger这两个子类:
    • SimpleTrigger:仅需触发一次或者以固定时间间隔周期执行适用;
    • CronTrigger:可以通过Cron表达式定义出各种复杂时间规则的调度方案。

4. Scheduler 调度器

  • 代表一个Quartz的独立运行容器;
  • TriggerJobDetail可以注册到Scheduler中;
  • 两者拥有各自的组及名称,组及名称必须唯一(两者可以相同,因为类型不同);
  • Scheduler定义了多个接口,允许外部通过组及名称访问和控制容器中Trigger和JobDetail。

关系描述:

  • Scheduler可以将Trigger绑定到某一JobDetail中,这样当Trigger触发时,对应的Job就被执行。
  • 一个Job可以对应多个Trigger,但一个Trigger只能对应一个Job。
  • 通过SchedulerFactory创建Scheduler实例,
  • Scheduler拥有一个SchedulerContext,保存着Scheduler上下文信息,Job和Trigger都可以访问
  • SchedulerContext内部通过Map维护这些上下文数据
  • SchedulerContext为保存和获取数据提供了多个put()和getXxx()的方法。

二、简单测试

helloword代码感受一下Quartz的工作流程:

package quartz;
 
import java.util.Date;
 
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
 
public class HelloWord implements Job{ 
   
 
	//实现自己的定时方法
	public void execute(JobExecutionContext arg0) throws JobExecutionException { 
   
		System.out.println("hello world " + new Date());
	}
	
}
package quartz;
 
import java.util.Date;
 
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.quartz.impl.StdSchedulerFactory;
 
 
public class SimpleExample { 
   
	
	public static void main(String[] args) throws SchedulerException{ 
   
		
		SimpleExample example=new SimpleExample();
		example.run();
	}
 
	public  void run() throws SchedulerException { 
   
		
		//获取scheduler实例
		Scheduler scheduler=StdSchedulerFactory.getDefaultScheduler();
		scheduler.start();
		
		//当前时间
		Date runTime=new Date();
		
		//定义一个 job 对象并绑定我们写的 HelloWord 类 
        // 真正执行的任务并不是Job接口的实例,而是用反射的方式实例化的一个JobDetail实例 
		JobDetail job=JobBuilder.newJob(HelloWord.class).withIdentity("job1","group1").build();
		
		// 定义一个触发器,startAt方法定义了任务应当开始的时间 .即下一个整数分钟执行
		Trigger trigger=TriggerBuilder.newTrigger().withIdentity("trigger1","group1").startAt(runTime).build();
		
		// 将job和Trigger放入scheduler
		scheduler.scheduleJob(job, trigger);
		
		//启动
		scheduler.start();
		
		try { 
   
			Thread.sleep(100);
		} catch (InterruptedException e) { 
   
			e.printStackTrace();
			scheduler.shutdown();
		}
		
	}
}

三、Spring+Quartz

在实际web应用中,我们通过使用spring框架来使用Quartz实现定时任务,一共有三种方式。
先导入依赖:

<dependency>
<span style="white-space:pre">	</span><groupId>org.springframework</groupId>
	<artifactId>spring-context-support</artifactId>
	<version>3.2.8.RELEASE</version>
</dependency>

3.1、方式一

配置三部分:1.任务类;2.调度触发方式;3.任务调用工厂:

<!-- 配置任务类 -->
	<bean id="quartzTask" class="org.springframework.scheduling.quartz.JobDetailBean">
		<property name="name" value="exampleJob"></property>
		<property name="quartzClass" value="spring.demo.pojo.QuartzTask"></property>
		<property name="jobDataAsMap">
			<map>
				<entry key="timeout" value="0" />
			</map>
		</property>
	</bean>
	<!-- 调度触发器方式 -->
	<bean id="cronTriggerBean" class="org.springframework.scheduling.quartz.CronTriggerBean">
		<property name="jobDetail">
			<ref bean="quartzTask" />
		</property>
		<!-- cron表达式 -->
		<property name="cronExpression">
			<value>10,15,20,25,30,35,40,45,50,55 * * * * ?</value>
		</property>
	</bean>
<!-- 调度工厂 -->
	<bean id="SpringJobSchedulerFactoryBean" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
		<property name="triggers">
			<list>
				<ref bean="cronTriggerBean" />
			</list>
		</property>
	</bean>

继承JobBean,并重写executeInternal(JobExecutionContext context)

<pre name="code" class="java">package spring.demo.pojo;
 
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.scheduling.quartz.QuartzJobBean;
 
 
//继承QuartzJobBean,并重写executeInternal方法
public class QuartzTask extends QuartzJobBean{ 
   
	
	private int timeout;
	private static int i = 0;
	
	//调度工厂实例化后,经过timeout时间开始执行调度
	public void setTimeout(int timeout) { 
   
		this.timeout = timeout;
	}
	
	@Override
	protected void executeInternal(JobExecutionContext context)
			throws JobExecutionException { 
   
		System.out.println("task running..."+ ++i + "进行中...");
	}
	
}

3.2、方式二

不需要继承基类,而是在spring-quratz.xml配置文件中,配置包装类,其他两个配置与上述一样:

<pre name="code" class="html"><!-- 包装工作类 -->
	<bean id="quartzJob" class="spring.demo.pojo.QuartzJob"></bean>
	<bean id="jobTask" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
		<!-- 需要包装的类,即调度类 -->
		<property name="targetObject">
			<ref bean="quartzJob" />
		</property>
		<!-- 调用类中的方法 -->
		<property name="targetMethod">
			<!-- 具体的方法 -->
			<value>work</value>
		</property>
	</bean>
	<!-- 调度触发器方式 -->
	<bean id="cronTriggerBean" class="org.springframework.scheduling.quartz.CronTriggerBean">
		<property name="jobDetail">
			<ref bean="quartzTask" />
		</property>
		<!-- cron表达式 -->
		<property name="cronExpression">
			<value>10,15,20,25,30,35,40,45,50,55 * * * * ?</value>
		</property>
	</bean>
<!-- 调度工厂 -->
	<bean id="SpringJobSchedulerFactoryBean" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
		<property name="triggers">
			<list>
				<ref bean="cronTriggerBean" />
			</list>
		</property>
	</bean>
package spring.demo.pojo;
 
public class QuartzJob { 
   
	
	public void work(){ 
   
		System.out.println("work running...");
	}
}

3.3、方式三

@Scheduled注解的方式实现,需要修改applicationContext.xml三个部分内容:

  1. xmlns添加:
xmlns:task="http://www.springframework.org/schema/task"
  1. xsi:schemaLocation添加:
http://www.springframework.org/schema/task  http://www.springframework.org/schema/task/spring-task-3.1.xsd 
  1. applicationContext.xml中添加:
<task:annotation-driven/>

最后在我们的定时任务上添加 @Scheduled注解 即可,一般都采用cronTrigger方式,即@Scheduled(cron=“相应的定时表达式”)

package spring.demo.service;
 
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
 
@Service
public class QuartzService { 
   
 
	@Scheduled(cron = "0/2 * * * * *")
	public void process() { 
   
		System.out.println("job run...");
	}
 
	public static void main(String[] args) throws InterruptedException { 
   
		ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
		while (true) { 
   
			System.out.println("main running...");
			Thread.sleep(10000);
		}
	}
}

建议采用第二种和第三种的方式实现Quartz比较简洁方便!

Cron表达式举例:

"30 * * * * ?" 每半分钟触发任务
"30 10 * * * ?" 每小时的10分30秒触发任务
"30 10 1 * * ?" 每天1点10分30秒触发任务
"30 10 1 20 * ?" 每月20号1点10分30秒触发任务
"30 10 1 20 10 ? *" 每年10月20号1点10分30秒触发任务
"30 10 1 20 10 ? 2011" 2011年10月20号1点10分30秒触发任务
"30 10 1 ? 10 * 2011" 2011年10月每天1点10分30秒触发任务
"30 10 1 ? 10 SUN 2011" 2011年10月每周日1点10分30秒触发任务
"15,30,45 * * * * ?" 每15秒,30秒,45秒时触发任务
"15-45 * * * * ?" 15到45秒内,每秒都触发任务
"15/5 * * * * ?" 每分钟的每15秒开始触发,每隔5秒触发一次
"15-30/5 * * * * ?" 每分钟的15秒到30秒之间开始触发,每隔5秒触发一次
"0 0/3 * * * ?" 每小时的第0分0秒开始,每三分钟触发一次
"0 15 10 ? * MON-FRI" 星期一到星期五的10点15分0秒触发任务
"0 15 10 L * ?" 每个月最后一天的10点15分0秒触发任务
"0 15 10 LW * ?" 每个月最后一个工作日的10点15分0秒触发任务
"0 15 10 ? * 5L" 每个月最后一个星期四的10点15分0秒触发任务
"0 15 10 ? * 5#3" 每个月第三周的星期四的10点15分0秒触发任务

这有的友友就郁闷了,这 cron 表达式这么麻烦怎么搞???莫急

cron 表达式在线生成:https://cron.qqe2.com/

四、项目中使用

帖子热度计算

每次发生点赞(给帖子点赞)、评论(给帖子评论)、加精的时候,就将这些帖子信息存入缓存 Redis 中,然后通过分布式的定时任务 Spring Quartz,每隔一段时间就从缓存中取出这些帖子进行计算分数。

帖子分数/热度计算公式:分数(热度) = 权重 + 发帖距离天数

  1. 自定义一个QuartzConfig 配置类,往Spring容器中注入JobDetail、Trigger的factoryBean实例:
    将SimpleTrigger绑定到postScoreRefreshJobDetail,当Trigger触发时(每五分钟触发一次),对应的job(postScoreRefreshJob)就被执行
package com.greate.community.config;

import com.greate.community.quartz.PostScoreRefreshJob;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.quartz.JobDetailFactoryBean;
import org.springframework.scheduling.quartz.SimpleTriggerFactoryBean;

/** * Spring Quartz 配置类,用于将数据存入数据库,以后直接从数据库中调用数据 */
@Configuration
public class QuartzConfig { 
   

    /** * 刷新帖子分数任务 * @return */
    @Bean
    public JobDetailFactoryBean postScoreRefreshJobDetail() { 
   
        JobDetailFactoryBean factoryBean = new JobDetailFactoryBean();
        factoryBean.setJobClass(PostScoreRefreshJob.class);
        factoryBean.setName("postScoreRefreshJob");
        factoryBean.setGroup("communityJobGroup");
        factoryBean.setDurability(true);
        factoryBean.setRequestsRecovery(true);
        return factoryBean;
    }

    /** * 刷新帖子分数触发器 * @return */
    @Bean
    public SimpleTriggerFactoryBean postScoreRefreshTrigger(JobDetail postScoreRefreshJobDetail) { 
   
        SimpleTriggerFactoryBean factoryBean = new SimpleTriggerFactoryBean();
        factoryBean.setJobDetail(postScoreRefreshJobDetail);
        factoryBean.setName("postScoreRefreshTrigger");
        factoryBean.setGroup("communityTriggerGroup");
        factoryBean.setRepeatInterval(1000 * 60 * 5); // 5分钟刷新一次
        factoryBean.setJobDataMap(new JobDataMap());
        return factoryBean;
    }
}

PostScoreRefreshJob 定时执行计算帖子热度,结果存入数据库,并将贴子插入elasticsearch服务器(同步搜索数据)。

package com.greate.community.quartz;

import com.greate.community.entity.DiscussPost;
import com.greate.community.service.DiscussPostService;
import com.greate.community.service.ElasticsearchService;
import com.greate.community.service.LikeService;
import com.greate.community.util.CommunityConstant;
import com.greate.community.util.RedisKeyUtil;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundSetOperations;
import org.springframework.data.redis.core.RedisTemplate;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

/** * 帖子分数计算刷新 */
public class PostScoreRefreshJob implements Job, CommunityConstant { 
   

    private static final Logger logger = LoggerFactory.getLogger(PostScoreRefreshJob.class);

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private DiscussPostService discussPostService;

    @Autowired
    private LikeService likeService;

    @Autowired
    private ElasticsearchService elasticsearchService;

    // Epoch 纪元
    private static final Date epoch;

    static { 
   
        try { 
   
            epoch = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse("2014-01-01 00:00:00");
        } catch (ParseException e) { 
   
            throw new RuntimeException("初始化 Epoch 纪元失败", e);
        }
    }

    //实现定时方法
    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException { 
   
        String redisKey = RedisKeyUtil.getPostScoreKey();
        BoundSetOperations operations = redisTemplate.boundSetOps(redisKey);

        if (operations.size() == 0) { 
   
            logger.info("[任务取消] 没有需要刷新的帖子");
            return ;
        }

        logger.info("[任务开始] 正在刷新帖子分数: " + operations.size());
        while (operations.size() > 0) { 
   
            this.refresh((Integer) operations.pop());
        }
        logger.info("[任务结束] 帖子分数刷新完毕");
    }

    /** * 刷新帖子分数 * @param postId */
    private void refresh(int postId) { 
   
        DiscussPost post = discussPostService.findDiscussPostById(postId);

        if (post == null) { 
   
            logger.error("该帖子不存在: id = " + postId);
            return ;
        }

        // 是否加精
        boolean wonderful = post.getStatus() == 1;
        // 评论数量
        int commentCount = post.getCommentCount();
        // 点赞数量
        long likeCount = likeService.findEntityLikeCount(ENTITY_TYPE_POST, postId);

        // 计算权重
        double w = (wonderful ? 75 : 0) + commentCount * 10 + likeCount * 2;
        // 分数 = 权重 + 发帖距离天数
        double score = Math.log10(Math.max(w, 1))
                + (post.getCreateTime().getTime() - epoch.getTime()) / (1000 * 3600 * 24);
        // 更新帖子分数
        discussPostService.updateScore(postId, score);
        // 同步搜索数据
        post.setScore(score);
        elasticsearchService.saveDiscusspost(post);

    }
}

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://yundeesoft.com/26335.html

(0)
上一篇 2023-05-09 20:00
下一篇 2023-05-15 10:00

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

关注微信