使用spring+quartz+react+antd搭建一個定時任務框架
使用springboot搭建後端服務
springboot相對於傳統的spring來講可以大大加快web專案的開發,配置檔案的減少也能讓整個專案簡潔明瞭
1.功能清單
包括以下幾項功能:
- 執行定時任務 ,可以在專案啟動時指定一系列任務
- 管理任務 ,提供增刪改查任務的介面
- 系統監控,監控系統執行狀態
2.定時任務功能開發
建立maven專案的過程不做講解,修改pom.xml新增依賴,在resource目錄下新增application.yml和application-dev.yml兩個配置檔案(不考慮執行環境的只需要建立前一個配置檔案就可以了)
1.依賴
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>timeTaskFrameWork</groupId> <artifactId>timeTaskFrameWork</artifactId> <version>1.0-SNAPSHOT</version> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.2.RELEASE</version> </parent> <dependencies> <!--springboot依賴--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context-support</artifactId> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>5.1.1.RELEASE</version> </dependency> <!--quartz依賴--> <dependency> <groupId>org.quartz-scheduler</groupId> <artifactId>quartz</artifactId> <version>2.2.1</version> </dependency> <!--fastjson依賴--> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.47</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> </plugins> </build> </project>
2.兩個配置檔案
1.application-dev.yml
server:
port: `你的埠`
logging:
level:
com: `日誌等級`
file: `日誌檔案`
2.application.yml
spring:
profiles:
active: dev
在application.yml中使用spring.profiles.active屬性可以方便的切換使用哪個配置檔案,如果只是個人開發,通常在前者中配置所有屬性就可以了,最終目錄結構如下
3.程式碼
1.封裝資訊
編寫兩個javabean用於封裝任務和工作類。編寫一個註解用於註解工作類,註解的作用會在後面進行介紹
package com.feng.fundation.mod; /** * Created by Feng * 任務模型 */ public class Task { private String name; private String cronExpress; private String status; private String group; private String description; private String jobClass; public Task() { } public Task(String name, String cronExpress, String status, String group, String description, String jobClass) { this.name = name; this.cronExpress = cronExpress; this.status = status; this.group = group; this.description = description; this.jobClass = jobClass; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getCronExpress() { return cronExpress; } public void setCronExpress(String cronExpress) { this.cronExpress = cronExpress; } public String getStatus() { return status; } public void setStatus(String status) { this.status = status; } public String getGroup() { return group; } public void setGroup(String group) { this.group = group; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public String getJobClass() { return jobClass; } public void setJobClass(String jobClass) { this.jobClass = jobClass; } }
package com.feng.fundation.mod;
/**
* Created by Feng
* 工作類模型
*/
public class Work {
private String name;
private String className;
private String description;
public Work(String name, String className, String description) {
this.name = name;
this.className = className;
this.description = description;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getClassName() {
return className;
}
public void setClassName(String className) {
this.className = className;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}
package com.feng.fundation.mod.annonation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Created by Feng
* 註解工作類
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface AvailableWork {
String description();
String name();
}
該註解有兩個屬性name和description,分別描述工作類的名稱和工作內容(描述),用此註解標註的類會被認為可以當做一個或多個任務執行,同時會提供介面讓使用者查詢這些工作類,以便於動態建立任務
2.初始化任務
框架提供一個介面用於在專案啟動時建立一些定時任務
package com.feng.fundation.init;
import com.feng.fundation.mod.Task;
import java.util.List;
/**
* Created by Feng
* must realize to provide beginning job
*/
public interface TaskProducer {
public List<Task> getInitialJob();
}
介面只有一個getInitialJob方法,返回一個由我們之前定義的任務模型組成的列表,列表中的任務會在專案啟動時建立,下面我們實現它。 首先先建立兩個簡單的工作類,繼承quartz框架提供的Job介面,用前面定義的AvailableWork註解註解此類
package com.feng.test;
import com.feng.fundation.mod.annonation.AvailableWork;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
/**
* Created by Feng
*/
@AvailableWork(name = "測試工作類",description = "日誌列印測試工作工作")
public class TestWork implements Job {
private final Logger logger= LoggerFactory.getLogger(this.getClass());
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
System.out.println(("測試工作1"));
}
}
package com.feng.test;
import com.feng.fundation.mod.annonation.AvailableWork;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
/**
* Created by Feng
*/
@AvailableWork(name = "測試掃描工作類",description = "測試工作掃描")
public class TestWork2 implements Job {
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
System.out.println(("測試工作掃描"));
}
}
建立了兩個工作類,只是簡單的列印一些資訊,接著實現初始化任務的介面
package com.feng.test;
import com.feng.fundation.init.TaskProducer;
import com.feng.fundation.mod.Task;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
/**
* Created by Feng
*/
@Component
public class TestTaskProducer implements TaskProducer {
@Override
public List<Task> getInitialJob() {
List<Task> jobs = new ArrayList();
Task testJob = new Task("test1","1/10 * * * * ?","0","test","測試任務1","com.feng.test.TestWork");
jobs.add(testJob);
return jobs;
}
}
我們在專案啟動時建立了一個每隔十秒執行一次的任務,要注意的是,用工作類為我們建立定時任務的工作是由quartz而不是spring完成的,到目前為止如果我們想在工作類中使用spring的Autowired或Resource註解進行屬性注入是不會生效的,需要手動完成一個任務工廠來替換原本的AdaptableJobFactory類來實現依賴注入功能,新任務工廠類程式碼如下
package com.feng.fundation.base;
import org.quartz.spi.TriggerFiredBundle;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.scheduling.quartz.AdaptableJobFactory;
import org.springframework.stereotype.Component;
/**
* Created by Feng
* replace AdaptableJobFactory with AutowiredJobFactory,realize spring Autowired
*/
@Component("autowiredJobFactory")
public class AutowiredJobFactory extends AdaptableJobFactory {
@Autowired
private AutowireCapableBeanFactory beanFactory;
@Override
protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
Object jobInstance = super.createJobInstance(bundle);
beanFactory.autowireBean(jobInstance);
return jobInstance;
}
}
這個時候我們可以編寫用於配置任務的java類了,這個配置類中必須完成兩件事
- 使用我們定義的AutowiredJobFactory類替換原本的任務工廠類
- 初始化所有定時任務
程式碼如下
package com.feng.fundation.config;
import com.feng.fundation.base.AutowiredJobFactory;
import com.feng.fundation.init.TaskProducer;
import com.feng.fundation.mod.Task;
import com.feng.fundation.util.QuartzUtil;
import org.quartz.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import javax.annotation.Resource;
import java.util.List;
/**
* Created by Feng
*/
@Configuration
public class SchedledConfiguration {
@Autowired
private AutowiredJobFactory autowiredJobFactory;
@Autowired
private TaskProducer initialJobProducer;
private final Logger logger= LoggerFactory.getLogger(this.getClass());
@Bean
public SchedulerFactoryBean schedulerFactoryBean() {
SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();
schedulerFactoryBean.setOverwriteExistingJobs(true);
schedulerFactoryBean.setJobFactory(autowiredJobFactory);
return schedulerFactoryBean;
}
@Bean
public Scheduler scheduler() throws SchedulerException {
List<Task> jobs = initialJobProducer.getInitialJob();
Scheduler scheduler = schedulerFactoryBean().getScheduler();
for (Task job : jobs) {
TriggerKey triggerKey = TriggerKey.triggerKey(job.getName(), job.getGroup());
CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);
try{
if (null == trigger) {
scheduler.scheduleJob(QuartzUtil.buildJobDetail(job), QuartzUtil.buildCronTrigger(job));
} else {
trigger = trigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(QuartzUtil.buildCronScheduleBuilder(job.getCronExpress())).build();
scheduler.rescheduleJob(triggerKey, trigger);
}
}catch (IllegalAccessException e) {
logger.error("新建工作"+job+"異常,reason:"+e.getMessage());
continue;
}
}
scheduler.start();
initialJobProducer = null;
return scheduler;
}
}
package com.feng.fundation.util;
import com.feng.fundation.mod.Task;
import org.quartz.*;
/**
* Created by Feng
*/
public class QuartzUtil {
public static JobDetail buildJobDetail(Task job) throws IllegalAccessException{
JobDetail jobDetail = null;
try {
Class jobClass = Class.forName(job.getJobClass());
jobDetail = JobBuilder.newJob(jobClass).withIdentity(job.getName(), job.getGroup()).withDescription(job.getDescription()).build();
} catch (ClassNotFoundException e) {
throw new IllegalAccessException("非法的工作類:"+job.getJobClass());
}
job.setStatus("1");
jobDetail.getJobDataMap().put("jobDetail", job);
return jobDetail;
}
public static CronTrigger buildCronTrigger(Task job) throws IllegalAccessException {
return TriggerBuilder.newTrigger().withIdentity(job.getName(), job.getGroup()).withSchedule(buildCronScheduleBuilder(job.getCronExpress())).build();
}
public static CronScheduleBuilder buildCronScheduleBuilder(String cronExpress) throws IllegalAccessException{
CronScheduleBuilder scheduleBuilder;
try {
scheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpress);
} catch (RuntimeException e) {
throw new IllegalAccessException("非法的時間表達式:" + cronExpress);
}
return scheduleBuilder;
}
}
這個時候執行專案,定時任務會正常執行
2.動態修改任務
編寫一個service用於在執行時動態增,刪,改,查,暫停,啟動任務
package com.feng.fundation.service.task;
import com.feng.fundation.mod.Task;
import org.quartz.SchedulerException;
import java.util.List;
/**
* Created by Feng
*/
public interface TaskService {
public List<Task> getJob(String name, String group) throws SchedulerException;
public void resumeJob(String name, String group) throws SchedulerException;
public void resumeAll() throws SchedulerException;
public void pauseJob(String name, String group) throws SchedulerException;
public void pauseAll() throws SchedulerException;
public void deleteJob(String name, String group) throws SchedulerException;
public void addJob(Task job) throws SchedulerException, IllegalAccessException;
public void updateJob(Task job) throws SchedulerException, IllegalAccessException;
}
package com.feng.fundation.service.task;
import com.feng.fundation.mod.Task;
import com.feng.fundation.util.QuartzUtil;
import org.quartz.*;
import org.quartz.impl.matchers.GroupMatcher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.thymeleaf.util.StringUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/**
* Created by Feng
*/
@Component
public class TaskServiceImpl implements TaskService {
@Autowired
private Scheduler scheduler;
private final Logger logger= LoggerFactory.getLogger(this.getClass());
@Override
public List<Task> getJob(String name, String group) throws SchedulerException {
List<Task> jobs = new ArrayList<>();
GroupMatcher<JobKey> matcher = StringUtils.isEmpty(group) ? GroupMatcher.anyJobGroup() : GroupMatcher.groupEndsWith(group);
Set<JobKey> jobKeySet = scheduler.getJobKeys(matcher);
for (JobKey jobKey : jobKeySet){
JobDetail jobDetail = scheduler.getJobDetail(jobKey);
Task job = (Task) jobDetail.getJobDataMap().get("jobDetail");
job.setStatus(scheduler.getTriggerState(TriggerKey.triggerKey(jobKey.getName(),jobKey.getGroup())).toString());
if(StringUtils.isEmpty(name) || name.equals(job.getName())) {
jobs.add((Task) jobDetail.getJobDataMap().get("jobDetail"));
}
}
return jobs;
}
@Override
public void pauseJob(String name, String group) throws SchedulerException {
if(!isEmpty(name,group)){
scheduler.pauseJob(JobKey.jobKey(name,group));
}
}
@Override
public void pauseAll() throws SchedulerException {
scheduler.pauseAll();
}
@Override
public void resumeJob(String name, String group) throws SchedulerException {
scheduler.resumeJob(JobKey.jobKey(name,group));
}
@Override
public void resumeAll() throws SchedulerException {
scheduler.resumeAll();
}
@Override
public void deleteJob(String name, String group) throws SchedulerException {
scheduler.deleteJob(JobKey.jobKey(name,group));
}
@Override
public void addJob(Task job) throws SchedulerException, IllegalAccessException {
JobDetail jobDetail = QuartzUtil.buildJobDetail(job);
Trigger trigger = QuartzUtil.buildCronTrigger(job);
scheduler.scheduleJob(jobDetail,trigger);
}
@Override
public void updateJob(Task job) throws SchedulerException, IllegalAccessException {
Trigger trigger = QuartzUtil.buildCronTrigger(job);
TriggerKey triggerKey = TriggerKey.triggerKey(job.getName(), job.getGroup());
scheduler.rescheduleJob(triggerKey,trigger);
}
private boolean isEmpty(String... values){
boolean isEmpty = false;
for(String value:values){
if(StringUtils.isEmpty(value)){
isEmpty = true;
}
}
return isEmpty;
}
}
這部分沒什麼好講的,注入我們之前在配置類中編寫的Scheduler,使用提供的對api任務進行管理 編寫一個控制器,對外提供管理任務的介面
package com.feng.fundation.controller.task;
import com.alibaba.fastjson.JSONObject;
import com.feng.fundation.mod.Task;
import com.feng.fundation.service.task.TaskService;
import com.feng.fundation.service.work.WorkService;
import org.quartz.SchedulerException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* Created by Feng
*/
@RestController
@RequestMapping(value = "/task")
public class TaskController {
@Autowired
private TaskService jobService;
@RequestMapping(value = "/get",method = RequestMethod.GET)
public List<Task> getAll(String name, String group) throws SchedulerException {
return jobService.getJob(name,group);
}
@RequestMapping(value = "/pause",method = RequestMethod.PATCH)
public void pause(@RequestBody JSONObject payload) throws SchedulerException {
jobService.pauseJob(payload.getString("name"),payload.getString("group"));
}
@RequestMapping(value = "/resume",method = RequestMethod.PATCH)
public void resume(@RequestBody JSONObject payload) throws SchedulerException {
jobService.resumeJob(payload.getString("name"),payload.getString("group"));
}
@RequestMapping(value = "/pause-all",method = RequestMethod.PATCH)
public void pauseAll() throws SchedulerException {
jobService.pauseAll();
}
@RequestMapping(value = "/resume-all",method = RequestMethod.PATCH)
public void resumeJob() throws SchedulerException {
jobService.resumeAll();
}
@RequestMapping(value = "/delete",method = RequestMethod.DELETE)
public void deleteJob(@RequestBody JSONObject payload) throws SchedulerException {
jobService.deleteJob(payload.getString("name"),payload.getString("group"));
}
@RequestMapping(value = "/add",method = RequestMethod.POST)
public void addJob(@RequestBody Task job) throws SchedulerException, IllegalAccessException {
jobService.addJob(job);
}
@RequestMapping(value = "/update",method = RequestMethod.POST)
public void updateJob(@RequestBody Task job) throws SchedulerException, IllegalAccessException {
jobService.updateJob(job);
}
}
啟動專案,訪問localhost:埠號/task/get會得到當前所有正在執行的任務資訊
3.對工作類進行管理
讓使用者在前端輸入工作類的全限定名是很不友好的一件事情,還記得我們之前定義的用於註解工作類的註解嗎,藉助它編寫一個service讓使用者能在前端直接獲取所有的工作類 先編寫一個工具類用於掃描指定包下的類,並對其進行篩選,主要功能借助spring自帶的包掃描工具實現,其中include方法用於新增一個過濾器來指定需要查詢的類,exclude方法用於排除不需要查詢的類,find方法執行查詢。
package com.feng.fundation.util;
import com.feng.fundation.mod.annonation.AvailableWork;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.core.type.filter.TypeFilter;
import java.util.Set;
public class SpringClassScanner {
private ClassPathScanningCandidateComponentProvider classPathScanningCandidateComponentProvider = new ClassPathScanningCandidateComponentProvider(false);
public SpringClassScanner include(TypeFilter filter){
classPathScanningCandidateComponentProvider.addIncludeFilter(filter);
return this;
}
public SpringClassScanner exclude(TypeFilter filter){
classPathScanningCandidateComponentProvider.addExcludeFilter(filter);
return this;
}
public Set<BeanDefinition> find(String scanPackage){
return this.classPathScanningCandidateComponentProvider.findCandidateComponents(scanPackage);
}
private ClassPathScanningCandidateComponentProvider createComponentScanner() {
ClassPathScanningCandidateComponentProvider provider
= new ClassPathScanningCandidateComponentProvider(false);
provider.addIncludeFilter(new AnnotationTypeFilter(AvailableWork.class));
return provider;
}
}
然後我們編寫一個查詢帶有AvailableWork註解的類的service
package com.feng.fundation.service.work;
import com.feng.fundation.mod.Work;
import com.feng.fundation.mod.annonation.AvailableWork;
import com.feng.fundation.util.SpringClassScanner;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.ApplicationContext;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.stereotype.Component;
import java.util.HashSet;
import java.util.Set;
/**
* Created by Feng
*/
@Component
public class WorkService {
private SpringClassScanner springClassScanner = new SpringClassScanner();
public Set<Work> getWorks() throws ClassNotFoundException {
Set<BeanDefinition> beanDefinitions = springClassScanner.include(new AnnotationTypeFilter(AvailableWork.class)).find("com");
Set<Work> works = new HashSet<>();
for(BeanDefinition beanDefinition:beanDefinitions){
Class workClass = Class.forName(beanDefinition.getBeanClassName());
AvailableWork availableWork = (AvailableWork) workClass.getAnnotation(AvailableWork.class);
works.add(new Work(availableWork.name(),beanDefinition.getBeanClassName(),availableWork.description()));
}
return works;
}
}
建立一個控制器,提供查詢共組類的介面
package com.feng.fundation.controller.work;
import com.feng.fundation.mod.Work;
import com.feng.fundation.service.work.WorkService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Set;
/**
* Created by Feng
*/
@RestController
@RequestMapping(value = "/work")
public class WorkController {
@Autowired
private WorkService workService;
@RequestMapping("/get")
public Set<Work> getWorks() throws ClassNotFoundException {
return workService.getWorks();
}
}
訪問,列印了我們定義的兩個工作類
3.系統監控功能
我們藉助springboot的actuator模組實現系統監控功能,只需要在pom.xml中引入該模組(前文的依賴中已經引入),同時在配置檔案中加入如下配置啟動所有監控端點
management:
endpoints:
web:
exposure:
include: "*"
訪問會列印如下資訊
{"status":"UP"}
其他端點就不做贅述了
4.下一個環節
至此,我們搭建了一個能夠執行定時任務,並在執行時動態修改任務,同時提供系統監控功能的的定時任務框架,但操作性遠遠談不上友好,在下一篇文章中,我們將使用react+antd為定時任務框架搭建一個前端的操作頁面