1. 程式人生 > >使用spring+quartz+react+antd搭建一個定時任務框架

使用spring+quartz+react+antd搭建一個定時任務框架

使用springboot搭建後端服務

springboot相對於傳統的spring來講可以大大加快web專案的開發,配置檔案的減少也能讓整個專案簡潔明瞭

1.功能清單

包括以下幾項功能:

  1. 執行定時任務 ,可以在專案啟動時指定一系列任務
  2. 管理任務 ,提供增刪改查任務的介面
  3. 系統監控,監控系統執行狀態

2.定時任務功能開發

建立maven專案的過程不做講解,修改pom.xml新增依賴,在resource目錄下新增application.ymlapplication-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();
}

該註解有兩個屬性namedescription,分別描述工作類的名稱和工作內容(描述),用此註解標註的類會被認為可以當做一個或多個任務執行,同時會提供介面讓使用者查詢這些工作類,以便於動態建立任務

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的AutowiredResource註解進行屬性注入是不會生效的,需要手動完成一個任務工廠來替換原本的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為定時任務框架搭建一個前端的操作頁面