almost 2 years ago

使用單一 Thread 跑完所有工作,想當然一個地方沒寫好可能就掛掉了,所以改用 Quartz 來調度工作

那為什麼不用之前 Spring Boot Schedule ,原本我以為沒有 Thread pool 的概念,後來才發現可以用 @Async 來實現異步執行,不過 Quartz 有他好用的地方還是可以導入來使用

啟動類別的寫法

Application.java
package com.lottery.scheduled;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@SpringBootApplication
@EnableTransactionManagement
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
  • 這次改用 Spring Data 整合的 JPA ,所以需要 @EnableTransactionManagement

Entity 實體

Biglotto.java
package com.lottery.scheduled.model.entity;

import java.io.Serializable;
import javax.persistence.*;
import lombok.Data;

@Data
@Entity
public class Biglotto implements Serializable {
    private static final long serialVersionUID = 1L;
    @Id
    private Integer id;
    private String num1;
    private String num2;
    private String num3;
    private String num4;
}

Repository 存取

BiglottoRepository.java
package com.lottery.scheduled.model.dao;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.CrudRepository;
import com.lottery.scheduled.model.entity.*;

public interface BiglottoRepository extends JpaRepository<Biglotto, Integer> {

}
  • 使用 JpaRepository 比 CrudRepository 方便一點提供更多基本操作

Service 層

BiglottoService.java
package com.lottery.scheduled.service;

import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.lottery.scheduled.model.dao.BiglottoRepository;
import com.lottery.scheduled.model.entity.Biglotto;

@Service
@Transactional
public class BiglottoService {
    @Autowired
    private BiglottoRepository biglottoRepository;

    public List<Biglotto> findAll() {
        return biglottoRepository.findAll(new Sort(Sort.Direction.ASC, "id"));
    }
}

接下來是真正執行的排程 Job

Job1.java
package com.lottery.scheduled.job;

import java.util.List;

import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.quartz.QuartzJobBean;
import org.springframework.stereotype.Component;
import com.lottery.scheduled.service.BiglottoService;

@Component
public class Job1 extends QuartzJobBean{
    @Autowired
    private BiglottoService biglottoService;

    @Override
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
        System.out.println("Job1 Start:"+Thread.currentThread().getId());
        List list = biglottoService.findAll();
        try {
            Thread.sleep(1000 * 30);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block

            e.printStackTrace();
        }
        System.out.println("Job1 End");
    }
}
  • 但是 Job 內其實無法直接使用 @Autowired 註解來注入,所以要提供下面這支 Autowired 來輔助

輔助 Job 內的自動注入

AutoWiringSpringBeanJobFactory.java
package com.lottery.scheduled;

import org.quartz.spi.TriggerFiredBundle;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.scheduling.quartz.SpringBeanJobFactory;

public class AutoWiringSpringBeanJobFactory extends SpringBeanJobFactory implements ApplicationContextAware{
    private transient AutowireCapableBeanFactory beanFactory;

    public void setApplicationContext(final ApplicationContext context){
        beanFactory = context.getAutowireCapableBeanFactory();
    }

    @Override
    protected Object createJobInstance(final TriggerFiredBundle bundle) throws Exception{
        final Object job = super.createJobInstance(bundle);
        beanFactory.autowireBean(job);
        return job;
    }
}

才是真正開始寫排程的設定

ScheduledTasks1.java
package com.lottery.scheduled.schedule;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.quartz.CronTriggerFactoryBean;
import org.springframework.scheduling.quartz.JobDetailFactoryBean;
import com.lottery.scheduled.job.Job1;

@Configuration 
@ComponentScan("com.lottery.scheduled.job") 
public class ScheduledTasks1 {
    
    @Bean(name="Job1")
    public JobDetailFactoryBean jobDetailFactoryBean(){
        JobDetailFactoryBean factory = new JobDetailFactoryBean();
        factory.setJobClass(Job1.class);
        factory.setGroup("mygroup");
        factory.setName("myjob");
        return factory;
    }

    //Job is scheduled after every 1 minute 

    @Bean(name="Cron1")
    public CronTriggerFactoryBean cronTriggerFactoryBean(){
        CronTriggerFactoryBean stFactory = new CronTriggerFactoryBean();
        stFactory.setJobDetail(jobDetailFactoryBean().getObject());
        stFactory.setStartDelay(1000);
        stFactory.setName("mytrigger");
        stFactory.setGroup("mygroup");
        stFactory.setCronExpression("0 0/1 * 1/1 * ? *");
        return stFactory;
    }
}
  • 這邊設定了排程時間 setCronExpression
  • 如果還有不同支的排程記得 setName 要不一樣
  • @Bean(name="Cron1") 是因為有多組排程要讓 Spring 區分用

QuartzConfig

QuartzConfiguration.java
package com.lottery.scheduled;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.quartz.CronTriggerFactoryBean;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;

@Configuration 
public class QuartzConfiguration {
    
    @Autowired
    @Qualifier("Cron1")
    private CronTriggerFactoryBean cron1;
    
    @Autowired
    @Qualifier("Cron2")
    private CronTriggerFactoryBean cron2;

    @Bean
    public AutoWiringSpringBeanJobFactory autoWiringSpringBeanJobFactory(){
        return new AutoWiringSpringBeanJobFactory();
    }

    @Bean
    public SchedulerFactoryBean schedulerFactoryBean() {
        SchedulerFactoryBean scheduler = new SchedulerFactoryBean();
        scheduler.setJobFactory(autoWiringSpringBeanJobFactory());
        scheduler.setTriggers(cron1.getObject(), cron2.getObject());
        return scheduler;
    }
}
  • 把之前設定好的 CronTriggerFactoryBean 用名稱注入後加到 SchedulerFactoryBean 啟動

設定檔

application.yml
spring:

  datasource:

    driverClassName: org.postgresql.Driver

    url: jdbc:postgresql://10.1.99.99:5432/Lottery

    username: root

    password: 123456789

    testWhileIdle: true

    validationQuery: SELECT 1

    

  jpa.show-sql: true
build.gradle
buildscript {
    ext {
        springBootVersion = '1.2.5.RELEASE'
    }
    repositories {
        mavenCentral()
        maven {
            url "https://plugins.gradle.org/m2/"
        }
    }
        dependencies {
            classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") 
            classpath("io.spring.gradle:dependency-management-plugin:0.5.2.RELEASE")
            classpath "info.robotbrain.gradle.lombok:lombok-gradle:1.1"
    }
}

apply plugin: "info.robotbrain.lombok"
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'
apply plugin: 'spring-boot' 
apply plugin: 'io.spring.dependency-management' 

jar {
    baseName = 'lotterySchedule'
    version = '0.0.1-SNAPSHOT'
}
sourceCompatibility = 1.8
targetCompatibility = 1.8

repositories {
    mavenCentral()
}

dependencies {
    compile 'org.quartz-scheduler:quartz:2.2.1'
    compile 'org.springframework:spring-context-support:4.1.6.RELEASE'
    compile 'org.springframework.boot:spring-boot-starter-data-jpa:1.2.5.RELEASE'   
    compile 'org.postgresql:postgresql:9.4-1202-jdbc42'
    testCompile group: 'junit', name: 'junit', version: '4.+'
}

eclipse {
    classpath {
         containers.remove('org.eclipse.jdt.launching.JRE_CONTAINER')
         containers 'org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8'
    }
}

task wrapper(type: Wrapper) {
    gradleVersion = '2.3'
}

執行結果

Job2 Start:14
Job1 Start:13
Hibernate: select biglotto0_.id as id1_0_, biglotto0_.b10count as b2_0_, biglotto0_.b15count as b3_0_, biglotto0_.b1count as b4_0_, biglotto0_.b5count as b5_0_, biglotto0_.date as date6_0_, biglotto0_.dcount as dcount7_0_, biglotto0_.num1 as num8_0_, biglotto0_.num2 as num9_0_, biglotto0_.num3 as num10_0_, biglotto0_.num4 as num11_0_, biglotto0_.num5 as num12_0_, biglotto0_.num6 as num13_0_, biglotto0_.num7 as num14_0_, biglotto0_.numsum as numsum15_0_, biglotto0_.period as period16_0_, biglotto0_.scount as scount17_0_, biglotto0_.t10count as t18_0_, biglotto0_.t15count as t19_0_, biglotto0_.t5count as t20_0_, biglotto0_.week as week21_0_ from biglotto biglotto0_ order by biglotto0_.id asc
Hibernate: select biglotto0_.id as id1_0_, biglotto0_.b10count as b2_0_, biglotto0_.b15count as b3_0_, biglotto0_.b1count as b4_0_, biglotto0_.b5count as b5_0_, biglotto0_.date as date6_0_, biglotto0_.dcount as dcount7_0_, biglotto0_.num1 as num8_0_, biglotto0_.num2 as num9_0_, biglotto0_.num3 as num10_0_, biglotto0_.num4 as num11_0_, biglotto0_.num5 as num12_0_, biglotto0_.num6 as num13_0_, biglotto0_.num7 as num14_0_, biglotto0_.numsum as numsum15_0_, biglotto0_.period as period16_0_, biglotto0_.scount as scount17_0_, biglotto0_.t10count as t18_0_, biglotto0_.t15count as t19_0_, biglotto0_.t5count as t20_0_, biglotto0_.week as week21_0_ from biglotto biglotto0_ order by biglotto0_.id asc
Job2 End
Job1 End
Job1 Start:15
Hibernate: select biglotto0_.id as id1_0_, biglotto0_.b10count as b2_0_, biglotto0_.b15count as b3_0_, biglotto0_.b1count as b4_0_, biglotto0_.b5count as b5_0_, biglotto0_.date as date6_0_, biglotto0_.dcount as dcount7_0_, biglotto0_.num1 as num8_0_, biglotto0_.num2 as num9_0_, biglotto0_.num3 as num10_0_, biglotto0_.num4 as num11_0_, biglotto0_.num5 as num12_0_, biglotto0_.num6 as num13_0_, biglotto0_.num7 as num14_0_, biglotto0_.numsum as numsum15_0_, biglotto0_.period as period16_0_, biglotto0_.scount as scount17_0_, biglotto0_.t10count as t18_0_, biglotto0_.t15count as t19_0_, biglotto0_.t5count as t20_0_, biglotto0_.week as week21_0_ from biglotto biglotto0_ order by biglotto0_.id asc
Job2 Start:16
Hibernate: select biglotto0_.id as id1_0_, biglotto0_.b10count as b2_0_, biglotto0_.b15count as b3_0_, biglotto0_.b1count as b4_0_, biglotto0_.b5count as b5_0_, biglotto0_.date as date6_0_, biglotto0_.dcount as dcount7_0_, biglotto0_.num1 as num8_0_, biglotto0_.num2 as num9_0_, biglotto0_.num3 as num10_0_, biglotto0_.num4 as num11_0_, biglotto0_.num5 as num12_0_, biglotto0_.num6 as num13_0_, biglotto0_.num7 as num14_0_, biglotto0_.numsum as numsum15_0_, biglotto0_.period as period16_0_, biglotto0_.scount as scount17_0_, biglotto0_.t10count as t18_0_, biglotto0_.t15count as t19_0_, biglotto0_.t5count as t20_0_, biglotto0_.week as week21_0_ from biglotto biglotto0_ order by biglotto0_.id asc
Job2 End
Job1 End
  • 可以看到是不同的 Thread Id 在執行指定的工作
  • JPA 底層好像是 Hibernate 吧?...XD

如果你想把排程狀態存在資料庫的話可以這樣改
增加 quartz.properties

quartz.properties
org.quartz.threadPool.threadCount = 3

org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX 

#org.quartz.jobStore.class = org.springframework.scheduling.quartz.LocalDataSourceJobStore
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.PostgreSQLDelegate
QuartzConfiguration.java
package com.lottery.scheduled;

import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.scheduling.quartz.CronTriggerFactoryBean;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;

@Configuration 
public class QuartzConfiguration {
    
    @Autowired
    private DataSource dataSource;
    
    @Autowired
    private ResourceLoader resourceLoader;
    
    @Autowired
    @Qualifier("Cron1")
    private CronTriggerFactoryBean cron1;
    
    @Autowired
    @Qualifier("Cron2")
    private CronTriggerFactoryBean cron2;

    @Bean
    public AutoWiringSpringBeanJobFactory autoWiringSpringBeanJobFactory(){
        return new AutoWiringSpringBeanJobFactory();
    }

    @Bean
    public SchedulerFactoryBean schedulerFactoryBean() {
        SchedulerFactoryBean scheduler = new SchedulerFactoryBean();
        Resource resource = resourceLoader.getResource("classpath:quartz.properties");
        scheduler.setConfigLocation(resource);
        scheduler.setDataSource(dataSource);
        scheduler.setJobFactory(autoWiringSpringBeanJobFactory());
        scheduler.setTriggers(cron1.getObject(), cron2.getObject());
        return scheduler;
    }
}
  • 增加 DataSource 跟 ResourceLoader

還要在資料庫新增相關表格後就可以在 qrtz_fired_triggers 這個資料表看到目前是誰正在執行

完成

參考資料
Quartz工作机制
Spring 4 + Quartz 2 Scheduler Integration Annotation Example using JavaConfig
quartz-cron-触发器规则

← Use SpringCloud To Synchronize All Nodes Configuration Spring Boot Schedule →
 
comments powered by Disqus