分布式quartz定时任务前端配置产出物整理

本文介绍 Quartz 定时任务管理系统的基本概念、配置及其实现分布式任务的方法。包括前端后端架构、Java 基础知识、Quartz 的使用技巧、Spring 整合、动态参数传递等内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前端

前端主要采用的是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表达式

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中取出一个调度器来接着执行。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值