用Quartz+Mysql+Spring+SpringMVC,写一个自己的小调度器

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_41376740/article/details/83585458

前言

  本来想写一个Quartz系列的,然前人之述备矣,于是就有了这篇实战,自己动手写一个小调度器。经过几天的努力终于完成了。PS: 笔者学习java未满一年,写的代码可能很烂。最后会给出详细的资料可以进一步了解。

环境

quartz: 2.3.0
spring系列: 5.1.0.RELEASE
开发工具: maven + IDEA

内容描述

  整合了调度器的持久化数据库,从CURD的小操作完成调度器的调度操作。包括更新Job的Trigger,暂停与挂起Job。其实这些基本的会了,之后只是添加各种方法不断完善。

项目结构

在这里插入图片描述

Maven依赖

<dependencies>
        <!--Spring依赖-->
        <!-- https://mvnrepository.com/artifact/org.springframework/spring-beans -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-beans</artifactId>
            <version>5.1.0.RELEASE</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.springframework/spring-core -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>5.1.0.RELEASE</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.1.0.RELEASE</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.springframework/spring-tx -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
            <version>5.1.0.RELEASE</version>
        </dependency>


        <!--和Quartz的相关依赖-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context-support</artifactId>
            <version>5.1.0.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.quartz-scheduler</groupId>
            <artifactId>quartz</artifactId>
            <version>2.3.0</version>
        </dependency>

        <!--Quartz日志整合-->
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-api</artifactId>
            <version>2.11.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.11.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-slf4j-impl</artifactId>
            <version>2.11.1</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.25</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.springframework/spring-test -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.1.0.RELEASE</version>
            <scope>test</scope>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.springframework/spring-web -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
            <version>5.1.0.RELEASE</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.springframework/spring-webmvc -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>5.1.0.RELEASE</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.13</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>4.0.1</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>

        <!-- https://mvnrepository.com/artifact/com.google.code.gson/gson -->
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.8.5</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
            <version>2.9.7</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.9.7</version>
        </dependency>

    </dependencies>

配置文件

spring-context.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:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">

    <!--Quartz本地调度任务-->
    <bean id="localQuartzScheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
        <property name="configLocation" value="classpath:quartz.properties"/>
    </bean>

    <bean id="taskScheduler" class="scheduler.TaskScheduler"/>
    
    <context:component-scan base-package="service"/>
    <context:component-scan base-package="dao"/>
</beans>

spring-mvc.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:mvc="http://www.springframework.org/schema/mvc"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/mvc
       http://www.springframework.org/schema/mvc/spring-mvc.xsd
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    <!-- 配置jsp显示ViewResolver -->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <!--<property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>-->
        <property name="prefix" value="/WEB-INF/jsp/"/>
        <property name="suffix" value=".jsp"/>
    </bean>

    <!--开启mvc注解-->
    <mvc:annotation-driven/>

    <!--让它只扫描Controller-->
    <context:component-scan base-package="controller" use-default-filters="false">
        <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
    </context:component-scan>

</beans>

log4j2-test.properties

status = error
name = PropertiesConfig
filters = threshold
filter.threshold.type = ThresholdFilter
filter.threshold.level = debug
appenders = console
appender.console.type = Console
appender.console.name = STDOUT
appender.console.layout.type = PatternLayout
appender.console.layout.pattern = %d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n
rootLogger.level = debug
rootLogger.appenderRefs = stdout
rootLogger.appenderRef.stdout.ref = STDOUT

quartz.properties

# Default Properties file for use by StdSchedulerFactory
# to create a Quartz Scheduler Instance, if a different
# properties file is not explicitly specified.
#

#默认或是自己改名字都行
org.quartz.scheduler.instanceName                                          = DefaultQuartzScheduler

#如果使用集群,instanceId必须唯一,设置成AUTO
#org.quartz.scheduler.instanceId                                            = AUTO

org.quartz.scheduler.rmi.export                                            = false
org.quartz.scheduler.rmi.proxy                                             = false
org.quartz.scheduler.wrapJobExecutionInUserTransaction                     = false

org.quartz.threadPool.class                                                = org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount                                          = 10
org.quartz.threadPool.threadPriority                                       = 5
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread = true

org.quartz.jobStore.misfireThreshold                                       = 60000
#============================================================================
# Configure JobStore
#============================================================================

#
#org.quartz.jobStore.class= org.quartz.simpl.RAMJobStore

#存储方式使用JobStoreTX,也就是数据库
org.quartz.jobStore.class                                                  = org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass                                    = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
#使用自己的配置文件
org.quartz.jobStore.useProperties                                          = true
#数据库中quartz表的表名前缀
org.quartz.jobStore.tablePrefix                                            = QRTZ_
org.quartz.jobStore.dataSource                                             = qzDS
#是否使用集群(如果项目只部署到 一台服务器,就不用了)
#org.quartz.jobStore.isClustered                                            = true

#============================================================================
# Configure Datasources
#============================================================================
#配置数据源
org.quartz.dataSource.qzDS.driver                                          = com.mysql.jdbc.Driver
org.quartz.dataSource.qzDS.URL                                             = jdbc:mysql://localhost:3306/quartz
org.quartz.dataSource.qzDS.user                                            = root
org.quartz.dataSource.qzDS.password                                        = lyy1314520
#org.quartz.dataSource.qzDS.maxConnection                                   = 10

程序入口

首先通过Listener监听器来初始化调度器。

    public void contextInitialized(ServletContextEvent sce) {
      /* This method is called when the servlet context is
         initialized(when the Web application is deployed). 
         You can initialize servlet context related data here.
      */
        WebApplicationContext context = WebApplicationContextUtils.findWebApplicationContext(sce.getServletContext());
        TaskScheduler scheduler = (TaskScheduler) context.getBean("taskScheduler");
        scheduler.init();
    }

核心代码

package scheduler;

import bean.MyJob;
import bean.MyTrigger;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.quartz.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;

import java.util.ArrayList;
import java.util.List;

public class TaskScheduler {

    @Autowired
    private SchedulerFactoryBean factory;
    private Scheduler scheduler;
    private static final Logger LOGGER = LogManager.getLogger();
    private List<MyJob> jobs = new ArrayList<>();

    public List<MyJob> getJobs() {
        return jobs;
    }

    public Scheduler getScheduler() {
        return this.scheduler;
    }

    /**
     * 在应用程序成功启动的时候,通过Listener进行调度器的start操作
     */
    public void init() {
        this.scheduler = factory.getScheduler();
        try {
            this.scheduler.start();
            LOGGER.info("调度器被初始化");
            System.out.println(this);
        } catch (SchedulerException e) {
            LOGGER.error("初始化调度器失败");
            throw new RuntimeException(e);
        }
    }

    /**
     * 默认 持久化,可恢复
     *
     * @param job 自定义job
     */
    public void addJob(MyJob job, MyTrigger myTrigger) {
        try {
            List<MyTrigger> triggers = job.getTriggers();

            JobKey jobKey = getJobKey(job);
            TriggerKey triggerKey = getTriggerKey(myTrigger);
            Trigger newTrigger = buildTrigger(myTrigger);

            // 已经存在的话就在原来的基础上添加新的Trigger
            if (this.scheduler.checkExists(jobKey)) {
                JobDetail oldJobDetail = this.scheduler.getJobDetail(jobKey);

                // Job相同,trigger也相同就说明已经存在这样的一个任务了,不需要再重新调度了
                if (this.scheduler.checkExists(triggerKey)) {
                    return;
                }

                this.scheduler.scheduleJob(oldJobDetail, newTrigger);
                triggers.add(myTrigger);
                return;
            }

            Class jobClazz = Class.forName(job.getJobClassName());
            JobDetail jobDetail = buildJobDetail(job, jobClazz);
            this.scheduler.scheduleJob(jobDetail, newTrigger);
            triggers.add(myTrigger);
        } catch (ClassNotFoundException e) {
            LOGGER.error("Job类没有找到: " + e.getMessage());
            throw new RuntimeException(e);
        } catch (SchedulerException e) {
            LOGGER.error("调度器异常" + e.getMessage());
            throw new RuntimeException(e);
        }
    }

    private JobDetail buildJobDetail(MyJob job, Class jobClazz) {
        return JobBuilder.newJob(jobClazz)
                .withIdentity(job.getJobName(), job.getJobGroup())
                .storeDurably()
                .withDescription(job.getDescription())
                .requestRecovery()
                .build();
    }

    private Trigger buildTrigger(MyTrigger myTrigger) {
        CronScheduleBuilder builder = CronScheduleBuilder.cronSchedule(myTrigger.getCron());
        return TriggerBuilder.newTrigger()
                .withIdentity(myTrigger.getName(), myTrigger.getGroup())
                .withSchedule(builder)
                .startAt(myTrigger.getStartTime())
                .endAt(myTrigger.getEndTime())
                .build();
    }

    /**
     * 删除job
     *
     * @param job job
     */
    public void deleteJobDetail(MyJob job) {
        JobKey jobKey = getJobKey(job);
        try {
            // 务必先删除trigger再删除job,这个跟数据库的外键结构有关系
            this.scheduler.deleteJob(jobKey);
        } catch (SchedulerException e) {
            throw new RuntimeException(e);
        }
    }

    private TriggerKey getTriggerKey(MyTrigger trigger) {
        return new TriggerKey(trigger.getName(), trigger.getGroup());
    }

    private JobKey getJobKey(MyJob job) {
        return new JobKey(job.getJobName(), job.getJobGroup());
    }

    /**
     * 给任务换上新的调度器
     *
     * @param job        旧job
     * @param newTrigger 新trigger
     */
    public void updateJobDetail(MyJob job, MyTrigger oldTrigger, MyTrigger newTrigger) {
        JobKey jobKey = getJobKey(job);
        try {
            if (!this.scheduler.checkExists(jobKey)) {
                return;
            }

            // 一个job 可以有多个trigger,多个job 不能对应一个trigger,一对多的关系
            List<? extends Trigger> triggers = this.scheduler.getTriggersOfJob(jobKey);
            if (triggers == null) {
                return;
            }

            for (Trigger trigger : triggers) {
                TriggerKey oldDbKey = trigger.getKey();
                TriggerKey oldKey = getTriggerKey(oldTrigger);
                if (oldDbKey.equals(oldKey)) {
                    Trigger t = buildTrigger(newTrigger);
                    // 会先删除旧的调度器使用新的
                    this.scheduler.rescheduleJob(oldDbKey, t);
                }
                // 如果job持久化了就不会删除job而只删除trigger
//                        this.scheduler.unscheduleJob(trigger.getKey());
            }
        } catch (SchedulerException e) {
            LOGGER.error("调度器异常");
            throw new RuntimeException(e);
        }
    }

    /**
     * 获得所有的Job
     */
    public List<MyJob> getAll() {
        return this.jobs;
    }

    public void pauseJob(MyJob job) {
        JobKey jobKey = getJobKey(job);
        try {
            this.scheduler.pauseJob(jobKey);
        } catch (SchedulerException e) {
            LOGGER.error("暂停job失败 " + e.getMessage());
            throw new RuntimeException(e);
        }
    }

    public void resumeJob(MyJob job) {
        JobKey jobKey = getJobKey(job);
        try {
            this.scheduler.resumeJob(jobKey);
        } catch (SchedulerException e) {
            LOGGER.error("恢复任务失败" + e.getMessage());
            throw new RuntimeException(e);
        }
    }
}

测试代码

我比较懒,没有写前端测试代码,于是直接用MockMVC来测试的,add和delete方法都经过jsp前端测试过了。下面的测试代码均已通过了测试。

import bean.MyTrigger;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.context.WebApplicationContext;
import utils.DateUtils;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup;

/**
 * UserController Tester.
 *
 * @author <Authors name>
 * @version 1.0
 * @since <pre>十月 1, 2018</pre>
 */

@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration({"classpath:/spring/spring-context.xml", "classpath:/spring/spring-mvc.xml"})
@TestExecutionListeners(listeners = {
        DependencyInjectionTestExecutionListener.class})
@Transactional
@Rollback
public class UserControllerTest {

    @Autowired
    private WebApplicationContext wac;

    private MockMvc mockMvc;

    @Before
    public void before() throws Exception {
        scheduler.TaskScheduler scheduler = wac.getBean(scheduler.TaskScheduler.class);
        scheduler.init();
        this.mockMvc = webAppContextSetup(this.wac).build();
    }

    @After
    public void after() throws Exception {
    }

    /**
     * Method: login(@Valid User user2, Model model, Errors errors)
     */
    @Test
    public void testAdd() throws Exception {
        MockHttpServletRequestBuilder builder = post("/add");
        builder.param("cron", "* * * * * ?");
        builder.param("jobName", "123456");
        builder.param("jobGroup", "123456");
        builder.param("jobClassName", "scheduler.Job1");
        builder.param("name", "trigger");
        builder.param("group", "triggers");
        builder.param("startTime", "2018-10-30 16:48:00");
        builder.param("endTime", "2018-10-30 16:49:00");
        mockMvc.perform(builder).andExpect(status().isOk()).andDo(print());
    }

    @Test
    public void testUpdate() throws Exception {
        LocalDateTime time = LocalDateTime.now();
        LocalDateTime startTime = time.plusHours(1);
        LocalDateTime endTime = time.plusHours(2);

        MyTrigger oldTrigger = new MyTrigger("trigger", "triggers", "", null, null, "");
        MyTrigger newTrigger = new MyTrigger("trigger2", "triggers", "", DateUtils.asDate(startTime), DateUtils.asDate(endTime), "* * * * * ?");

        List<MyTrigger> list = new ArrayList<>();
        list.add(oldTrigger);
        list.add(newTrigger);

        ObjectMapper mapper = new ObjectMapper();
        String json = mapper.writeValueAsString(list);

        MockHttpServletRequestBuilder builder = post("/update");
        builder.contentType(MediaType.APPLICATION_JSON_UTF8).content(json);
        builder.param("jobName", "123456");
        builder.param("jobGroup", "123456");
        builder.param("jobClassName", "scheduler.Job1");

        mockMvc.perform(builder).andExpect(status().isOk()).andDo(print());
    }

    @Test
    public void testAllJob() throws Exception {
        MockHttpServletRequestBuilder builder = post("/getAll");
        mockMvc.perform(builder)
                .andExpect(status().isOk())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
                .andDo(print());
    }
}

常见的基本问题:

  1. Scheduler一直空指针异常。
    答:MockMVC的测试环境不会经过Listener这个环节,所以在Listener里面进行的初始化也就不会被执行,自然就是空了,这也是我为什么在before里面添加init方法调用的原因。
  2. 自己的quartz.properties文件不生效
    答:spring-context-support 这个依赖看看有没有加上。
  3. 因为没有日志输出而起不来程序
    答:把Log4j2和slf4j整合一下
  4. 调度任务如何持久化到数据库里面?
    答:quartz官网下载完整的,里面有各种数据库的.sql的文件。就像这样:
    在这里插入图片描述

资料整理:


github:

详细代码地址:https://github.com/21want28k/java-/tree/master/quartz

扫描二维码关注公众号,回复: 3864592 查看本文章

总结

  Quartz还是很强大的啦~小菜鸟只学到其中的冰山一角,还未在实际中运用过,不过用起来的确不太容易,但是配置什么的都搞好了,再写好自己的调度器就可以完成日常的操作啦~我的联系方式:937930940,如果哪边调不通可以留言或者直接找我。

猜你喜欢

转载自blog.csdn.net/qq_41376740/article/details/83585458
今日推荐