接上一篇文章 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层
这图画的十分有水平:这就是一个标准的SpringBoot项目!
问题在于,有多少事情,需要我们亲自去做,有多少事情,是Spring Batch来做?
前面一篇文章里面有Execution,Step, Job,JobInstance,这些都是SpringBatch来做,我们统统都不做。
这里仅仅需要做的事:
- 仅需1张表,存储参数,这里命名为 Task,字段列表:
(id, params, method, result)
。 - 仅需1个DAO Repository,实现用JPA,或者Mybatis都行。
- 仅需1个Controller,一个RequestMapping,提供创建并执行的入口。
- 其余的全靠编,编写调用图即可。
代码
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自己维护,不需要我们手动建立
有了这些表,我们可以读取这些表,查询每一次作业的状态,返回值,异常等信息。非常详细。
展望
本系统充分利用了SpringBatch的开箱即用功能,但尚未完成的内容,包括执行状态的展示等。
但根据SpringBatch的表结构,只要跑一次,就可以看到数据的体现。
因此,可以扩展Controller,抓取,追踪每一个JobInstance的情况,JobExecution,ExecutionContext,这些都不难。这里就只抛砖引玉了。
例如页面可以做成这样: