目录
使用springboot搭建后端服务
springboot相对于传统的spring来讲可以大大加快web项目的开发,配置文件的减少也能让整个项目简洁明了
1.功能清单
包括以下几项功能:
- 运行定时任务 ,可以在项目启动时指定一系列任务
- 管理任务 ,提供增刪改查任务的接口
- 系统监控,监控系统运行状态
最终效果如下
1.任务管理界面
2.新增修改
3.系统监控
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();
}
}
访问http://localhost:端口/work/get,打印了我们定义的两个工作类
3.系统监控功能
我们借助springboot的actuator模块实现系统监控功能,只需要在pom.xml中引入该模块(前文的依赖中已经引入),同时在配置文件中加入如下配置启动所有监控端点
management:
endpoints:
web:
exposure:
include: "*"
访问http://localhost:端口/actuator/health会打印如下信息
{"status":"UP"}
其他端点就不做赘述了
4.源码
5.下一个环节
至此,我们搭建了一个能够执行定时任务,并在运行时动态修改任务,同时提供系统监控功能的的定时任务框架,但操作性远远谈不上友好,在下一篇文章中,我们将使用react+antd为定时任务框架搭建一个前端的操作页面