前端
前端主要采用的是knockout+jquery形式
后端
java基础
判断一个类是否实现了某个接口或者继承自某个类
isAssignableFrom
//测试是否实现了父类
boolean re1= Object.class.isAssignableFrom(IsAssignableFromTest.class);
//测试是否实现了接口
boolean re2=Serializable.class.isAssignableFrom(IsAssignableFromTest.class);
quartz
主要参考内容:Quartz 2.2 动态添加、修改和删除定时任务
spring使用@Autowired注入Job中的实体为null
应该是因为quartz的job是由quartz自己的context管理的,没有交给spring。网上有很多方法,但是我采用的方法(就是测试有用的方法如下),但是该方法似乎不是很好,感觉配置比较好,可是没有找到可以用的
关键代码
SpringBeanAutowiringSupport.processInjectionBasedOnCurrentContext(this);
package com.ufgov.quartz.job;
import java.util.Date;
import com.ufgov.service.SysAutotaskMonitorService;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.support.SpringBeanAutowiringSupport;
/**
* 测试Job,测试完成后可以删除
*/
@Component
public class MyJob implements Job {
@Autowired
private SysAutotaskMonitorService sysAutotaskMonitorServiceImpl;
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
// TODO: 2018/4/17 解决@Autowired注入的问题
SpringBeanAutowiringSupport.processInjectionBasedOnCurrentContext(this);
System.out.println("测试Job开始");
boolean isSuccess = true;
try {
System.out.println(jobExecutionContext.getJobDetail().getKey().getName());
} catch (Exception e) {
isSuccess = false;
e.printStackTrace();
} finally {
sysAutotaskMonitorServiceImpl.jobHelper(jobExecutionContext,isSuccess,new Date());
System.out.println("测试Job结束");
}
}
}
startNow
当使用cron表达式时,startNow是不会生效的。目前看startNow只适用于SimpleTrigger。
定时任务的动态参数传递
JobDetail有一个JobDataMap可以传递参数
JobDetail jobDetail = JobBuilder.newJob(clazz).withIdentity(jobKey).build();
//设置调度参数
String autotaskParam = autotask.getAutotaskParam();
if(StringUtils.isNotEmpty(autotaskParam)){
Map<String, Object> map = new HashMap<String, Object>();
map = (new Gson()).fromJson(autotaskParam, map.getClass());
jobDetail.getJobDataMap().putAll(map);
}
手动触发一个定时任务(立刻执行一次)
SchedulerFactoryInstance().getScheduler().triggerJob(new JobKey(autotaskId.toString(), autotaskId.toString()));
CronTrigger和SimpleTrigger
CronTrigger的使用场景是:使用cron表达式可以表述出来的定时规律
SimpleTrigger的使用场景是:从开始日期到结束日期,每隔多久执行一次,共执行多少次
quartz的使用套路(不考虑分布式等复杂场景)
1.相关类和接口的简单描述
- CronTrigger和SimpleTrigger都实现接口Trigger
- TriggerKey描述的Trigger的信息;JobKey描述的是Job的信息
- SimpleTrigger和CronTriggerSimple分别由ScheduleBuilder和CronScheduleBuilder来生成。而ScheduleBuilder和CronScheduleBuilder则是通过TriggerBuilder的withSchedule方法来生成Trigger的
套路为:生成Trigger->生成Job->加入Schedule
public static ResponseState addJob(SysAutotask autotask) {
ResponseState state = new ResponseState();
try {
// 任务名,任务组,任务执行类
Class clazz = Class.forName(autotask.getAutotaskBean());
boolean f = Job.class.isAssignableFrom(clazz);
if(!f){
logger.error(autotask.getAutotaskBean()+"没有实现接口org.quartz.Job");
state.setSuccess(false);
state.setMessage(autotask.getAutotaskBean()+"没有实现接口org.quartz.Job");
return state;
}
TriggerKey triggerKey = new TriggerKey(autotask.getAutotaskName(), autotask.getAutotaskName());
// 触发器
TriggerBuilder<Trigger> triggerBuilder = TriggerBuilder.newTrigger();
// 触发器名,触发器组
triggerBuilder.withIdentity(triggerKey);
triggerBuilder.startNow();
Trigger trigger = null;
if(autotask.getAutotaskType() == 1){
//数据校验
SimpleDateFormat formatDate = new SimpleDateFormat("yyyy-MM-dd");
String startDateStr = autotask.getStartDate(),endDateStr = autotask.getEndDate();
//为开始和结束时间设置默认值
if(StringUtils.isEmpty(startDateStr)){
startDateStr = formatDate.format(new Date());
}
if(StringUtils.isEmpty(endDateStr)){
endDateStr = "2100-12-31";
}
Date startDate = formatDate.parse(startDateStr);
Date endDate = formatDate.parse(endDateStr);
//设置间隔和执行次数
// 如果设置的运行次数为空,则设置为999999
SimpleScheduleBuilder simpleScheduleBuilder = SimpleScheduleBuilder.simpleSchedule();
simpleScheduleBuilder.withIntervalInSeconds(autotask.getTaskinterval());
simpleScheduleBuilder.withRepeatCount(autotask.getRunTimes() == null ? 999999 : autotask.getRunTimes());
//触发器名称 触发器分组 执行次数 间隔时间(毫秒级)
// SimpleTrigger实现Trigger接口的子接口。此处只指定了开始执行定时任务的时间,使用默认的重复次数(0次)和重复间隔(0秒)
trigger = triggerBuilder
.startAt(startDate)
.endAt(endDate)
.withSchedule(simpleScheduleBuilder)
.build();
}else if(autotask.getAutotaskType() == 2){
CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(resolveCron(autotask));
// 触发器时间设定
triggerBuilder.withSchedule(cronScheduleBuilder);
// 创建CronTrigger对象
trigger = triggerBuilder.build();
}
//设置job相关信息
JobKey jobKey = new JobKey(autotask.getAutotaskName(), autotask.getAutotaskName());
JobDetail jobDetail= JobBuilder.newJob(clazz).withIdentity(jobKey).build();
// 调度容器设置JobDetail和Trigger
Scheduler scheduler = schedulerFactory.getScheduler();
scheduler.scheduleJob(jobDetail, trigger);
// 启动
if (!scheduler.isShutdown()) {
scheduler.start();
}
}catch (ClassNotFoundException e){
e.printStackTrace();
state.setSuccess(false);
state.setMessage("没有找到类"+autotask.getAutotaskBean());
// TODO: 18-4-16 这里要设置RuntimeException吗?
// throw new RuntimeException(e);
}catch (SchedulerException e){
e.printStackTrace();
state.setSuccess(false);
state.setMessage("调度错误:");
}catch (ParseException e){
e.printStackTrace();
state.setSuccess(false);
state.setMessage("日期格式错误:"+autotask.getStartDate()+","+autotask.getEnabled());
}
return state;
}
关于quartz的cron表达式
web容器初始化时执行一次
可以采用两种方法
1.配置listener
该方法存在的问题是listener生效时,Spring的bean并没有装配完成,因此使用***@Autowired注解的属性获取到的是null***,但是可以通过动态的获取***ApplicationContext***来解决问题
public void contextDestroyed(ServletContextEvent e) {
logger.info("系统停止...");
}
public void contextInitialized(ServletContextEvent e) {
logger.info("系统初始化开始...");
//获取bean
WebApplicationContext webApplicationContext= WebApplicationContextUtils.getWebApplicationContext(e.getServletContext());
SysAutotaskService sysAutotaskServiceImpl = (SysAutotaskService) webApplicationContext.getBean(SysAutotaskService.class);
web.xml
<!--系统参数初始化监听器 -->
<listener>
<listener-class>com.ufgov.system.StartInit</listener-class>
</listener>
listener
package com.ufgov.system;
import com.ufgov.util.Constans;
import com.ufgov.util.MemcachedUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.CollectionUtils;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import java.util.Date;
import java.util.List;
public class StartInit implements ServletContextListener {
static final Logger logger = LoggerFactory.getLogger(StartInit.class);
public void contextDestroyed(ServletContextEvent e) {
logger.info("系统停止...");
}
public void contextInitialized(ServletContextEvent e) {
logger.info("系统初始化开始...");
logger.info("...清除报备人...");
List<String> list = MemcachedUtils.getKeysForMap();
if(!CollectionUtils.isEmpty(list)) {
for (String str : list) {
if (StringUtils.isNotEmpty(str) && str.startsWith(Constans.REPORT_INFO)){
MemcachedUtils.delete(str);
}
}
}
list = MemcachedUtils.getKeysForMap();
logger.info("...清除报备人结束...");
//TODO:校验这个是不是默认无限时长
MemcachedUtils.add(Constans.REPORT_COUNT_KEY,Constans.REPORT_COUNT,new Date(0));
logger.info("...设置报备人数上线为:"+MemcachedUtils.get(Constans.REPORT_COUNT_KEY));
logger.info("系统初始化结束...");
}
}
2.实现InitializingBean的afterPropertiesSet方法
当Spring初始化继承了InitializingBean的类之后,Spring会执行afterPropertiesSet方法。也可以用init-method方法,这两种方式同时使用时,afterPropertiesSet先于init-method调用。
该方法也存在一个问题,
使用@Component注解的类执行两次
原因
- 可能是在web.xml中配置的问题,代码如下
......
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:conf/applicationContext.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
......
<servlet>
<servlet-name>spring</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:conf/spring-mvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
......
更深入的原因未知,有时间研究一下
代码
package com.ufgov.system;
import com.ufgov.entity.SysAutotask;
import com.ufgov.quartz.QuartzManager;
import com.ufgov.service.SysAutotaskService;
import org.apache.commons.collections.CollectionUtils;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 初始化容器的时候执行<br>
* 1.加载系统参数
* 2.加载定时任务(记得分布式的处理)
*/
// 18-4-16 使用@Component会导致类被初始化两次,原因可能是在web.xml中的配置,使用@Service可以解决
//@Component
@Service
public class InitListener implements InitializingBean{//,ServletContextAware
//通过sring容器生产出实现了InitializingBean接口的类的实例后,它就会调用afterPropertiesSet方法,通过这个方法,你可以检查你的bean是否正确地被初始化了.当然,你也可以用init-method方法.这两种方式可以同时使用,调用的顺序为init-method后调用.
//在Spring中,凡是实现ServletContextAware接口的类,都可以取得ServletContext
private static Logger logger = Logger.getLogger(InitListener.class);
@Autowired
private SysAutotaskService sysAutotaskServiceImpl;
@Override
public void afterPropertiesSet() throws Exception {
// TODO: 18-4-16 改写原来加载系统参数的方法
//加载定时任务
// TODO: 18-4-16 分布式的处理
logger.info("-----------------开始加载定时任务----------------");
List<SysAutotask> autotaskList = sysAutotaskServiceImpl.queryForList();
if(!CollectionUtils.isEmpty(autotaskList)){
for (SysAutotask autotask : autotaskList) {
if(autotask.getEnabled() == 1) {
logger.info("加载"+autotask.getAutotaskName());
QuartzManager.addJob(autotask);
logger.info(autotask.getAutotaskName()+"加载完成");
}
}
}
logger.info("-----------------定时任务加载完成----------------");
}
/**
* 获取ServletContext
* @param servletContext
*/
// @Override
// public void setServletContext(ServletContext servletContext) {
// }
}
ServletContextAware的理解
在Spring中,凡是实现ServletContextAware接口的类,都可以取得ServletContext。
分布式任务的处理
思路
多台服务器同时执行定时任务,不仅是资源的浪费,更有可能造成定时任务的错误执行。所以就要指定多台服务器中的某一台来执行定时任务。
本来想自己实现:在服务器启动时,其中一台服务器加载定时任务,其它服务器就不再加载。但是却满足不了定时任务的动态添加。当从前端动态的增删改定时任务时,无法确定路由到哪台服务器,***这样虽然也避免了多台服务器的同时执行,但是却不利于对定时任务的监控,另外如果某台服务器停止,那么这台服务器上的定时任务也就停止了。***所以不万不得已也不想采用这中方法。
通过网络学习,得知quartz本身就具备了分布式任务处理的能力。如加载时的重复任务甄别,分布式任务中的一台机器停止后,有其它机器接替继续执行等。所以这个就很好的解决了我的需求。
不考虑分布式和动态增删改
核心配置如下。即使用类+方法的方式指定定时任务的处理方式
<bean id="autoAccessInfoQuartzDitail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
<!--处理定时任务的class-->
<property name="targetObject" ref="autoAccessInfoQuartz"></property>
<!--处理定时任务的方法-->
<property name="targetMethod">
<value>work</value>
</property>
</bean>
全量配置实例(有删减):
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN 2.0//EN" "http://www.springframework.org/dtd/spring-beans-2.0.dtd">
<beans default-autowire="byName" default-lazy-init="true">
<!-- 定时器配置目标类_start -->
<!-- 自动统计当天信息并记录 -->
<bean id="autoAccessInfoQuartz" class="com.ufgov.quartz.AutoAccessInfoQuartz">
</bean>
<!-- 定时器配置目标类_end -->
<!-- 定时器配置_Start -->
<!-- 自动统计当天信息并记录 -->
<bean id="autoAccessInfoQuartzDitail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
<property name="targetObject" ref="autoAccessInfoQuartz"></property>
<property name="targetMethod">
<value>work</value>
</property>
</bean>
<!-- 定时器配置_End -->
<!-- 定义时间间隔触发器_Start -->
<!-- 自动统计当天信息并记录 每天23:00执行统计 <value>0 30 23 * * ?</value>-->
<bean id="autoAccessInfoQuartzTigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
<property name="jobDetail" ref="autoAccessInfoQuartzDitail"></property>
<property name="cronExpression">
<value>0 0 23 * * ?</value>
</property>
</bean>
<!-- 定义时间间隔触发器_End -->
<bean id="SysUserLoginValidateQuartz" class="com.ufgov.quartz.SysUserLoginValidateQuartz">
</bean>
<bean id="SysUserLoginValidateQuartzDitail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
<property name="targetObject" ref="SysUserLoginValidateQuartz"></property>
<property name="targetMethod">
<value>work</value>
</property>
</bean>
<bean id="SysUserLoginValidateQuartzTigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
<property name="jobDetail" ref="SysUserLoginValidateQuartzDitail"></property>
<property name="cronExpression">
<!-- <value>0 0/2 * * * ?</value> -->
<value>0 0 23 * * ?</value>
</property>
</bean>
<!-- 启动定时器 -->
<!-- 自动统计当天信息并记录 -->
<bean id="autoAccessInfoQuartzStartJob" lazy-init="false" autowire="no" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="triggers">
<list>
<ref bean="SysUserLoginValidateQuartzTigger"/>
</list>
</property>
</bean>
</beans>
Java代码,有删除
package com.ufgov.quartz;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.ufgov.service.SysUserLoginValidateService;
/**
* 定时清除登录限制次数
*/
@Component
public class SysUserLoginValidateQuartz {
@Autowired
SysUserLoginValidateService userLoginValidateServiceImpl;
public void work() {
userLoginValidateServiceImpl.updateUserLoginIndex();
}
}
可以看出,如果不考虑分布式和动态的增删改,那么这种方法是非常直观和简单的。
不考虑分布式的定时任务动态增删改
如下QuartzManager就很好的解决了**单机定时任务的动态增删改问题。
需要说明的是,这种方法无需继承QuartzJobBean,直接实现Job接口即可,且不需要在Spring的配置文件中做特殊配置。
package com.ufgov.quartz;
import com.ufgov.entity.SysAutotask;
import com.ufgov.util.page.ResponseState;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import org.springframework.scheduling.quartz.QuartzJobBean;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
*
* 定时任务管理类<br>
* 这里的问题是目前是除了id之外的所有参数均可以修改,因此jobKey和TriggerKey均使用<b>id</b>来标识
* 这个id在每个Job中会用得到
*
* 2018-4-19 因为要使用分布式调度,如果使用QuartzManager就无法分布式处理。具体原因应该与Spring和quartz的机制有关,不做深入。
* 因此,改为使用QuartzmanagerService来调度任务
*
*/
@Deprecated
public class QuartzManager {
private static final Logger logger = Logger.getLogger(QuartzManager.class);
private static SchedulerFactory schedulerFactory = getSchedulerFactoryInstance();
public static SchedulerFactory getSchedulerFactoryInstance(){
if(schedulerFactory == null){
schedulerFactory = new StdSchedulerFactory();
}
return schedulerFactory;
}
/**
* @param autotask
* @return
* @Description: 添加一个定时任务<br>
* 实际调用的是重载的addJob()方法
*/
public static ResponseState addJob(SysAutotask autotask) {
ResponseState state = new ResponseState();
try {
// TODO: 2018/4/17 校验autotask_code是否重复
// 任务名,任务组,任务执行类
Class clazz = Class.forName(autotask.getAutotaskBean());
if (!QuartzJobBean.class.isAssignableFrom(clazz)) {
logger.error(autotask.getAutotaskBean() + "没有实现接口org.springframework.scheduling.quartz.QuartzJobBean");
state.setSuccess(false);
state.setMessage(autotask.getAutotaskBean() + "没有实现接口org.springframework.scheduling.quartz.QuartzJobBean");
return state;
}
TriggerKey triggerKey = new TriggerKey(autotask.getAutotaskId().toString(), autotask.getAutotaskId().toString());
// 触发器
TriggerBuilder<Trigger> triggerBuilder = TriggerBuilder.newTrigger();
// 触发器名,触发器组
triggerBuilder.withIdentity(triggerKey);
triggerBuilder.startNow();
Trigger trigger = null;
if (autotask.getAutotaskType() == 1) {
// TODO: 18-4-16 这个要不要做成在controller中处理,然后数据库设置非空???
//数据校验
SimpleDateFormat formatDate = new SimpleDateFormat("yyyy-MM-dd");
String startDateStr = autotask.getStartDate(), endDateStr = autotask.getEndDate();
//为开始和结束时间设置默认值
if (StringUtils.isEmpty(startDateStr)) {
startDateStr = formatDate.format(new Date());
}
if (StringUtils.isEmpty(endDateStr)) {
endDateStr = "2100-12-31";
}
Date startDate = formatDate.parse(startDateStr);
Date endDate = formatDate.parse(endDateStr);
//设置间隔和执行次数
// 如果设置的运行次数为空,则设置为999999
SimpleScheduleBuilder simpleScheduleBuilder = SimpleScheduleBuilder.simpleSchedule();
simpleScheduleBuilder.withIntervalInSeconds(autotask.getTaskinterval());
simpleScheduleBuilder.withRepeatCount(autotask.getRunTimes() == null ? 999999 : autotask.getRunTimes());
//触发器名称 触发器分组 执行次数 间隔时间(毫秒级)
// SimpleTrigger实现Trigger接口的子接口。此处只指定了开始执行定时任务的时间,使用默认的重复次数(0次)和重复间隔(0秒)
trigger = triggerBuilder
.startAt(startDate)
.endAt(endDate)
.withSchedule(simpleScheduleBuilder)
.build();
} else if (autotask.getAutotaskType() == 2) {
CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(resolveCron(autotask));
// 触发器时间设定
triggerBuilder.withSchedule(cronScheduleBuilder);
// 创建CronTrigger对象
trigger = triggerBuilder.build();
}
//设置job相关信息
JobKey jobKey = new JobKey(autotask.getAutotaskId().toString(), autotask.getAutotaskId().toString());
JobDetail jobDetail = JobBuilder.newJob(clazz).withIdentity(jobKey).build();
// 调度容器设置JobDetail和Trigger
Scheduler scheduler = schedulerFactory.getScheduler();
scheduler.scheduleJob(jobDetail, trigger);
// 启动
if (!scheduler.isShutdown()) {
scheduler.start();
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
state.setSuccess(false);
state.setMessage("没有找到类" + autotask.getAutotaskBean());
// TODO: 18-4-16 这里要设置RuntimeException吗?
// throw new RuntimeException(e);
} catch (SchedulerException e) {
e.printStackTrace();
state.setSuccess(false);
state.setMessage("调度错误:");
} catch (ParseException e) {
e.printStackTrace();
state.setSuccess(false);
state.setMessage("日期格式错误:" + autotask.getStartDate() + "," + autotask.getEnabled());
}
return state;
}
/**
* 移除job
*
* @param autotask
*/
public static void removeJob(SysAutotask autotask) {
try {
Scheduler scheduler = schedulerFactory.getScheduler();
TriggerKey triggerKey = TriggerKey.triggerKey(autotask.getAutotaskId().toString(), autotask.getAutotaskId().toString());
scheduler.pauseTrigger(triggerKey);// 停止触发器
scheduler.unscheduleJob(triggerKey);// 移除触发器
scheduler.deleteJob(JobKey.jobKey(autotask.getAutotaskId().toString(), autotask.getAutotaskId().toString()));// 删除任务
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 修改一个任务,两种方法<br>
* 1.删除之后添加
* 2.修改
* 第一种比较简单,用第一种
* @param autotask
*/
public static void modifyJob(SysAutotask autotask) {
//TOTEST:使用这种方法不知道有没有问题,注意观察一下
removeJob(autotask);
addJob(autotask);
}
/**
* 生成cron表达式
*
* @param autotask
*/
private static String resolveCron(SysAutotask autotask) {
StringBuilder cronSb = new StringBuilder();
if (autotask.getAutotaskType() == 1) {//间隔运行
return "";
} else if (autotask.getAutotaskType() == 2) {//定期运行
//这里要注意处理周和日的冲突问题,在cron表达式中体现在?的使用上
Short sceduleCrontype = autotask.getScheduleCrontype();
cronSb.append("0 ")//秒
//现在的情况是时分总是要的,因此放在外层
.append(autotask.getMinuteOfDay().toString()).append(" ")//分
.append(autotask.getHourOfDay().toString()).append(" ");//时
switch (sceduleCrontype) {
//0:每年 1:每月 2:每周 3:每日
case 0://每年
//正常情况下,这几个值都不能为null,需要在前边校验,此处略去校验
cronSb.append(autotask.getDayOfMonth().toString()).append(" ")//日
.append(autotask.getMonthOfYear().toString()).append(" ? *");//月
break;
case 1://每月
cronSb.append(autotask.getDayOfMonth().toString()).append(" ")//日
.append("* ? *");
break;
case 2://每周
cronSb.append("? ").append(autotask.getDayOfWeek()).append(" *");//月
break;
case 3://每日
cronSb.append("* * ? *");//月
break;
}
}
return cronSb.toString();
}
}
其中一个job的实现
package com.ufgov.quartz.job;
import com.ufgov.service.SysAutotaskMonitorService;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.quartz.QuartzJobBean;
import org.springframework.web.context.support.SpringBeanAutowiringSupport;
import java.util.Date;
/**
* 测试Job,测试完成后可以删除
*/
public class MyJob2 implements Job {
//注意这里的Spring bean注入问题
@Autowired
private SysAutotaskMonitorService sysAutotaskMonitorServiceImpl;
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
// TODO: 2018/4/17 解决@Autowired注入的问题
SpringBeanAutowiringSupport.processInjectionBasedOnCurrentContext(this);
System.out.println("测试Job开始");
boolean isSuccess = true;
try {
System.out.println(jobExecutionContext.getJobDetail().getKey().getName());
} catch (Exception e) {
isSuccess = false;
e.printStackTrace();
} finally {
sysAutotaskMonitorServiceImpl.jobHelper(jobExecutionContext,isSuccess,new Date());
System.out.println("测试Job结束");
}
}
}
分布式定时任务
参考资料
quartz集群分布式(并发)部署解决方案-Spring
spring与quartz整合实现分布式动态创建,删除,改变执行时间定时任务(mysql数据库)
参考资料带给我的主要是不能使用静态的QuartzManager类,要想实现分布式必须要持久,所以自定义job继承需要QuartzJobBean,然后需要将原来的QuartzManager改为Spring可以管理的Service
QuartzJobBean
QuartzJobBean是一个实现了Job接口的抽象类,源码如下
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package org.springframework.scheduling.quartz;
import java.lang.reflect.Method;
import java.util.Map;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.PropertyAccessorFactory;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
public abstract class QuartzJobBean implements Job {
private static final Method getSchedulerMethod;
private static final Method getMergedJobDataMapMethod;
public QuartzJobBean() {
}
public final void execute(JobExecutionContext context) throws JobExecutionException {
try {
Scheduler scheduler = (Scheduler)ReflectionUtils.invokeMethod(getSchedulerMethod, context);
Map<?, ?> mergedJobDataMap = (Map)ReflectionUtils.invokeMethod(getMergedJobDataMapMethod, context);
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
MutablePropertyValues pvs = new MutablePropertyValues();
pvs.addPropertyValues(scheduler.getContext());
pvs.addPropertyValues(mergedJobDataMap);
bw.setPropertyValues(pvs, true);
} catch (SchedulerException var6) {
throw new JobExecutionException(var6);
}
this.executeInternal(context);
}
protected abstract void executeInternal(JobExecutionContext var1) throws JobExecutionException;
static {
try {
Class<?> jobExecutionContextClass = ClassUtils.forName("org.quartz.JobExecutionContext", QuartzJobBean.class.getClassLoader());
getSchedulerMethod = jobExecutionContextClass.getMethod("getScheduler");
getMergedJobDataMapMethod = jobExecutionContextClass.getMethod("getMergedJobDataMap");
} catch (Exception var1) {
throw new IllegalStateException("Incompatible Quartz API: " + var1);
}
}
}
如果自定义job没有继承QuartzJobBean而是直接实现Job接口,那么只要直接实现Job接口的execute方法即可。继承QuartzJobBean的好处是可以将定时任务持久化到数据库,从而将分布式任务的调度问题交给quartz自己解决,我们不需要关注处理的细节。
Quartz的11张表
这个在quartz的源码下就有,我的路径如下quartz-2.2.3/docs/dbTables***,我用的是Oracle数据库,所以执行对应的Oracle语句即可
Quartz配置文件
1.properties
这个在quartz中也是有实例的,我在quartz-2.2.3/examples/example11*下找到quartz.properties这个文件,我做了一下修改,以便与Spring结合(参考了网络资料)
#============================================================================
# Configure Main Scheduler Properties
#============================================================================
org.quartz.scheduler.instanceName = ClusteredScheduler
org.quartz.scheduler.instanceId = AUTO
org.quartz.scheduler.skipUpdateCheck = true
#============================================================================
# Configure ThreadPool
#============================================================================
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount = 10
org.quartz.threadPool.threadPriority = 5
#============================================================================
# Configure JobStore
#============================================================================
org.quartz.jobStore.misfireThreshold = 60000
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
org.quartz.jobStore.useProperties = false
org.quartz.jobStore.tablePrefix = QRTZ_
org.quartz.jobStore.isClustered = true
org.quartz.jobStore.clusterCheckinInterval =15000
applicationContext-quartz.xml(参考网络资料)
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:task="http://www.springframework.org/schema/task" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task.xsd">
<description>
spring任务调度,quartz任务调度,该文件是空配置,确保能启动定时任务,具体业务配置需在页面进行配置即可,不用描述在该文件中
</description>
<task:annotation-driven executor="quartzTaskExecutor"/>
<task:executor id="quartzTaskExecutor" keep-alive="900" pool-size="10" queue-capacity="20"/>
<!-- Quartz集群Schduler -->
<bean id="clusterQuartzScheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<!-- Triggers集成 动态配置 -->
<!-- <property name="triggers">
<list>
<ref bean="allocationPlanTrigger" />
</list>
</property> -->
<!-- quartz配置文件路径, 指向cluster配置 -->
<property name="configLocation" value="classpath:conf/quartz.properties"/>
<!-- 启动时延期2秒开始任务 -->
<property name="startupDelay" value="2"/>
<!-- 保存Job数据到数据库所需的数据源 -->
<property name="dataSource" ref="dataSource"/>
<!-- Job接受applicationContext的成员变量名 -->
<property name="schedulerContextAsMap">
<map>
<!-- spring管理的服务需要放到这里,才能够注入成功 -->
<description>schedulerContextAsMap</description>
<!--<entry key="allocationOrderService" value-ref="allocationOrderService"/>-->
</map>
</property>
<property name="applicationContextSchedulerContextKey" value="applicationContext"/>
<!-- 修改job时,更新到数据库 -->
<property name="overwriteExistingJobs" value="true"/>
</bean>
<!-- 定时任务 页面动态配置-->
<!-- <bean id="myTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
<property name="jobDetail" ref="myTask" />
每5秒执行一次
<property name="cronExpression" value="*/5 * * * * ? " />
</bean>
<bean id="myTask" class="org.springframework.scheduling.quartz.JobDetailFactoryBean">
<property name="durability" value="true" />
<property name="jobClass" value="com.yestae.tss.common.quartz.task.MyTask" />
</bean> -->
</beans>
service
package com.ufgov.service;
import com.ufgov.entity.SysAutotask;
import com.ufgov.util.page.ResponseState;
/**
*
* 定时任务管理
*
*/
public interface QuartzManagerService {
/**
* @param autotask
* @return
* @Description: 添加一个定时任务<br>
*/
ResponseState addJob(SysAutotask autotask);
/**
* 移除job
*
* @param autotask
*/
void removeJob(SysAutotask autotask);
/**
* 修改一个任务,两种方法<br>
* 1.删除之后添加
* 2.修改
* 第一种比较简单,用第一种
* @param autotask
*/
void modifyJob(SysAutotask autotask);
}
package com.ufgov.service.impl;
import com.ufgov.entity.SysAutotask;
import com.ufgov.service.QuartzManagerService;
import com.ufgov.util.page.ResponseState;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;
import org.quartz.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.quartz.QuartzJobBean;
import org.springframework.stereotype.Service;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* 定时任务管理类<br>
* 这里的问题是目前是除了id之外的所有参数均可以修改,因此jobKey和TriggerKey均使用<b>id</b>来标识
* 这个id在每个Job中会用得到
*/
@Service
public class QuartzManagerServiceImpl implements QuartzManagerService {
private final Logger logger = Logger.getLogger(QuartzManagerServiceImpl.class);
@Autowired
private Scheduler scheduler;
/**
* @param autotask
* @return
* @Description: 添加一个定时任务
*/
public ResponseState addJob(SysAutotask autotask) {
ResponseState state = new ResponseState();
try {
// TODO: 2018/4/17 校验autotask_code是否重复
// 任务名,任务组,任务执行类
Class clazz = Class.forName(autotask.getAutotaskBean());
if (!QuartzJobBean.class.isAssignableFrom(clazz)) {
logger.error(autotask.getAutotaskBean() + "没有实现接口org.springframework.scheduling.quartz.QuartzJobBean");
state.setSuccess(false);
state.setMessage(autotask.getAutotaskBean() + "没有实现接口org.springframework.scheduling.quartz.QuartzJobBean");
return state;
}
TriggerKey triggerKey = new TriggerKey(autotask.getAutotaskId().toString(), autotask.getAutotaskId().toString());
// 触发器
TriggerBuilder<Trigger> triggerBuilder = TriggerBuilder.newTrigger();
// 触发器名,触发器组
triggerBuilder.withIdentity(triggerKey);
triggerBuilder.startNow();
Trigger trigger = null;
if (autotask.getAutotaskType() == 1) {
// TODO: 18-4-16 这个要不要做成在controller中处理,然后数据库设置非空???
//数据校验
SimpleDateFormat formatDate = new SimpleDateFormat("yyyy-MM-dd");
String startDateStr = autotask.getStartDate(), endDateStr = autotask.getEndDate();
//为开始和结束时间设置默认值
if (StringUtils.isEmpty(startDateStr)) {
startDateStr = formatDate.format(new Date());
}
if (StringUtils.isEmpty(endDateStr)) {
endDateStr = "2100-12-31";
}
Date startDate = formatDate.parse(startDateStr);
Date endDate = formatDate.parse(endDateStr);
//设置间隔和执行次数
// 如果设置的运行次数为空,则设置为999999
SimpleScheduleBuilder simpleScheduleBuilder = SimpleScheduleBuilder.simpleSchedule();
simpleScheduleBuilder.withIntervalInSeconds(autotask.getTaskinterval());
simpleScheduleBuilder.withRepeatCount(autotask.getRunTimes() == null ? 999999 : autotask.getRunTimes());
//触发器名称 触发器分组 执行次数 间隔时间(毫秒级)
// SimpleTrigger实现Trigger接口的子接口。此处只指定了开始执行定时任务的时间,使用默认的重复次数(0次)和重复间隔(0秒)
trigger = triggerBuilder
.startAt(startDate)
.endAt(endDate)
.withSchedule(simpleScheduleBuilder)
.build();
} else if (autotask.getAutotaskType() == 2) {
CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(resolveCron(autotask));
// 触发器时间设定
triggerBuilder.withSchedule(cronScheduleBuilder);
// 创建CronTrigger对象
trigger = triggerBuilder.build();
}
//设置job相关信息
JobKey jobKey = new JobKey(autotask.getAutotaskId().toString(), autotask.getAutotaskId().toString());
JobDetail jobDetail = JobBuilder.newJob(clazz).withIdentity(jobKey).build();
// 调度容器设置JobDetail和Trigger
scheduler.scheduleJob(jobDetail, trigger);
// 启动
if (!scheduler.isShutdown()) {
scheduler.start();
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
state.setSuccess(false);
state.setMessage("没有找到类" + autotask.getAutotaskBean());
// TODO: 18-4-16 这里要设置RuntimeException吗?
// throw new RuntimeException(e);
} catch (SchedulerException e) {
e.printStackTrace();
state.setSuccess(false);
if(e instanceof ObjectAlreadyExistsException){
state.setMessage("调度错误:任务"+String.valueOf(autotask.getAutotaskId())
+ "." + String.valueOf(autotask.getAutotaskId()) + "已存在!" );
}else {
state.setMessage("调度错误:"+e.getMessage());
}
} catch (ParseException e) {
e.printStackTrace();
state.setSuccess(false);
state.setMessage("日期格式错误:" + autotask.getStartDate() + "," + autotask.getEnabled());
}
return state;
}
/**
* 移除job
*
* @param autotask
*/
public void removeJob(SysAutotask autotask) {
try {
TriggerKey triggerKey = TriggerKey.triggerKey(autotask.getAutotaskId().toString(), autotask.getAutotaskId().toString());
scheduler.pauseTrigger(triggerKey);// 停止触发器
scheduler.unscheduleJob(triggerKey);// 移除触发器
scheduler.deleteJob(JobKey.jobKey(autotask.getAutotaskId().toString(), autotask.getAutotaskId().toString()));// 删除任务
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 修改一个任务,两种方法<br>
* 1.删除之后添加
* 2.修改
* 第一种比较简单,用第一种
*
* @param autotask
*/
public void modifyJob(SysAutotask autotask) {
//TOTEST:使用这种方法不知道有没有问题,注意观察一下
removeJob(autotask);
addJob(autotask);
}
/**
* 生成cron表达式
*
* @param autotask
*/
private String resolveCron(SysAutotask autotask) {
StringBuilder cronSb = new StringBuilder();
if (autotask.getAutotaskType() == 1) {//间隔运行
return "";
} else if (autotask.getAutotaskType() == 2) {//定期运行
//这里要注意处理周和日的冲突问题,在cron表达式中体现在?的使用上
Short sceduleCrontype = autotask.getScheduleCrontype();
cronSb.append("0 ")//秒
//现在的情况是时分总是要的,因此放在外层
.append(autotask.getMinuteOfDay().toString()).append(" ")//分
.append(autotask.getHourOfDay().toString()).append(" ");//时
switch (sceduleCrontype) {
//0:每年 1:每月 2:每周 3:每日
case 0://每年
//正常情况下,这几个值都不能为null,需要在前边校验,此处略去校验
cronSb.append(autotask.getDayOfMonth().toString()).append(" ")//日
.append(autotask.getMonthOfYear().toString()).append(" ? *");//月
break;
case 1://每月
cronSb.append(autotask.getDayOfMonth().toString()).append(" ")//日
.append("* ? *");
break;
case 2://每周
cronSb.append("? ").append(autotask.getDayOfWeek()).append(" *");//月
break;
case 3://每日
cronSb.append("* * ? *");//月
break;
}
}
return cronSb.toString();
}
}
在容器启动时的加载方式也要修改为使用Service调度。事实上,在外部只能通过Service来调度才会使分布式任务生效
/**
* 初始化定时任务
* @param webApplicationContext
*/
private void initQuartzJob(WebApplicationContext webApplicationContext){
//加载定时任务
// TODO: 18-4-16 分布式的处理
logger.info("-----------------开始加载定时任务----------------");
//获取bean
SysAutotaskService sysAutotaskServiceImpl = webApplicationContext.getBean(SysAutotaskService.class);
List<SysAutotask> autotaskList = sysAutotaskServiceImpl.queryForList();
if(!org.apache.commons.collections.CollectionUtils.isEmpty(autotaskList)){
for (SysAutotask autotask : autotaskList) {
if(autotask.getEnabled() == 1) {
// TODO: 2018/4/17 根据sys_autotask_monitor的记录来判断间隔任务是否已经执行完了
// TODO: 2018/4/17 如果已经执行完成了就不在加载了
// TODO: 2018/4/17 这个地方要不要做简单点,就是在任务内部处理,如果任务的次数已经完成,则置为运行完成
logger.info("加载"+autotask.getAutotaskName());
ResponseState state = webApplicationContext.getBean(QuartzManagerService.class).addJob(autotask);
if(state.getSuccess()) {
logger.info(autotask.getAutotaskName() + "加载完成");
}else{
logger.info(autotask.getAutotaskName() + "加载失败!失败原因:"+state.getMessage());
}
}
}
}
logger.info("-----------------定时任务加载完成----------------");
}
Quartz的11张表大概作用
表名称 | 作用 |
---|---|
QRTZ_TRIGGERS | 存放触发器信息。目前已知的是CronTrigger和SimpleTrigger都会存在,暂时不知道BlobTrigger会不会存在 |
QRTZ_SIMPROP_TRIGGERS | 暂不知详细信息 |
QRTZ_BLOB_TRIGGERS | 以Blob类型存储的触发器,暂时不知道详细用处 |
QRTZ_CRON_TRIGGERS | 以Cron类型存储的触发器 |
QRTZ_SIMPLE_TRIGGERS | 以Simple类型存储的触发器 |
QRTZ_CALENDARS | 日历信息,暂时不知道详细用处 |
QRTZ_SCHEDULER_STATE | 调度器状态。我的理解是,在分布式系统中,每一个调度都会注册在这个表中,然后quartz用定时任务来轮询哪个可用,然后动态的转移调度任务 |
QRTZ_FIRED_TRIGGERS | 已经被触发的触发器。其中的Instance_name可以看出,其实存放的是QRTZ_SCHEDULER_STATE中的调度器所衍生出的触发器 |
QRTZ_PAUSED_TRIGGER_GRPS | 存放暂停的触发器。暂时不知道详细用法 |
QRTZ_JOB_DETAILS | 所有的job详细信息都会存在这张表。 |
QRTZ_LOCKS | 存储过程的悲观锁信息。暂时不知道详细用法 |
对quartz实现分布式定时任务的推测
- 每一台服务器启动时,quartz将调度信息存储在QRTZ_SCHEDULER_STATE中,并将job信息、Trigger信息都分别存储在对应表中
- Quartz分配一个服务器来加载运行定时任务,记录在QRTZ_FIRED_TRIGGERS中
- 所有的服务器都有一个quartz自己控制的定时任务,这个定时任务相当于一个心跳,即本服务器的定时任务是否依然在运行,以及哪些服务器没有上传心跳信息。
- 如果检测到某服务器没有上传心跳信息,就从QRTZ_SCHEDULER_STATE中移除。如果是正在执行定时任务的服务器没有上传,则从QRTZ_SCHEDULER_STATE中取出一个调度器来接着执行。