用Quartz+Mysql+Spring+SpringMVC,写一个自己的小调度器
前言
本来想写一个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());
}
}
常见的基本问题:
- Scheduler一直空指针异常。
答:MockMVC的测试环境不会经过Listener这个环节,所以在Listener里面进行的初始化也就不会被执行,自然就是空了,这也是我为什么在before里面添加init方法调用的原因。 - 自己的quartz.properties文件不生效
答:spring-context-support 这个依赖看看有没有加上。 - 因为没有日志输出而起不来程序
答:把Log4j2和slf4j整合一下 - 调度任务如何持久化到数据库里面?
答:quartz官网下载完整的,里面有各种数据库的.sql的文件。就像这样:
资料整理:
- 翻译的文档: https://www.w3cschool.cn/quartz_doc/quartz_doc-h4ux2cq6.html
- 学习的系列: https://blog.****.net/u010648555/article/details/54891264?utm_source=blogxgwz0
- 两个注解DisallowConcurrentExecution,PersistJobDataAfterExecution: https://www.cnblogs.com/daxin/archive/2013/05/27/3101972.html
- 在线cron: http://cron.qqe2.com/
- **调度器大全(我看到的最完整的一篇): https://my.oschina.net/songhongxu/blog/802574
- 较完整的教程:https://gitbook.cn/books/59f936abce433c5e065c2ae1/index.html
- 错过的任务处理:https://blog.****.net/lnara/article/details/8658258?utm_source=blogkpcl0
- 数据库表字段讲解:https://www.bbsmax.com/A/6pdDrqlLJw/
- Quartz性能优化:http://www.jfh.com/jfperiodical/article/1297
- 在Listener里面获取Spring容器bean:https://blog.****.net/shentianzhi2009/article/details/39343245
- SpringMVC同时封装多个相同的bean:https://blog.****.net/qq_37479086/article/details/78395083
- MockMvc:https://blog.****.net/kqZhu/article/details/78836275 https://blog.****.net/Adam_allen/article/details/79919921
github:
详细代码地址:https://github.com/21want28k/java-/tree/master/quartz
总结
Quartz还是很强大的啦~小菜鸟只学到其中的冰山一角,还未在实际中运用过,不过用起来的确不太容易,但是配置什么的都搞好了,再写好自己的调度器就可以完成日常的操作啦~我的联系方式:937930940,如果哪边调不通可以留言或者直接找我。