Spring Boot
-
轻松创建可以运行的独立的,基于生产级 Spring 的应用程序
-
Embed Tomcat, Jetty or Undertow 无需 war 部署,可以直接 java -jar
-
提供可选择的 Stater,快速接入
-
可以自动配置 Spring 和第三方 jar
-
提供生产级的特性如 metrics、health checks and externalized configuration
-
无代码生产和不需要 xml 配置
Spring Boot Starter
-
Starter 是 Spring Boot 推出的解决项目依赖以及快速集成各方组件,使得接入无需或者只需简单在 application.yml 中配置。
-
官方 Starter
https://github.com/spring-projects/spring-boot/tree/master/spring-boot-project/spring-boot-starters
Quartz
- 用的最多的 Java 作业调度库
- 可以通过数据库方式实现分布式作业调度
- GitHub 地址
https://github.com/quartz-scheduler/quartz
目前 Spring 和 Quartz 集成现状
- Quartz 在 xml 中的配置
<!-- 任务testJob配置 -->
<bean name="testJob" class="com.smoke.quartz.TestJob"/>
<bean id="testJobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
<!-- 执行的类 -->
<property name="targetObject">
<ref bean="testJob" />
</property>
<!-- 类中的方法 -->
<property name="targetMethod">
<value>sayHello</value>
</property>
</bean>
<bean id="cronTrigger" class="org.springframework.scheduling.quartz.CronTriggerBean">
<property name="jobDetail">
<ref bean="testJobDetail" />
</property>
<!-- 每一秒钟执行一次 -->
<property name="cronExpression">
<value>0/1 * * * * ?</value>
</property>
</bean>
- 任务实现的类
public class TestJob {
public void sayHello() {
System.out.println(new Date() + " test job");
}
}
- 在 Spring Boot 1.x 中 就是把对应的 xml 改写成 Java Bean
这样做的缺点
- 每一个业务 job 类都要定义一个 jobDetail 任务类和 cronTrigger 触发类。
- 如果在 xml 中维护,非常不便利,并且开发起来很麻烦效率比较慢,这种配置我们其实不应该关心的。
- 缺少任务触发机制(测试环境测试麻烦),和修改机制。
开始我们的 Quartz Starter
目标分析
- 所有的 job 类只需要一个注解就可以自动注入完成。
- 有单次触发任务执行的接口方便测试。
- 更完善点有监控机制或者任务的动态功能。
分析后我们目前采取如下方案
/**
* 过滤数据 晚上2点执行
*/
@QuartzJob(cron = "0 0 2 * * ?", desc = "过滤业务数据")
public class FilterBizJob extends AbstractQuartzJob {
@Autowired
private FilterBizService filterBizService;
@Override
public void execute0(JobExecutionContext context) {
filterBizService.filterData();
}
}
- 任务只需要继承 AbstractQuartzJob 和实现 QuartzJob 注解
- job 内支持 Autowired 注解
- Quartz 是支持 MySQL 分布式的,如果非分布式我们建议直接使用 Spring @scheduled 等。
如何实现?读者可以结合我提的问题然后自己思考下解决方案,以及为什么会有这样的需求
背景
- 首先说下,我们的 Spring Boot 版本是 1.5.2.RELEASE。如果是 2.x 版本官方有一个集成的 Quartz 的。当然我们这个也可以升级到 2.x 版本。
第一步
- 引入相关依赖包
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<optional>true</optional>
</dependency>
<!-- druid 数据的 Starter -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>
<!-- lombok 生成set get log等工具 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- quartz 所需的包 -->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<exclusions>
<exclusion>
<groupId>c3p0</groupId>
<artifactId>c3p0</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz-jobs</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
</dependencies>
- 在 resources下添加 quartz.properties 配置文件如下
org.quartz.scheduler.instanceName = DefaultClusteredScheduler
org.quartz.scheduler.instanceId = AUTO
org.quartz.scheduler.skipUpdateCheck = true
# quartz 执行线程池配置
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount = 15
org.quartz.threadPool.threadPriority = 5
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread = true
# 配置数据库方式实现分布式定时任务
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.PostgreSQLDelegate
org.quartz.jobStore.misfireThreshold = 60000
org.quartz.jobStore.useProperties = true
org.quartz.jobStore.tablePrefix = QRTZ_
org.quartz.jobStore.isClustered = true
org.quartz.jobStore.clusterCheckinInterval =15000
- 在配置 Quartz 对应的数据库的时候,需要根据 Quartz 将那些表放到 quartz 数据库中。
第二步
- 定义 QuartzJob 注解,在 Spring @Component 注解基础上定义,方便 Spring 注入。
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface QuartzJob {
/**
* cron 表达式
*
* @return
*/
String cron() default "";
/**
* job name
*
* @return
*/
String name() default "";
/**
* job group
*
* @return
*/
String group() default "";
/**
* 任务描述
*
* @return
*/
String desc() default "";
}
- 实现 AbstractQuartzJob 任务父类
/**
* QuartzJobBean 是 Spring 定义的 Quartz job 对象
*
*/
@Slf4j
public abstract class AbstractQuartzJob extends QuartzJobBean {
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
Stopwatch watch = Stopwatch.createStarted();
// 获取 job 信息
JobInfo jobInfo = getJobInfo();
log.info("[executeInternal] jobInfo={} startTime={}", jobInfo, new Date());
// 执行业务的方法
execute0(context);
watch.stop();
// 日志记录任务执行的 总时间
log.info("[executeInternal] jobInfo={} endTime={} jobExecuteTime={} ", getJobInfo(), new Date(),
watch.toString());
}
private JobInfo getJobInfo() {
// 从 class 上获取 QuartzJob 注解
QuartzJob quartzJob = AnnotationUtils.findAnnotation(this.getClass(), QuartzJob.class);
return JobInfo.create(quartzJob, this);
}
/**
* 模板
*
* @param context
*/
public abstract void execute0(JobExecutionContext context);
}
- 再看 JobInfo 类
@Getter
@Setter
public class JobInfo {
public final static String DEFAULT_JOB_GROUP = "DEFAULT";
private String cron;
private String name;
private String group;
private String desc;
public static JobInfo create(QuartzJob quartzJob, Job job) {
Assert.notNull(quartzJob, "QuartzJob not exsit");
Assert.isTrue(CronExpression.isValidExpression(quartzJob.cron()), "cron表达式错误");
JobInfo jobInfo = new JobInfo();
jobInfo.setCron(quartzJob.cron());
jobInfo.setDesc(quartzJob.desc());
if (StringUtils.hasText(quartzJob.group())) {
jobInfo.setGroup(quartzJob.group());
}
// 默认为class简单名
String jobName = StringUtils.isEmpty(quartzJob.name()) ? job.getClass().getSimpleName() : quartzJob.name();
jobInfo.setName(jobName);
return jobInfo;
}
}
- 再来看
/**
*
* 让 quartz 的job
* 支持Autowire spring注入注解
*/
public final class AutoWiringSpringBeanJobFactory extends SpringBeanJobFactory
implements ApplicationContextAware {
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@Override
protected Object createJobInstance(final TriggerFiredBundle bundle) throws Exception {
final Object job = super.createJobInstance(bundle);
applicationContext.getAutowireCapableBeanFactory().autowireBean(job);
return job;
}
}
- 最关键的 QuartzAutoConfiguration Starter中的自动加载类
@Import(DataSourceConfig.class)
@ConditionalOnProperty(prefix = "quartz", name = "enable", matchIfMissing = true)
@Configurable
@ComponentScan(basePackageClasses = QuartzAutoConfiguration.class)
public class QuartzAutoConfiguration {
/**
* 每个项目都有唯一不同的 集群名称
*/
@Value("${quartz.cluster.scheduler.name}")
private String schedulerClusterName;
/**
* 是否自动启动 默认true
* 比如 仿生成环境 不开启
*/
@Value("${quartz.auto.start.up:true}")
private boolean autoStartUp;
@Autowired
private ApplicationContext applicationContext;
@Autowired
@Qualifier(QUARTZ_DATA_SOURCE)
private DataSource quartzDataSource;
/**
* 注入我们刚 支持 job 注解的类
* @return
*/
@Bean
public AutoWiringSpringBeanJobFactory jobFactory() {
AutoWiringSpringBeanJobFactory factory = new AutoWiringSpringBeanJobFactory();
factory.setApplicationContext(applicationContext);
return factory;
}
/**
* 注入我们的 quartz 监听类
* @return
*/
@Bean
public QuartzJobAndTriggerListener quartzJobAndTriggerListener() {
QuartzJobAndTriggerListener listener = new QuartzJobAndTriggerListener();
return listener;
}
/**
* Spring quartz 创建bean的工厂类
* @return
*/
@Bean
public SchedulerFactoryBean schedulerFactoryBean(QuartzJobAndTriggerListener quartzJobAndTriggerListener) {
SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();
schedulerFactoryBean.setApplicationContext(applicationContext);
schedulerFactoryBean.setStartupDelay(10);
schedulerFactoryBean.setOverwriteExistingJobs(true);
schedulerFactoryBean.setDataSource(quartzDataSource);
// 设置 quartz 配置文件
schedulerFactoryBean.setConfigLocation(applicationContext.getResource("classpath:quartz.properties"));
// 设置监听
schedulerFactoryBean.setGlobalJobListeners(quartzJobAndTriggerListener);
schedulerFactoryBean.setGlobalTriggerListeners(quartzJobAndTriggerListener);
schedulerFactoryBean.setJobFactory(jobFactory());
schedulerFactoryBean.setBeanName(schedulerClusterName);
Holder holder = getQuartzHolder();
schedulerFactoryBean.setAutoStartup(autoStartUp);
// 设置我们获取的所有jobdetail
schedulerFactoryBean.setJobDetails(holder.jobDetailList.toArray(new JobDetail[holder.jobDetailList.size()]));
// 设置我们获取的所有trigger
schedulerFactoryBean.setTriggers(holder.cronTriggerList.toArray(new Trigger[holder.cronTriggerList.size()]));
return schedulerFactoryBean;
}
/**
* 获取所有的 job实例
*
* @return
*/
public Holder getQuartzHolder() {
// 根据job接口类 获取我们的所有的注入的 业务job
Map<String, Job> quartzJobMap = applicationContext.getBeansOfType(Job.class);
Set<String> quartNameSet = quartzJobMap.keySet();
List<CronTrigger> triggerList = Lists.newArrayList();
List<JobDetail> jobDetailList = Lists.newArrayList();
for (String quartName : quartNameSet) {
Job job = quartzJobMap.get(quartName);
QuartzJob quartzJob = AnnotationUtils.findAnnotation(job.getClass(), QuartzJob.class);
JobInfo jobInfo = JobInfo.create(quartzJob, job);
// 创建 JobDetail
JobDetail jobDetail = JobBuilder.newJob(job.getClass()).withDescription(jobInfo.getDesc())
.storeDurably(true).withIdentity(jobInfo.getName(), jobInfo.getGroup()).build();
// 创建 CronTrigger 触发器
CronTrigger cronTrigger
= TriggerBuilder.newTrigger().forJob(jobDetail).withIdentity(jobInfo.getName(), jobInfo.getGroup())
.withSchedule(CronScheduleBuilder.cronSchedule(jobInfo.getCron())).build();
jobDetailList.add(jobDetail);
triggerList.add(cronTrigger);
}
return new Holder(triggerList, jobDetailList);
}
@Data
@AllArgsConstructor
public static class Holder {
private List<CronTrigger> cronTriggerList;
private List<JobDetail> jobDetailList;
}
- ConditionalOnProperty 是 Spring Boot 中条件注解表示 根据配置的变量中是否包含某个属性。
@ConditionalOnProperty(prefix = "quartz", name = "enable", matchIfMissing = true)
去找 application.yml 中的 quartz.enable 如果没有找到默认为true 如果是false 在不会自动加载QuartzAutoConfiguration类
- 最后在 resources/META-INF 配置 QuartzAutoConfiguration 自动加载的文件
- spring.factories 文件内容
# Auto Configure 在 Spring Boot 启动的时候自动启动 QuartzAutoConfiguration
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.smoke.common.quartz.starter.QuartzAutoConfiguration
- spring-configuration-metadata.json 是我们配置application.yml 自定义的提示文件
{
"properties": [
{
"name": "quartz.enable",
"type": "java.lang.Boolean",
"description": "quartz enable",
"defaultValue": true
},
{
"name": "quartz.cluster.scheduler.name",
"type": "java.lang.String",
"description": "quartz cluster scheduler name"
},
{
"name": "quartz.auto.start.up",
"type": "java.lang.Boolean",
"description": "quartz is auto start up",
"defaultValue": true
}
]
}
- 再看下我们任务的触发类
/**
*
* 其实就是我们普通的 controller
*
*/
@RestController
@RequestMapping("/quartz")
public class QuartzController {
@Autowired
private QuartzManager quartzManager;
/**
* 触发一次 任务
*
* @param jobName
* @param jobGroup
* @return
*/
@GetMapping("trigger")
public String trigger(@RequestParam(value = "jobName") String jobName,
@RequestParam(value = "jobGroup", required = false,
defaultValue = JobInfo.DEFAULT_JOB_GROUP) String jobGroup) {
Assert.hasLength(jobName, "job name is empty");
Assert.hasLength(jobGroup, "job group is empty");
if (quartzManager.trigger(jobName, jobGroup)) {
return "success";
}
return "fail";
}
最后整体看下
- Starter artifactId 的定义就是项目名称
官方命名格式为: spring-boot-starter-{name}
非官方建议命名格式:{name}-spring-boot-starter
- 红色方框是入口
- Spring Boot Starter 是通过 spring.factories 中定义的类自动加载的,然后配合相关的注解。
项目中集成
- 项目中引入
<dependency>
<groupId>com.smoke.common</groupId>
<artifactId>quartz-spring-boot-starter</artifactId>
</dependency>
- application.yml 中配置
quartz:
enable: true
cluster:
scheduler:
name: CRM-ClusteredScheduler-DEV
spring:
datasource:
# quartz 数据源配置
quartz:
url: jdbc:mysql://ip:port/quartz?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true
username:xxxx
password: xxxx
- 就可以写job了
/**
* 每日凌晨1点执行一次
*
*/
@Slf4j
@QuartzJob(cron = "0 0 1 * * ?", desc = "获取用户信息")
public class GetUserInfoJob extends AbstractQuartzJob {
@Autowired
private GetUserInfoService getUserInfoService;
@Override
public void execute0(JobExecutionContext context) {
getUserInfoService.getUserInfoJob();
}
}
- 测试环境测试触发
用 postman get调用 GetUserInfoJob 就是我们的jobName
/你的项目前缀/quartz/trigger?jobName=GetUserInfoJob
进一步分析
- 对应大部分项目的定时任务足够可用
- 缺乏监控界面
- 缺乏动态增删改查,触发的界面
- 缺少任务分片机制
- 缺少任务依赖机制
- 缺少故障转移等很多功能
总结
-
本次我们基于 Spring Boot 的基础上,通过 quartz—spring-boot-stater 的实例可以开始基本的 Starter 自定义开发,当然还缺少 Stater 的监控标准。提出了为什么我们要做这个组件封装,以及对代码中存在的缺陷的思考。
-
最后分析了我们的分布式定时任务缺少的一些功能,但是我们选择一个开源中间件首先考虑的是我们的场景,比如我们是否需要上面提出的复杂的功能。
推荐几个生产可用的分布式定时任务
- xxl-job https://github.com/xuxueli/xxl-job
- Elastic-Job https://github.com/elasticjob