用Spring Batch实现通用批量调度系统

·  阅读 1205

接上一篇文章 Spring Batch的核心概念

了解了Spring Batch的核心概念以后,可以用Spring Batch实现一个比较通用的批量参数调度系统

后面简称批量调度系统。

说明

强烈要求先花5分钟阅读上一篇文章。 本篇文章设计并提供了部分代码,实现了该批量调度系统,仅供参考。

例如,有参数 1,2,3,4,5(也可以是时间),需要自动地轮流调度某个Service。

service.doWork(1);
service.doWork(2);
service.doWork(3);
service.doWork(4);
service.doWork(5);
复制代码

具体doWork是其他系统所作的事情,本系统不关心。

这里可能就要问了,直接一个for循环不行吗?

for(val pa: seq) {
    service.doWork(pa);
}
复制代码

行,但是给我PA,这个能叫方案?这不就是单系统,单JVM,低可用性,低可靠性嘛?

请看后面的设计和实现!

功能

需求分析

如图,本文的目标是图中中间位置的Spring Batch Schedule System

场景

  • 它没有时间限制,仅构建一个调用序列,也可以称作是工作流,workflow.
  • 不关注执行的任务本身,仅关注参数序列
  • 关注每次调用的结果,异常,可以看到每次调用的参数
  • 失败的情况下,具备重试功能

设计

系统的组成:

  • Bootstrap启动类
  • Config
  • Controller
  • Dispatcher
  • DAO层

Spring Batch设计

这图画的十分有水平:这就是一个标准的SpringBoot项目!

问题在于,有多少事情,需要我们亲自去做,有多少事情,是Spring Batch来做?

前面一篇文章里面有Execution,Step, Job,JobInstance,这些都是SpringBatch来做,我们统统都不做。

这里仅仅需要做的事:

  • 仅需1张表,存储参数,这里命名为 Task,字段列表:(id, params, method, result)
  • 仅需1个DAO Repository,实现用JPA,或者Mybatis都行。
  • 仅需1个Controller,一个RequestMapping,提供创建并执行的入口。
  • 其余的全靠编,编写调用图即可。

Job 对应Tasklet

代码

Bootstrap启动类

@SpringBootApplication
@EnableBatchProcessing
public class BatchJobApplication {
    public static void main(String[] args) {
        SpringApplication.run(BatchJobApplication.class, args);
    }
}
复制代码

Config

AppConfig

上一篇文章中有,JobLauncher,是SpringBatch的组件!这里声明一下,给他定义一个异步的线程池!

@Configuration
public class AppConfig {
    @Value("${job.core.pool.size:50}")
    private int jobCorePoolSize;

    @Value("${job.max.pool.size:100}")
    private int jobMaxPoolSize;

    @Bean("appAsyncJobLauncher")
    public JobLauncher jobLauncher(JobRepository jobRepository) {
        ThreadPoolTaskExecutor simpleAsyncTaskExecutor = new ThreadPoolTaskExecutor();
        simpleAsyncTaskExecutor.setThreadNamePrefix("job-Exec-");
        simpleAsyncTaskExecutor.setCorePoolSize(jobCorePoolSize);
        simpleAsyncTaskExecutor.setMaxPoolSize(jobMaxPoolSize);
        simpleAsyncTaskExecutor.afterPropertiesSet();
        try {
            SimpleJobLauncher jobLauncher = new SimpleJobLauncher();
            jobLauncher.setJobRepository(jobRepository);
            jobLauncher.setTaskExecutor(simpleAsyncTaskExecutor);
            jobLauncher.afterPropertiesSet();
            return jobLauncher;
        } catch (Exception e) {
            throw new BatchConfigurationException(e);
        }
    }
}
复制代码

Datasource

标配的HikariDatasource连接池。也可以不用定义,使用默认的即可,这里显式定义出来,表示用这种方式,可以再定义一套数据连接,连接自己的业务数据库,和SpringBatch的数据库区别开!!

    @Bean
    @Primary
    public DataSource datasource(@Qualifier("dataSourceProperties") DataSourceProperties properties) {
        return properties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
    }

    @Bean
    @Primary
    @ConfigurationProperties("spring.datasource")
    public DataSourceProperties dataSourceProperties() {
        return new DataSourceProperties();
    }
复制代码

Controller

@RestController
@RequestMapping("/api/task")
public class JobController {
    private static final Logger logger = LoggerFactory.getLogger(JobController.class);
    @Autowired
    private JobDispatcher jobDispatcher;
    
...//启动作业入口    
    @PostMapping("/executeBatchTask")
    public ResponseEntity executeBatchTask(@RequestBody AutoExecParam autoExecParam) {
        JobExecution jobExecution = jobDispatcher.launchJobFlow(autoExecParam);
        if (jobExecution == null) {
            return new ResponseEntity<>(ApiRet.buildFailed("Failed"), null, HttpStatus.OK);
        }
        logger.info("jobExection execute command proceed, async executionId = {}", jobExecution.getJobId());
        return new ResponseEntity<>(ApiRet.buildSuccess(), null, HttpStatus.OK);
    }
...
复制代码

Dispatcher

@Service
public class JobDispatcher {
    @Autowired
    public JobBuilderFactory jobBuilderFactory;
    @Autowired
    public StepBuilderFactory stepBuilderFactory;

    public JobExecution launchJobFlow(AutoExecParam autoExecParam) {
        //1. 获取作业定义task,例如以参数paramArray=[1,2,3,4,5] 和调用的目标serviceA等信息
        JobBuilder jobBuilder = this.jobBuilderFactory.get("某些前缀规则"+ "_" + dateFormat + "-" + LocalDateTime.now().getHour() / 8);
        
        //2. 定义Tasklet表示的Steps
        List<String> stepNames = Lists.newArrayList();
        List<TaskletStep> taskletSteps = Lists.newArrayList();
        
        paramArray.forEach(x -> {
            final int step = i.getAndIncrement();
            String stepName = String.format("sid:%d_step:%d", task.getId(), step);
            //这里我们直接把每一步的参数传入SimpleTasklet
            TaskletStep build = this.stepBuilderFactory.get(stepName)
                    .tasklet(new SimpleTasklet(schdtask, paramArray.getJSONObject(step).getInnerMap(), actualExecutor))
                    .build();
            stepNames.add(stepName);
            taskletSteps.add(build);
        });
        
         //3. 这里以串行为例,构建执行网络图
         SimpleJobBuilder start = jobBuilder.start(taskletSteps.get(0));
            for (int step = 1; step < taskletSteps.size(); step++) {
                start.next(taskletSteps.get(step));
            }
         //4. 这里构建出作业Job   
         Job builtJob = start.build();
        
         //5. 执行作业
        JobExecution run = null;
        try {
            Date date = new Date();
            JobParametersBuilder jobParametersBuilder = new JobParametersBuilder();

            for (String key : regularParams.keySet()) {
                jobParametersBuilder.addString(key, regularParams.getString(key));
            }
            //将执行参数写入JobParameters
            for (int step = 0; step < i.get(); step++) {
                JSONObject jsonObject = paramArray.getJSONObject(step);
                jobParametersBuilder.addString(stepNames.get(step), jsonObject.toJSONString());
            }
            //这里仅仅往参数里随意写一个标记
            jobParametersBuilder.addDate("parameterGenerated", date);

            run = jobLauncher.run(build, jobParametersBuilder.toJobParameters());
        } catch (JobExecutionAlreadyRunningException | JobRestartException | JobInstanceAlreadyCompleteException | JobParametersInvalidException e) {
            e.printStackTrace();
        }
        return run;
    }
}
复制代码

Tasklet

public class SimpleTasklet implements Tasklet {
    public SimpleTasklet(Yourtask task,
                         Map<String, Object> sequencedParam,
                         ActualExecutor actualExecutor) {
        this.task = task;
        this.sequencedParam = sequencedParam; //本次的参数集合
        this.actualExecutor = actualExecutor; //自己的业务调用出口
    }
    
     @Override
    public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) {
        //调用执行serviceA.doWork
        Resp resp = actualExecutor.doWork();
        if (resp == null) {
        	return RepeatStatus.FINISHED;
        }
        //其他操作
        return RepeatStatus.FINISHED;
    }
}
复制代码

注意RepeatStatus枚举类的两种状态含义是不同的

篇幅有限,这里不会探讨他们的用途和场景。

public enum RepeatStatus {

	/**
	 * Indicates that processing can continue.
	 */
	CONTINUABLE(true), 
	/**
	 * Indicates that processing is finished (either successful or unsuccessful)
	 */
	FINISHED(false);
}    
复制代码

完成

有了这些内容,基本上一个批量参数调度系统已经完成了,如果给出全部代码,那估计会吓到读者。

只需要使用Boostrap的启动即可,SpringBatch会在数据库中建立自己的表:

SpringBatch自己维护,不需要我们手动建立 Spring Batch ERD

有了这些表,我们可以读取这些表,查询每一次作业的状态,返回值,异常等信息。非常详细。

展望

本系统充分利用了SpringBatch的开箱即用功能,但尚未完成的内容,包括执行状态的展示等。

但根据SpringBatch的表结构,只要跑一次,就可以看到数据的体现。

因此,可以扩展Controller,抓取,追踪每一个JobInstance的情况,JobExecution,ExecutionContext,这些都不难。这里就只抛砖引玉了。

例如页面可以做成这样: Spring Batch Schedule System

分类:
后端
标签:
分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改