手把手封装 Quartz Starter

 

 

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 配置文件如下 enter image description here
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 自动加载的文件

enter image description here

  • 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";
  }

最后整体看下

enter image description here

  • 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&amp;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

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值