Spring boot 集成Quartz 實現定時任務動態管理服務端

調查了下用Spring boot集成Quartz來實現定時任務的動態管理,記下來備用。

主要使用 Spring boot、Quartz、Mybatis實現,其中Quartz任務在SqlServer數據庫保存,業務數據庫是Oracle。

需要實現數據源的動態管理,定時任務使用Spring IOC管理等功能。

創建Maven project

POM中依賴如下:

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.10.RELEASE</version>
</parent>
<properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <spring.boot.version>1.5.10.RELEASE</spring.boot.version>
        <spring.cloud.version>Edgware.SR2</spring.cloud.version>
        <java.version>1.8</java.version>
</properties> 
<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!-- Spring Boot Test 依賴 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- GSON -->
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
        </dependency>
        <!-- 代碼簡化 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!-- 日誌 Log4j2 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-log4j2</artifactId>
        </dependency>
        <!-- Log4j2 異步支持 -->
        <dependency>
            <groupId>com.lmax</groupId>
            <artifactId>disruptor</artifactId>
            <version>3.3.6</version>
        </dependency>
        <!-- 使用 @ConfigurationProperties @Value 使用 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>        
        <!-- quartz -->
        <dependency>
            <groupId>org.quartz-scheduler</groupId>
            <artifactId>quartz</artifactId>
            <version>2.2.1</version>
        </dependency>
        <dependency>
            <groupId>org.quartz-scheduler</groupId>
            <artifactId>quartz-jobs</artifactId>
            <version>2.2.1</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context-support</artifactId>
        </dependency>
        <!-- mybatis -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.1</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.github.pagehelper/pagehelper-spring-boot-starter -->
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
            <version>1.2.3</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.8</version>
        </dependency>
        <!--ojdbc -->
        <dependency>
            <groupId>com.oracle</groupId>
            <artifactId>ojdbc6</artifactId>
            <version>11.2.0</version>
        </dependency>
        <!-- sqlserver -->
        <dependency>
            <groupId>com.microsoft.sqlserver</groupId>
            <artifactId>sqljdbc4</artifactId>
            <version>4.0</version>
        </dependency>
        <!-- api document -->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.2.2</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.2.2</version>
        </dependency>
        <!--junit -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <configuration>
                        <source>${java.version}</source>
                        <target>${java.version}</target>
                        <skip>true</skip>
                        <encoding>${project.build.sourceEncoding}</encoding>
                    </configuration>
                </plugin>
                <!-- 忽略Test包 -->
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-surefire-plugin</artifactId>
                    <configuration>
                        <skipTests>true</skipTests>
                    </configuration>
                </plugin>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                    <configuration>
                        <mainClass>com.demo.service.job.JobManagerApplication</mainClass>
                    </configuration>
                    <executions>
                        <execution>
                            <goals>
                                <goal>repackage</goal>
                            </goals>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>

上面需要注意的是ojdbc的jar需要手動安裝到本地maven倉庫。

配置Quartz

在SqlServer中創建Quartz需要的數據庫和表,這裏的建表語句可以自行到Quartz官網下載;
在src/main/resources下新建quartz.properties文件用來配置Quartz:
org.quartz.scheduler.instanceName = DefaultQuartzScheduler
org.quartz.scheduler.rmi.export = false
org.quartz.scheduler.rmi.proxy = false
org.quartz.scheduler.wrapJobExecutionInUserTransaction = false
# 線程池配置
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount = 5
org.quartz.threadPool.threadPriority = 5
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread = true
#任務持久化
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.tablePrefix = QRTZ_
org.quartz.jobStore.dataSource = qzDS
org.quartz.jobStore.misfireThreshold = 5000
org.quartz.dataSource.qzDS.driver =  com.microsoft.sqlserver.jdbc.SQLServerDriver
org.quartz.dataSource.qzDS.URL = jdbc:sqlserver://127.0.0.1;DatabaseName=QUARTZ_DB
org.quartz.dataSource.qzDS.user = 
org.quartz.dataSource.qzDS.password = 
org.quartz.dataSource.qzDS.maxConnections = 10
編寫代碼,使用配置文件中的配置,並且把定時任務交給Spring IOC處理

自定義任務工廠類,將定時任務交給Spring

@Component
public class QuartzJobFactory extends AdaptableJobFactory {
    @Autowired
    private AutowireCapableBeanFactory autowireCapableBeanFactory;

    /**
     * @see org.springframework.scheduling.quartz.AdaptableJobFactory#createJobInstance(org.quartz.spi.TriggerFiredBundle)
     */
    @Override
    protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
        Object jobInstance = super.createJobInstance(bundle);
        // 實現Job的IOC管理
        autowireCapableBeanFactory.autowireBean(jobInstance);
        return jobInstance;
    }
}

配置定時任務調度器

@Configuration
public class SchedulerConfig {
    @Autowired
    private QuartzJobFactory jobFactory;

    /**
     * 定時任務初始化監聽器
     * 
     * @Title: initListener
     */
    @Bean
    public QuartzInitializerListener initListener() {
        return new QuartzInitializerListener();
    }

    /**
     * 任務調度器
     * 
     * @Title: scheduler
     * @throws IOException
     */
    @Bean(name = "Scheduler")
    public Scheduler scheduler() throws IOException {
        return schedulerFactoryBean().getScheduler();
    }

    /**
     * 任務調度器工廠
     * 
     * @Title: schedulerFactoryBean
     * @throws IOException
     */
    @Bean(name = "SchedulerFactory")
    public SchedulerFactoryBean schedulerFactoryBean() throws IOException {
        SchedulerFactoryBean factory = new SchedulerFactoryBean();
        factory.setQuartzProperties(quartzProperties());
        factory.setOverwriteExistingJobs(true);
        factory.setStartupDelay(15);
        factory.setJobFactory(jobFactory);
        return factory;
    }

    /**
     * 讀取定時任務配置
     * 
     * @Title: quartzProperties
     * @throws IOException
     */
    @Bean
    public Properties quartzProperties() throws IOException {
        PropertiesFactoryBean propertiesFactoryBean = new PropertiesFactoryBean();
        propertiesFactoryBean.setLocation(new ClassPathResource("/quartz.properties"));
        propertiesFactoryBean.afterPropertiesSet();
        return propertiesFactoryBean.getObject();
    }
}

配置多數據源

定義application.properties
server.port = 8001
spring.application.name =job-manager
#business data source#
biz.datasource.type = com.alibaba.druid.pool.DruidDataSource
biz.datasource.url = jdbc:oracle:thin:@127.0.0.1:1521:orcl
biz.datasource.driver-class-name = oracle.jdbc.OracleDriver
biz.datasource.username = 
biz.datasource.password = 
#job data sources#
job.datasource.type = com.alibaba.druid.pool.DruidDataSource
job.datasource.url = jdbc:sqlserver://127.0.0.1;DatabaseName=QUARTZ_DB
job.datasource.driver-class-name = com.microsoft.sqlserver.jdbc.SQLServerDriver
job.datasource.username = 
job.datasource.password = 
#druid#
spring.druid.initialSize = 10
spring.druid.minIdle = 10
spring.druid.maxActive = 20
spring.druid.maxWait = 60000
spring.druid.maxOpenPreparedStatements = 50
spring.druid.validationQuery = select count(0) from dual
spring.druid.testWhileIdle = true
#Mybatis#
mybatis.mapper-locations = classpath:mapper/mybatis-sqlmap-*.xml
mybatis.type-aliases-package = com.demo.service.job.entity
#PageHelper#
pagehelper.autoDialect = true
pagehelper.closeConn = true
pagehelper.offset-as-page-num = false
使用Aspect實現動態切換數據源

創建數據源名稱枚舉

public enum DataSourceNameEnum {
        /** 業務庫 */
        BUSINESS("biz"),
        /** 定時任務庫 */
        QUARTZ_JOB("job");
    /** 數據源名稱 */
    private String name;

    /**
     * 
     * 構造函數
     * 
     * @param name
     */
    private DataSourceNameEnum(String name) {
        this.name = name;
    }

    /**
     * 獲取:數據源名稱
     */
    public String getName() {
        return name;
    }
}

自定義數據源註解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DataSourceAnnotation {
    /** 默認使用業務數據庫 */
    DataSourceNameEnum value() default DataSourceNameEnum.BUSINESS;
}

數據源上下文管理器

public class DataSourceContextHolder {
    final static ThreadLocal<String> local = new ThreadLocal<>();

    public static void setDataSourceName(String name) {
        local.set(name);
    }

    public static String getDataSourceName() {
        return local.get();
    }
}

實現動態數據源

@Log4j2
public class DynamicDataSource extends AbstractRoutingDataSource {

    /**
     * @see org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource#determineCurrentLookupKey()
     */
    @Override
    protected Object determineCurrentLookupKey() {
        String dataSourceName = DataSourceContextHolder.getDataSourceName();
        log.debug("Current data source name is {}.", dataSourceName);
        return dataSourceName;
    }
}

數據源配置管理

@Configuration
public class DataSourceConfig {
    @Bean(name = "biz")
    @ConfigurationProperties(prefix = "biz.datasource")
    public DataSource bizDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean(name = "job")
    @ConfigurationProperties(prefix = "job.datasource")
    public DataSource jobDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean(name = "dataSource")
    @Primary
    public DataSource dataSource() {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        DataSource biz = bizDataSource();
        DataSource job = jobDataSource();
        dynamicDataSource.setDefaultTargetDataSource(biz);
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DataSourceNameEnum.BUSINESS.getName(), biz);
        targetDataSources.put(DataSourceNameEnum.QUARTZ_JOB.getName(), job);
        dynamicDataSource.setTargetDataSources(targetDataSources);
        return dynamicDataSource;
    }
}

使用AOP實現數據源切換

@Aspect
@Order(1)
@Component
@Log4j2
public class DataSourceAspect {
    @Pointcut("execution(* com.demo.service.job.service.impl.*Impl.*(..))")
    public void aspect() {}

    @Before("aspect()")
    private void before(JoinPoint point) {
        Object target = point.getTarget();
        String methodName = point.getSignature().getName();
        Class<?> clazz = target.getClass();
        Class<?>[] parameterTypes = ((MethodSignature) point.getSignature()).getMethod().getParameterTypes();

        Method method = null;
        try {
            method = clazz.getMethod(methodName, parameterTypes);
        } catch (Exception e) {
            log.error("Get {}.{} ERROR", clazz.getName(), methodName);
        }
        if (null != method && method.isAnnotationPresent(DataSourceAnnotation.class)) {
            DataSourceAnnotation dataSource = method.getAnnotation(DataSourceAnnotation.class);
            String name = dataSource.value().getName();
            DataSourceContextHolder.setDataSourceName(name);
            log.debug("Switch Data Source To {}.", name);
        }
    }
}


最重要的是,關閉Spring boot自動數據源配置

// 禁用數據源自動配置
@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
// 事務處理在數據源切換完成後
@EnableTransactionManagement(order = 2)
public class JobManagerApplication {
    public static void main(String[] args) {
        SpringApplication.run(JobManagerApplication.class, args);
    }
}

定時任務動態管理的實現

自定義定時任務接口

public interface BaseQuartzJob extends Job {
    void execute(JobExecutionContext executionContext) throws JobExecutionException;
}

自定義任務

@Log4j2
public class PrintJob implements BaseQuartzJob {
    @Override
    public void execute(JobExecutionContext executionContext) throws JobExecutionException {
        log.info("execute printjob now");
    }
}
@Log4j2
public class FyAssetJob implements BaseQuartzJob {
    @Autowired
    private TAssetFyLmsRunLogService assetFyLmsRunLogService;

    @Override
    public void execute(JobExecutionContext executionContext) throws JobExecutionException {
        log.info("=== check run log begin===");
        TAssetFyLmsRunLogPageParam param = new TAssetFyLmsRunLogPageParam();
        param.setPageNum(1);
        param.setPageSize(12);
        List<TAssetFyLmsRunLog> list = assetFyLmsRunLogService.getAll(param);
        if (null != list && !list.isEmpty()) {
            for (TAssetFyLmsRunLog item : list) {
                log.debug(item.toString());
            }
        }
        log.info("=== check run log end===");
    }
}

其中,第二個任務,使用了Spring IOC的Bean。

定時任務管理

@Log4j2
@RestController
public class QuartzJobController {
    @Autowired
    @Qualifier("Scheduler")
    private Scheduler scheduler;

    @Autowired
    private QuartzJobInfoService quartzJobInfoService;

    /**
     * 分頁檢索
     * 
     * @Title: list
     */
    @ApiOperation(value = "分頁檢索",
        notes = "")
    @RequestMapping(value = "/list",
        method = RequestMethod.POST)
    public ResponseEntity<BaseResponse<BasePageResult<QuartzJobInfo>>> list(
        @RequestBody BaseRequest<QuartzJobPageParam> req) {
        BaseResponse<BasePageResult<QuartzJobInfo>> response = new BaseResponse<>();
        BasePageResult<QuartzJobInfo> result = quartzJobInfoService.list(req);
        response.setResult(result);
        return new ResponseEntity<BaseResponse<BasePageResult<QuartzJobInfo>>>(response, HttpStatus.OK);
    }

    /**
     * 新增任務
     * 
     * @Title: add
     */
    @ApiOperation(value = "新增任務",
        notes = "")
    @RequestMapping(value = "/add",
        method = RequestMethod.POST)
    public ResponseEntity<BaseResponse<String>> add(@RequestBody BaseRequest<QuarzJobParam> req) {
        BaseResponse<String> response = new BaseResponse<>();
        String code = ResultCodeEnum.SUCCESS.getCode();
        String result = "新增定時任務成功!";

        // get parameter fro request
        QuarzJobParam item = req.getParam();
        String jobClassName = item.getJobClassName();
        String jobGroupName = item.getJobGroup();
        String cronExpression = item.getCornExpression();

        try {
            // build jobDetail and trigger
            JobDetail jobDetail = JobBuilder.newJob(getClass(jobClassName).getClass())
                .withIdentity(jobClassName, jobGroupName).build();
            // 表達式調度構建器(即任務執行的時間)
            CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression);
            // 按新的cronExpression表達式構建一個新的trigger
            CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(jobClassName, jobGroupName)
                .withSchedule(scheduleBuilder).build();
            // 執行調度
            scheduler.start();
            scheduler.scheduleJob(jobDetail, trigger);
        } catch (SchedulerException e) {
            code = ResultCodeEnum.ERROR.getCode();
            result = "調度定時任務失敗!";
            log.error("調度定時任務失敗:{}", e.getMessage());
        } catch (Exception e) {
            code = ResultCodeEnum.ERROR.getCode();
            result = "構建定時任務失敗!";
            log.error("構建定時任務失敗:{}", e.getMessage());
        }

        response.setCode(code);
        response.setResult(result);
        return new ResponseEntity<BaseResponse<String>>(response, HttpStatus.OK);
    }

    /**
     * 暫停任務
     * 
     * @Title: pause
     */
    @ApiOperation(value = "暫停任務",
        notes = "")
    @RequestMapping(value = "/pause",
        method = RequestMethod.POST)
    public ResponseEntity<BaseResponse<String>> pause(@RequestBody BaseRequest<QuarzJobParam> req) {
        BaseResponse<String> response = new BaseResponse<>();
        String code = ResultCodeEnum.SUCCESS.getCode();
        String result = "暫停定時任務成功!";

        // get parameter fro request
        QuarzJobParam item = req.getParam();
        String jobClassName = item.getJobClassName();
        String jobGroupName = item.getJobGroup();

        try {
            scheduler.pauseJob(JobKey.jobKey(jobClassName, jobGroupName));
        } catch (SchedulerException e) {
            code = ResultCodeEnum.ERROR.getCode();
            result = "暫停定時任務失敗!";
            log.error("暫停定時任務失敗:{}", e.getMessage());
        }

        response.setCode(code);
        response.setResult(result);
        return new ResponseEntity<BaseResponse<String>>(response, HttpStatus.OK);
    }

    /**
     * 恢復任務
     * 
     * @Title: resume
     */
    @ApiOperation(value = "恢復任務",
        notes = "")
    @RequestMapping(value = "/resume",
        method = RequestMethod.POST)
    public ResponseEntity<BaseResponse<String>> resume(@RequestBody BaseRequest<QuarzJobParam> req) {
        BaseResponse<String> response = new BaseResponse<>();
        String code = ResultCodeEnum.SUCCESS.getCode();
        String result = "恢復定時任務成功!";

        // get parameter fro request
        QuarzJobParam item = req.getParam();
        String jobClassName = item.getJobClassName();
        String jobGroupName = item.getJobGroup();

        try {
            scheduler.resumeJob(JobKey.jobKey(jobClassName, jobGroupName));
        } catch (SchedulerException e) {
            code = ResultCodeEnum.ERROR.getCode();
            result = "恢復定時任務失敗!";
            log.error("恢復定時任務失敗:{}", e.getMessage());
        }

        response.setCode(code);
        response.setResult(result);
        return new ResponseEntity<BaseResponse<String>>(response, HttpStatus.OK);
    }

    /**
     * 刪除任務
     * 
     * @Title: remove
     */
    @ApiOperation(value = "刪除任務",
        notes = "")
    @RequestMapping(value = "/remove",
        method = RequestMethod.POST)
    public ResponseEntity<BaseResponse<String>> remove(@RequestBody BaseRequest<QuarzJobParam> req) {
        BaseResponse<String> response = new BaseResponse<>();
        String code = ResultCodeEnum.SUCCESS.getCode();
        String result = "刪除定時任務成功!";

        // get parameter fro request
        QuarzJobParam item = req.getParam();
        String jobClassName = item.getJobClassName();
        String jobGroupName = item.getJobGroup();

        try {
            scheduler.pauseTrigger(TriggerKey.triggerKey(jobClassName, jobGroupName));
            scheduler.unscheduleJob(TriggerKey.triggerKey(jobClassName, jobGroupName));
            scheduler.deleteJob(JobKey.jobKey(jobClassName, jobGroupName));
        } catch (SchedulerException e) {
            code = ResultCodeEnum.ERROR.getCode();
            result = "刪除定時任務失敗!";
            log.error("刪除定時任務失敗:{}", e.getMessage());
        }

        response.setCode(code);
        response.setResult(result);
        return new ResponseEntity<BaseResponse<String>>(response, HttpStatus.OK);
    }

    /**
     * 更新任務觸發器
     * 
     * @Title: refresh
     */
    @ApiOperation(value = "更新任務",
        notes = "")
    @RequestMapping(value = "/refresh",
        method = RequestMethod.POST)
    public ResponseEntity<BaseResponse<String>> refresh(@RequestBody BaseRequest<QuarzJobParam> req) {
        BaseResponse<String> response = new BaseResponse<>();
        String code = ResultCodeEnum.SUCCESS.getCode();
        String result = "更新定時任務成功!";

        // get parameter fro request
        QuarzJobParam item = req.getParam();
        String jobClassName = item.getJobClassName();
        String jobGroupName = item.getJobGroup();
        String cronExpression = item.getCornExpression();

        try {
            TriggerKey triggerKey = TriggerKey.triggerKey(jobClassName, jobGroupName);
            // 表達式調度構建器
            CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression);
            CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);
            // 按新的cronExpression表達式重新構建trigger
            trigger = trigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(scheduleBuilder).build();
            // 按新的trigger重新設置job執行
            scheduler.rescheduleJob(triggerKey, trigger);
        } catch (SchedulerException e) {
            code = ResultCodeEnum.ERROR.getCode();
            result = "更新定時任務失敗!";
            log.error("更新定時任務失敗:{}", e.getMessage());
        }

        response.setCode(code);
        response.setResult(result);
        return new ResponseEntity<BaseResponse<String>>(response, HttpStatus.OK);
    }

    /**
     * 獲得任務類
     * 
     * @Title: getClass
     * @throws Exception
     */
    private static BaseQuartzJob getClass(String className) throws Exception {
        Class<?> clazz = Class.forName(className);
        return (BaseQuartzJob) clazz.newInstance();
    }
}

主要的代碼已經帖完了,看一下工程架構

啓動項目,使用SWAGGER查看API

可以使用swagger直接進行測試,將我們編寫的兩個定時任務添加到定時任務調度器中。

下圖是定時任務的執行日誌


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章