使用spring+quartz+react+antd搭建一个定时任务框架

使用springboot搭建后端服务

springboot相对于传统的spring来讲可以大大加快web项目的开发,配置文件的减少也能让整个项目简洁明了

1.功能清单

包括以下几项功能:

  1. 运行定时任务 ,可以在项目启动时指定一系列任务
  2. 管理任务 ,提供增刪改查任务的接口
  3. 系统监控,监控系统运行状态

最终效果如下
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.初始化任务

框架提供一个接口用于在项目启动时创建一些定时任务

扫描二维码关注公众号,回复: 6783928 查看本文章
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();
    }
}

访问http://localhost:端口/work/get,打印了我们定义的两个工作类
在这里插入图片描述

3.系统监控功能

我们借助springboot的actuator模块实现系统监控功能,只需要在pom.xml中引入该模块(前文的依赖中已经引入),同时在配置文件中加入如下配置启动所有监控端点

management:
  endpoints:
    web:
      exposure:
       include: "*"

访问http://localhost:端口/actuator/health会打印如下信息

{"status":"UP"}

其他端点就不做赘述了

4.源码

点此下载

5.下一个环节

至此,我们搭建了一个能够执行定时任务,并在运行时动态修改任务,同时提供系统监控功能的的定时任务框架,但操作性远远谈不上友好,在下一篇文章中,我们将使用react+antd为定时任务框架搭建一个前端的操作页面

猜你喜欢

转载自blog.csdn.net/qq_35488769/article/details/83628374