spring batch Job详解


github地址:

https://github.com/a18792721831/studybatch.git

文章列表:

spring batch 入门

spring batch连接数据库

spring batch元数据

spring batch Job详解

spring batch step详解

spring batch ItemReader详解

spring batch itemProcess详解

spring batch itemWriter详解

spring batch 作业流

spring batch 健壮性

spring batch 扩展性

Job调度原理

一个Job由1个或者多个Step组成,Step有读写处理三部分组成;Job运行期间,所有的数据通过Job Repository进行持久化,同时通过Job Launcher负责调度Job作业。

image-20201109165536490

Job的基本配置

Job的核心属性:

image-20201109165640147

Job的组成:

image-20201109165705304

Job重启

通过设置restartable可以定义job是否可以重启。默认情况下Job是可以重启的,但是需要注意,即使配置了Job可以重启,仍然需要保证Job Instance的状态一定不为"COMPLETED".

不可重启Job

不可重启job的定义非常简单,只需要调用一个方法即可:

image-20201109185217758

    @Bean
    public Job noResJob(JobBuilderFactory jobBuilderFactory, StepBuilderFactory stepBuilderFactory) {
    
    
        return jobBuilderFactory.get("job-4-no-restart")
                .incrementer(new RunIdIncrementer())
                .preventRestart()
                .flow(stepBuilderFactory.get("step-4-no-restart").tasklet(new Tasklet() {
    
    
                    @Override
                    public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
    
    
                        System.out.println("exec!" + new SimpleDateFormat("YYYY-MM-DD HH:MM:SS").format(new Date()));
                        return RepeatStatus.CONTINUABLE;
                    }
                }).build())
                .end()
                .build();
    }

这里创建了一个job,名字是job-4-no-restart,然后设置不可重启(默认是可以重启的)。这个Job非常的简单,只是不停的在打印exec!+时间。

是一个死循环。所以,需要手动停止。手动停止,在数据库中就不是"COMPLETED".

image-20201109184850812

image-20201109184830405

此时状态不是"COMPLETED",但是因为我们设置了不可重启,所以,在不修改任何代码的情况下,重新启动服务:

image-20201109185023074

就会抛出JobRestartException。

可重启Job

Job默认是可以重启的。

我们拷贝不可重启的bean,然后去掉设置不可重启的操作。

image-20201109185303763

记得在调度中启动

image-20201109185339279

第一次运行 还是死循环

image-20201109185408462

手动停止

image-20201109185503426

image-20201109185441387

然后重新启动

image-20201109185733935

虽然也异常了,但是,异常非常明显不一样,这是说有一个JobExecution已经在执行了。

我们现在修改job,不要让job是一个死循环,而是当循环次数大于10次的时候,抛出异常:

image-20201109192634158

然后,在启动的时候修改job名字,或者传入一个参数。

image-20201109192709983

启动在循环到第10次的时候,出现异常

image-20201109192738791

此时数据库中,记录的状态是FAILED.接着在不修改参数的前提下,注释掉抛出异常的代码。

image-20201109192949280

因为我们没有修改Job的名字,也没有修改Job的参数,所以,在spring batch看来,这就是同一个Job Instance。

image-20201109193306234

接着重新启动,重新运行这个Job Instance

image-20201109193253875

Job拦截器

spring batch框架在Job执行阶段提供了拦截器,使得在Job执行前后能够加入自定义的业务逻辑处理。

Job单个拦截器

Job 执行阶段拦截器需要实现接口:JobExecutionListener

@Configuration
public class JobListener implements JobExecutionListener {
    
    

    @Override
    public void beforeJob(JobExecution jobExecution) {
    
    
        System.out.println("JobListener before " + jobExecution.getJobInstance().getJobName());
    }

    @Override
    public void afterJob(JobExecution jobExecution) {
    
    
        System.out.println("JobListener after " + jobExecution.getExitStatus().getExitDescription());
    }
}

使用

@Configuration
@EnableBatchProcessing
public class LisJobConf {
    
    

    @Bean
    public String runJob(JobLauncher jobLauncher, Job lisJob) throws JobParametersInvalidException, JobExecutionAlreadyRunningException, JobRestartException, JobInstanceAlreadyCompleteException {
    
    
        jobLauncher.run(lisJob, new JobParametersBuilder().addLong("id", 1L).toJobParameters());
        return "";
    }

    @Bean
    public Job lisJob(JobBuilderFactory jobBuilderFactory, StepBuilderFactory stepBuilderFactory, JobListener jobListener) {
    
    
        return jobBuilderFactory.get("study4-lis")
                .start(stepBuilderFactory.get("study4-step")
                        .tasklet(new Tasklet() {
    
    
                            @Override
                            public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
    
    
                                System.out.println("exec");
                                return RepeatStatus.FINISHED;
                            }
                        }).build())
                .listener(jobListener)
                .build();
    }

}

执行结果

image-20201109195042406

这里有个坑,Job的监听,只能实现接口,不能使用注解。

因为其他的监听,有Object参数的重载,而Job的监听Builder,没有Object的重载。

比如

@Component
public class AnnJobListener {
    
    

    @BeforeJob
    public void beforeJob(JobExecution jobExecution) {
    
    
        System.out.println("before " + jobExecution.getJobInstance().getJobName());
    }

    @AfterJob
    public void afterJob(JobExecution jobExecution) {
    
    
        System.out.println("after " + jobExecution.getJobInstance().getJobName());
    }

}
@EnableBatchProcessing
@Configuration
public class LisAnnJobConf{
    
    

    @Bean
    public String runJob(JobLauncher jobLauncher,Job annJob) throws JobParametersInvalidException, JobExecutionAlreadyRunningException, JobRestartException, JobInstanceAlreadyCompleteException {
    
    
        jobLauncher.run(annJob, new JobParameters());
        return "";
    }

    @Bean
    public Job annJob(JobBuilderFactory jobBuilderFactory, StepBuilderFactory stepBuilderFactory, AnnJobListener annJobListener) {
    
    
        return jobBuilderFactory.get("study4-anno")
                .start(stepBuilderFactory.get("study-anno")
                .tasklet(new Tasklet() {
    
    
                    @Override
                    public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
    
    
                        System.out.println("exec");
                        return RepeatStatus.FINISHED;
                    }
                })
                        .listener(annJobListener) // 有listerner(Object)的方法重载
                        .build())
                // .listener(annJobListener) 没有listener(Object)的方法重载
                .build();
    }

}

启动

image-20201109203534237

前后操作都没有执行。

Job组合拦截器

在Job中不尽可以配置单个的拦截器,还可以使用CompositeJobExecutionListener实现组合拦截器。

拦截器的顺序根据注册的先后顺序,进行拦截。

比如现在有3个拦截器

@Component
public class SecJobListener implements JobExecutionListener {
    
    
    @Override
    public void beforeJob(JobExecution jobExecution) {
    
    
        System.out.println("SecJobListener before : " + jobExecution.getJobInstance().getJobName());
    }

    @Override
    public void afterJob(JobExecution jobExecution) {
    
    
        System.out.println("SecJobListener after : " + jobExecution.getJobInstance().getJobName());
    }
}
@Component
public class JobListener implements JobExecutionListener {
    
    

    @Override
    public void beforeJob(JobExecution jobExecution) {
    
    
        System.out.println("JobListener before " + jobExecution.getJobInstance().getJobName());
    }

    @Override
    public void afterJob(JobExecution jobExecution) {
    
    
        System.out.println("JobListener after " + jobExecution.getExitStatus().getExitDescription());
    }
}
@Component
public class AnnJobListener implements JobExecutionListener {
    
    

    @Override
    public void beforeJob(JobExecution jobExecution) {
    
    
        System.out.println("AnnJobListener before " + jobExecution.getJobInstance().getJobName());
    }

    @Override
    public void afterJob(JobExecution jobExecution) {
    
    
        System.out.println("AnnJobListener after " + jobExecution.getJobInstance().getJobName());
    }

}

接着,使用组合拦截器,将这三个拦截器配置给一个Job

@EnableBatchProcessing
@Configuration
public class MoreLisJobConf {
    
    

    @Bean
    public String jobRunner(JobLauncher jobLauncher,Job moreLisJob) throws JobParametersInvalidException, JobExecutionAlreadyRunningException, JobRestartException, JobInstanceAlreadyCompleteException {
    
    
        jobLauncher.run(moreLisJob, new JobParametersBuilder().addLong("id", 1L).toJobParameters());
        return "";
    }

    @Bean
    public Job moreLisJob(AnnJobListener annJobListener, JobListener jobListener, SecJobListener secJobListener, JobBuilderFactory jobBuilderFactory, StepBuilderFactory stepBuilderFactory) {
    
    
        CompositeJobExecutionListener listener = new CompositeJobExecutionListener();
        listener.register(annJobListener);
        listener.register(jobListener);
        listener.register(secJobListener);
        return jobBuilderFactory.get("study4-more-listener")
                .start(stepBuilderFactory.get("study4-more-listener")
                        .tasklet(new Tasklet() {
    
    
                            @Override
                            public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
    
    
                                System.out.println("exec!" + LocalTime.now().toString());
                                return RepeatStatus.FINISHED;
                            }
                        }).build())
                .listener(listener)
                .build();
    }

}

组合拦截器,是基于拦截器做了封装。组合拦截器内有一个拦截器列表。同时组合拦截器也实现了JobExecutionListener接口,当spring batch调用组合拦截器的before方法时,内部实际是调用全部的注册的拦截器的before方法;当spring batch调用组合拦截器的after方法时,内部实际倒序调用全部注册的拦截器的after方法。

执行结果

image-20201110192447726

Job Parameters校验

spring batch框架提供了Job作业参数的校验功能,Job Parameters支持4种类型的参数:字符串、时间、长整型和双精度。但是传入的参数不一定符合这个规则,那么就需要对参数进行校验。除了类型校验,还可以有其他的校验,可以自己制定自定义的校验。需要实现接口JobParametersValidator

当然spring batch也提供了一些简单的校验类,可供我们使用

image-20201110193005900

自定义的Job Parameters校验

首先需要实现接口校验的接口

@Component
public class ParameValidatory implements
        JobParametersValidator {
    
    
    @Override
    public void validate(JobParameters parameters) throws JobParametersInvalidException {
    
    
        System.out.println(parameters.getParameters());
        System.out.println("ParameValidatory + " + parameters.getClass().getSimpleName());
    }
}

接着配置到Job上

@EnableBatchProcessing
@Configuration
public class ParameJobConf {
    
    


    @Bean
    public String runJob(JobLauncher jobLauncher,Job parJob) throws JobParametersInvalidException, JobExecutionAlreadyRunningException, JobRestartException, JobInstanceAlreadyCompleteException {
    
    
        jobLauncher.run(parJob,new JobParametersBuilder().addLong("id", 2L).toJobParameters());
        return "";
    }

    @Bean
    public Job parJob(JobBuilderFactory jobBuilderFactory, StepBuilderFactory stepBuilderFactory, ParameValidatory parameValidatory) {
    
    
        return jobBuilderFactory.get("study4-parame")
                .validator(parameValidatory)
                .start(stepBuilderFactory.get("study4-parame")
                .tasklet(new Tasklet() {
    
    
                    @Override
                    public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
    
    
                        System.out.println("exec!"+ LocalTime.now());
                        return RepeatStatus.FINISHED;
                    }
                }).build()).build();
    }

}

启动

image-20201110194643254

为什么会被调用两次呢?

我们打个断点调试下

第一次调用在SimpleJobLauncher中的调用:

image-20201110195027760

第二次是在AbstractJob中调用的

image-20201110195108218

在JobLauncher真正运行的时候,会在执行线程地方启动后,调用Job的execut方法中调用AbstractJob中的validate方法。

默认的Job Parameters校验

spring batch框架默认提供了Job ParametersValidator的实现。

定义使用默认Job Parameters校验的Job

@Configuration
@EnableBatchProcessing
public class DefParJobConnf {
    
    

    @Bean
    public String runJob(JobLauncher jobLauncher,Job defParJob) throws JobParametersInvalidException, JobExecutionAlreadyRunningException, JobRestartException, JobInstanceAlreadyCompleteException {
    
    
        jobLauncher.run(defParJob, new JobParametersBuilder().addLong("id", 2L).toJobParameters());
        return "";
    }

    @Bean
    public Job defParJob(JobBuilderFactory jobBuilderFactory, StepBuilderFactory stepBuilderFactory) {
    
    
        return jobBuilderFactory.get("study4-def-par")
                .start(stepBuilderFactory.get("study4-def-par")
                .tasklet(new Tasklet() {
    
    
                    @Override
                    public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
    
    
                        System.out.println("exec ! " + LocalTime.now().toString());
                        return RepeatStatus.FINISHED;
                    }
                }).build())
                .validator(new DefaultJobParametersValidator(new String[]{
    
    "id"}, new String[]{
    
    "id"}))
                .build();
    }

}

正确运行了

image-20201110201025978

我们要求参数校验,id必填,name可选。

image-20201110201518394

接着将id传入空,name 传入非空

image-20201110201559779

验证通过

image-20201110201709782

如果一个参数都没有呢?

image-20201110202000259

抛出了Job Parameters验证异常,缺失必须的参数:id.

组合的Job Parameters校验

spring 框架还提供了组合校验器CompositeJobparametersValidator

@Configuration
@EnableBatchProcessing
public class CompParJobConf {
    
    

    @Bean
    public String runJob(JobLauncher jobLauncher, Job compParJob) throws JobParametersInvalidException, JobExecutionAlreadyRunningException, JobRestartException, JobInstanceAlreadyCompleteException {
    
    
        jobLauncher.run(compParJob, new JobParametersBuilder().toJobParameters());
        return "";
    }

    @Bean
    public Job compParJob(JobBuilderFactory jobBuilderFactory, StepBuilderFactory stepBuilderFactory, ParameValidatory parameValidatory) {
    
    
        CompositeJobParametersValidator validator = new CompositeJobParametersValidator();
        validator.setValidators(Arrays.asList(parameValidatory, new DefaultJobParametersValidator(new String[]{
    
    "id"}, new String[]{
    
    "name"})));
        return jobBuilderFactory.get("study4-comp-par")
                .start(stepBuilderFactory.get("study4-comp-par")
                        .tasklet(new Tasklet() {
    
    
                            @Override
                            public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
    
    
                                System.out.println("exec! " + LocalTime.now().toString());
                                return RepeatStatus.FINISHED;
                            }
                        }).build())
                .validator(validator)
                .build();
    }

}

我们定义了两个Job Parameters校验器,其中一个是必须有id,可选的name。自定义的Job Parameters校验器则是打印参数列表。

第一次,我们什么参数都不传:

image-20201110203508467

提示少id参数

接着我们传入id参数

运行成功

image-20201110203608269

增加name参数,也是可以运行成功的

image-20201110203705625

需要注意,如果传入了必选参数和可选参数之外的参数,则验证失败(只针对默认的参数校验器,自定义的根据自己的规则校验)

image-20201110203831262

运行

image-20201110203900886

参数校验异常,提示date参数既不在必选参数,也不在可选参数。

Job抽象与继承

抽象Job

在spring batch中,job等同于bean。只要将job这个bean注入到容器中即可。

所以i,创建抽象Job就是创建抽象类。

public abstract class AbsSimpleJob extends SimpleJob {
    
    

    protected final Logger log = LoggerFactory.getLogger(this.getClass());

    abstract void beforeExec();

    @Override
    protected void doExecute(JobExecution execution) throws JobInterruptedException, JobRestartException, StartLimitExceededException {
    
    
        beforeExec();
        log.info(" do execute before");
        super.doExecute(execution);
        log.info(" do execute after");
        afterExec();
    }

    abstract void afterExec();
}

我们创建了一个抽象的Job,这个抽象的Job继承于SimpleJob,SimpleJob是一个空实现AbstractJob的子类。

在抽象Job中,我们定义了前置和后置操作。在前置后置之间执行真正的操作。

因为AbsSimpleJob是一个抽象类,是无法实例化为bean实例,然后注入到容器中的,所以,需要创建子类,子类实现了AbsSimpleJob,并且,注入到容器中。

第一个子类,PlaySimpleJob

public class PlaySimpleJob extends AbsSimpleJob {
    
    
    @Override
    void beforeExec() {
    
    
        log.info(" play simple before ");
    }

    @Override
    void afterExec() {
    
    
        log.info(" play simple after ");
    }
}

PlaySimpleJob子类,实现了父类定义的前置后置操作。

前置后置操作也是非常的简单,只是打印一句话。

接着,我们创建第二个子类SleepSimpleJob

public class SleepSimpleJob extends AbsSimpleJob {
    
    
    @Override
    void beforeExec() {
    
    
        log.info(" sleep simple before ");
    }

    @Override
    void afterExec() {
    
    
        log.info(" sleep simple after ");
    }
}

SleepSimpleJob中的操作和PlaySimpleJob中的操作相同,都是打印日志。

目前为止,我们创建了1个抽象类,2个抽象类的子类。

那么,抽象的job和实现的job如何使用呢?

我们创建job配置类

@Configuration
@EnableBatchProcessing
public class AbsJobConf {
    
    

    @Bean
    public String runAbsJob(PlaySimpleJob playSimpleJob, SleepSimpleJob sleepSimpleJob, JobLauncher jobLauncher) throws JobParametersInvalidException, JobExecutionAlreadyRunningException, JobRestartException, JobInstanceAlreadyCompleteException {
    
    
        jobLauncher.run(playSimpleJob, new JobParametersBuilder().addLong("id", 2L).toJobParameters());
        jobLauncher.run(sleepSimpleJob, new JobParametersBuilder().addLong("id", 2L).toJobParameters());
        return "";
    }

    @Bean
    public PlaySimpleJob playSimpleJob(JobRepository jobRepository, Step absStep) {
    
    
        PlaySimpleJob playSimpleJob = new PlaySimpleJob();
        playSimpleJob.setName("study4-play-simple-job");
        playSimpleJob.setJobRepository(jobRepository);
        playSimpleJob.addStep(absStep);
        return playSimpleJob;
    }

    @Bean
    public SleepSimpleJob sleepSimpleJob(JobRepository jobRepository, Step absStep) {
    
    
        SleepSimpleJob sleepSimpleJob = new SleepSimpleJob();
        sleepSimpleJob.setName("study4-sleep-simpleo-job");
        sleepSimpleJob.setJobRepository(jobRepository);
        sleepSimpleJob.addStep(absStep);
        return sleepSimpleJob;
    }

    @Bean
    public Step absStep(StepBuilderFactory stepBuilderFactory) {
    
    
        return stepBuilderFactory.get("study4-abs-step")
                .tasklet(new Tasklet() {
    
    
                    @Override
                    public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
    
    
                        System.out.println("exec ! " + LocalTime.now());
                        return RepeatStatus.FINISHED;
                    }
                }).build();

    }

}

我们创建了一个step,然后使用注解,将实现了AbsSimpleJob的子类,注入到容器中。需要注意装配Job。如果Job在内部自己已经实现装配,那么我们在注入到容器中时,只需要注入JobRepository属性即可。

最后调度。

image-20201111162706365

继承Job

在抽象Job中就知道,Job就是一个Bean,那么就可以实现继承。

比如我们新增PlayGameSimpleJob继承PlaySimpleJob

public class PlayGameSimpleJob extends PlaySimpleJob {
    
    

    public PlayGameSimpleJob() {
    
    
        this("play-game-simple-job");
    }

    public PlayGameSimpleJob(String name) {
    
    
        setName(name);
    }

}

然后调度即可

@EnableBatchProcessing
@Configuration
public class ExJobConf {
    
    

    @Bean
    public String runExJob(JobLauncher jobLauncher, Job playGameSimpleJob) throws JobParametersInvalidException, JobExecutionAlreadyRunningException, JobRestartException, JobInstanceAlreadyCompleteException {
    
    
        jobLauncher.run(playGameSimpleJob, new JobParametersBuilder().addDate("date", new Date()).toJobParameters());
        return "";
    }

    @Bean
    public PlayGameSimpleJob playGameSimpleJob(JobRepository jobRepository, Step playGameStep) {
    
    
        PlayGameSimpleJob playGameSimpleJob = new PlayGameSimpleJob("study4-play-game-job");
        playGameSimpleJob.setJobRepository(jobRepository);
        playGameSimpleJob.setSteps(Arrays.asList(playGameStep));
        return playGameSimpleJob;
    }

    @Bean
    public Step playGameStep(StepBuilderFactory stepBuilderFactory) {
    
    
        return stepBuilderFactory.get("study4-play-game-step")
                .tasklet(new Tasklet() {
    
    
                    @Override
                    public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
    
    
                        System.out.println(" play game step ");
                        return RepeatStatus.FINISHED;
                    }
                }).build();
    }
}

启动

image-20201111163957358

作用域绑定

作用域

Scope用来声明Ioc容器中对象的存活空间,即在Ioc容器在对象进入相应的Scope之前,生成并装配这些对象,在该对象不再处于这些Scope的限定范围之后,容器通常会销毁这些对象。

StepScope是spring batch框架提供的自定义的Scope,将spring bean定义为StepScope,支持spring bean在Step开始的时候初始化,在Step结束的时候销毁spring bean,将spring bean的生命周期与Step绑定。

JobScope是spring batch框架提供的自定义的Scope,将spring bean定义为JobScope,支持spring bean在Job开始的时候初始化,在Job结束的时候销毁spring bean,将spring bean的生命周期与Job绑定。

参数绑定–LateBinding

在之前配置job的时候,job的名字,step的名字,以及一些参数等等,都是直接写死的。

这样非常不利于扩展,而且也不符合实际,很多东西,是只有在运行的时候,才知道的。

spring batch可以通过属性后绑定的技术,支持在运行期间获取属性的值。

spring batch框架通过特定的表达式支持为Job或者Step关联的实体使用后绑定技术。在StepScope和JobScope中spring batch框架提供的可以使用的实体包括jobParameters,jobExecutionContext,stepExecutionContext.

举例

@EnableBatchProcessing
@Configuration
public class ScoJobConf {
    
    

    private AtomicInteger atomicInteger = new AtomicInteger();

    @Bean
    public String runJob(JobLauncher jobLauncher, Job job) throws JobParametersInvalidException, JobExecutionAlreadyRunningException, JobRestartException, JobInstanceAlreadyCompleteException {
    
    
        jobLauncher.run(job, new JobParametersBuilder()
                .addString("stepName", "study4-sco-step")
                .addString("name", "   sco   ")
                .addLong("time", 10L)
                .addString("writeMessage", " write msg ")
                .addString("processMessage", " process msg")
                .addDate("date", new Date())
                .toJobParameters());
        return "";
    }


    @Bean
    public Job job(JobBuilderFactory jobBuilderFactory, Step step) {
    
    
        return jobBuilderFactory.get("study4-sco-job")
                .start(step)
                .validator(new DefaultJobParametersValidator(new String[]{
    
    "stepName", "name", "time"}, new String[]{
    
    "writeMessage", "processMessage", "date"}))
                .build();
    }

    @Bean
    @JobScope
    public Step step(ItemReader<String> reader, ItemProcessor<String, String> processor, ItemWriter<String> writer, StepBuilderFactory stepBuilderFactory, @Value("#{jobParameters['stepName']}") String stepName) {
    
    
        return stepBuilderFactory.get(stepName)
                .<String, String>chunk(10)
                .reader(reader)
                .processor(processor)
                .writer(writer)
                .build();
    }

    @Bean
    @StepScope
    public ItemProcessor<String, String> processor(@Value("#{jobParameters['processMessage']}") String processMessage) {
    
    
        return new ItemProcessor<String, String>() {
    
    
            @Override
            public String process(String item) throws Exception {
    
    
                System.out.println(processMessage + item);
                return item;
            }
        };
    }

    @Bean
    @StepScope
    public ItemWriter<String> writer(@Value("#{jobParameters['writeMessage']}") String writeMessage) {
    
    
        return new ItemWriter<String>() {
    
    
            @Override
            public void write(List<? extends String> items) throws Exception {
    
    
                items.stream().forEach(x -> System.out.println(writeMessage + x));
            }
        };
    }

    @Bean
    @StepScope
    public ItemReader<String> reader(@Value("#{jobParameters['name']}") String name, @Value("#{jobParameters['time']}") int time) {
    
    
        ItemReader reader = new ItemReader<String>() {
    
    
            @Override
            public String read() throws Exception, UnexpectedInputException, ParseException, NonTransientResourceException {
    
    
                if (atomicInteger.incrementAndGet() > 20) {
    
    
                    return null;
                }
                return name;
            }
        };
        return reader;
    }

}

运行

image-20201112193329361

数据库记录

image-20201112193357874

Job运行

spring batch框架提供一组执行job的接口。包括JobLauncher,JobExplorer和JobOperator三个操作Job的接口。

image-20201112193639458

JobLauncher是最常用的作业调度器,通过给定的Job Name和Job Parameters可以执行Job;JobExplorer主要负责从JobRepository中获取执行的信息,包括获取作业实例、获取作业执行器、获取正在运行的作业执行器,获取作业列表等操作;JobOperator包含了JobLauncher和JobExplorer中的大部分操作。

调度作业

配置好的Job需要调度才能运行,一般可以通过外部框架,结合使用,调度Job运行。

当然,也可以使用JobLauncher调度Job

    @Bean
    public String runJob(JobLauncher jobLauncher, Job job) throws JobParametersInvalidException, JobExecutionAlreadyRunningException, JobRestartException, JobInstanceAlreadyCompleteException {
    
    
        jobLauncher.run(job, new JobParametersBuilder()
                .addString("stepName", "study4-sco-step")
                .addString("name", "   sco   ")
                .addLong("time", 10L)
                .addString("writeMessage", " write msg ")
                .addString("processMessage", " process msg")
                .addDate("date", new Date())
                .toJobParameters());
        return "";
    }

同步异步

默认情况下,JobLauncher的run操作通过同步方式调用Job,任何调用Job的客户端需要等待Job的执行结果返回后才能结束。

image-20201112195010781

比如

@EnableBatchProcessing
@Configuration
public class LauJobConf {
    
    

    @Bean
    public String runLauJob(JobLauncher jobLauncher,Job lauJob) throws JobParametersInvalidException, JobExecutionAlreadyRunningException, JobRestartException, JobInstanceAlreadyCompleteException {
    
    
        System.out.println("start " + LocalDateTime.now());
        jobLauncher.run(lauJob, new JobParametersBuilder().addDate("date", new Date()).toJobParameters());
        System.out.println("over " + LocalDateTime.now());
        return "";
    }



    @Bean
    public Job lauJob(JobBuilderFactory jobBuilderFactory, StepBuilderFactory stepBuilderFactory) {
    
    
        return jobBuilderFactory.get("study4-lau-job")
                .start(stepBuilderFactory.get("study4-lau-step")
                .tasklet(new Tasklet() {
    
    
                    @Override
                    public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
    
    
                        TimeUnit.SECONDS.sleep(10);
                        return RepeatStatus.FINISHED;
                    }
                }).build())
                .build();
    }

}

这就是同步执行,任务中的线程睡眠,调度操作也会被阻塞

image-20201112200426332

image-20201112200536996

同步操作的优势在于作业一旦执行完毕,调用客户端能够立刻收到返回值。但在实际的使用中,往往Job的执行所需时间太长,不能一直等下去。

所以,为了提高资源利用率,需要使用异步的方式调用Job。

JobLauncher提供了异步执行Job的能力。

比如

@EnableBatchProcessing
@Configuration
public class SyncJobConf {
    
    

    @Bean
    public String runLauJob(JobLauncher jobLauncher, Job lauJob) throws JobParametersInvalidException, JobExecutionAlreadyRunningException, JobRestartException, JobInstanceAlreadyCompleteException {
    
    
        System.out.println("start " + LocalDateTime.now());
        jobLauncher.run(lauJob, new JobParametersBuilder().addDate("date", new Date()).toJobParameters());
        System.out.println("over " + LocalDateTime.now());
        return "";
    }

    @Bean
    public JobLauncher jobLauncher(JobRepository jobRepository) {
    
    
        SimpleJobLauncher jobLauncher = new SimpleJobLauncher();
        jobLauncher.setJobRepository(jobRepository);
        jobLauncher.setTaskExecutor(new ConcurrentTaskExecutor());
        return jobLauncher;
    }

    @Bean
    public Job lauJob(JobBuilderFactory jobBuilderFactory, StepBuilderFactory stepBuilderFactory) {
    
    
        return jobBuilderFactory.get("study4-lau-job")
                .start(stepBuilderFactory.get("study4-lau-step")
                        .tasklet(new Tasklet() {
    
    
                            @Override
                            public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
    
    
                                TimeUnit.SECONDS.sleep(10);
                                return RepeatStatus.FINISHED;
                            }
                        }).build())
                .build();
    }

}

执行结果

image-20201112203419237

定时任务执行

spring 中也有一个轻量级的调度器,所以,我们可以借助spring中的调度,实现任务的执行。

首先开启调度

image-20201114130927310

接着创建调度执行的目标job

@EnableBatchProcessing
@Configuration
@Component
public class SchJobConf {
    
    

    @Autowired
    private JobLauncher jobLauncher;

    @Autowired
    @Qualifier("schJob")
    private Job schJob;

    @Scheduled(cron = "* * * * * *")
    @Autowired
    public void runJob() throws JobParametersInvalidException, JobExecutionAlreadyRunningException, JobRestartException, JobInstanceAlreadyCompleteException {
    
    
        jobLauncher.run(schJob, new JobParametersBuilder().addDate("date", new Date()).toJobParameters());
    }

    @Bean
    public JobLauncher jobLauncher(JobRepository jobRepository) {
    
    
        SimpleJobLauncher jobLauncher = new SimpleJobLauncher();
        jobLauncher.setJobRepository(jobRepository);
        ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler();
        threadPoolTaskScheduler.setPoolSize(3);
        threadPoolTaskScheduler.initialize();
        jobLauncher.setTaskExecutor(threadPoolTaskScheduler);
        return jobLauncher;
    }

    @Bean
    public Job schJob(JobBuilderFactory jobBuilderFactory, StepBuilderFactory stepBuilderFactory){
    
    
        return jobBuilderFactory.get("study4-sch-job")
                .start(stepBuilderFactory.get("study4-sch-step")
                .tasklet(new Tasklet() {
    
    
                    @Override
                    public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
    
    
                        System.out.println("study-sch-execute");
                        return RepeatStatus.FINISHED;
                    }
                }).build())
                .build();
    }

}

在这里我们主要做了三个操作:

  1. 通过注解,向spring容器中注入了我们的目标操作schJob。
  2. 因为调度是异步执行,所以,执行器需要初始化线程池,所以我们给jobLauncher初始化了线程池
  3. 拥有@Scheduled注解的调度方法,需要注意,调度方法不能有参数,所以,将调度方法中用到的执行器和任务,全部以属性的方式注入。

接着启动微服务,就会每一秒中执行一次任务:

image-20201114131301537

Web接口启动任务

通过上述几种方式,我们可以看出,不管是同步还是异步,不管是定时任务,还是命令行,还是Web应用,其实际上都是调用JobLauncher去启动任务。

所以,Web接口启动的原因也非常的简单,通过暴露Controller接口,在Controller接口中,调用jobLauncher启动任务。

Job 终止

在前面体验异步、定时任务、Web接口启动任务的代码中,启动之后就无法停止了,只能通过关闭JVM的方式,来停止Job的调度。

但是,关闭JVM的方式来终止Job,终归不是一个好的退出方式。

所以,spring batch提供了停止正在运行Job的能力,通过接口JobOperator中的stop方法实现。

代码中终止

首先创建一个执行步的操作

    class StopTasklet implements Tasklet {
    
    

        @Override
        public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
    
    
            int sum = 10;
            while (sum > 0) {
    
    
                TimeUnit.SECONDS.sleep(1);
                System.out.println("stop step " + chunkContext.getStepContext().getStepName() + " exec " + LocalDateTime.now());
                sum--;
            }
            return RepeatStatus.FINISHED;
        }
    }

我们的执行步中的操作,就是每隔1秒,打印执行步的名字和时间。

接着创建三个执行步

    @Bean
    public Step stopStep1(StepBuilderFactory stepBuilderFactory) {
    
    
        return stepBuilderFactory.get("study4-stop-step-1")
                .tasklet(new StopTasklet()).build();
    }

    @Bean
    public Step stopStep2(StepBuilderFactory stepBuilderFactory) {
    
    
        return stepBuilderFactory.get("study4-stop-step-2")
                .tasklet(new StopTasklet()).build();
    }

    @Bean
    public Step stopStep3(StepBuilderFactory stepBuilderFactory) {
    
    
        return stepBuilderFactory.get("study4-stop-step-3")
                .tasklet(new StopTasklet()).build();
    }

接着创建job

    @Bean
    public Job stopJob(JobBuilderFactory jobBuilderFactory, Step stopStep1, Step stopStep2, Step stopStep3) {
    
    
        return jobBuilderFactory.get("study4-stop-job")
                .start(stopStep1)
                .next(stopStep2)
                .next(stopStep3)
                .build();
    }

因为是异步启动,所以需要给JobLauncher注入线程池

    @Bean
    public JobLauncher jobLauncher(JobRepository jobRepository) {
    
    
        SimpleJobLauncher jobLauncher = new SimpleJobLauncher();
        jobLauncher.setJobRepository(jobRepository);
        jobLauncher.setTaskExecutor(new ConcurrentTaskExecutor());
        return jobLauncher;
    }

接着调度执行job

    @Bean
    public String runJob(JobLauncher jobLauncher, Job stopJob, JobOperator jobOperator) throws JobParametersInvalidException, JobExecutionAlreadyRunningException, JobRestartException, JobInstanceAlreadyCompleteException, InterruptedException, NoSuchJobException, NoSuchJobExecutionException, JobExecutionNotRunningException {
    
    
        jobLauncher.run(stopJob, new JobParametersBuilder().addDate("date", new Date()).toJobParameters());
        TimeUnit.SECONDS.sleep(15);
        jobOperator.stop(jobOperator.getRunningExecutions(stopJob.getName()).iterator().next());
        return "";
    }

在执行的方法中,我们注入了JobOperator接口。

在job中,我们有三个执行步,每个执行步10秒,每个1秒会打印执行步的名字和时间。

在调度中,我们主线程调度成功了job后,会暂时睡眠15秒,此时执行步正好执行在第二个执行步。

因为job终止的最小单位就是执行步,如果我们停止的时候,job正好执行在执行步中,那么是不会立刻终止的,而是等当前执行步执行完成后,终止。

所以,在第15秒的时候,终止job,job会将第二个执行部执行完成,然后终止job。

也就是预期结果中,第三个执行步不会执行。

image-20201114141543826

image-20201114141556465

第三个执行步没有执行,在数据库中查询job执行结果,也是终止

image-20201114141655638

JMX终止

我们创建一个和代码中终止的相同的job

    @Bean
    public Job stopJob(JobBuilderFactory jobBuilderFactory, Step stopStep1, Step stopStep2, Step stopStep3) {
    
    
        return jobBuilderFactory.get("study4-jmx-job")
                .start(stopStep1)
                .next(stopStep2)
                .next(stopStep3)
                .build();
    }


    @Bean
    public Step stopStep1(StepBuilderFactory stepBuilderFactory) {
    
    
        return stepBuilderFactory.get("study4-jmx-step-1")
                .tasklet(new StopTasklet()).build();
    }

    @Bean
    public Step stopStep2(StepBuilderFactory stepBuilderFactory) {
    
    
        return stepBuilderFactory.get("study4-jmx-step-2")
                .tasklet(new StopTasklet()).build();
    }

    @Bean
    public Step stopStep3(StepBuilderFactory stepBuilderFactory) {
    
    
        return stepBuilderFactory.get("study4-jmx-step-3")
                .tasklet(new StopTasklet()).build();
    }

    class StopTasklet implements Tasklet {
    
    

        @Override
        public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
    
    
            int sum = 30;
            while (sum > 0) {
    
    
                TimeUnit.SECONDS.sleep(1);
                System.out.println("stop step " + chunkContext.getStepContext().getStepName() + " exec " + LocalDateTime.now());
                sum--;
            }
            return RepeatStatus.FINISHED;
        }
    }

使用异步启动

    @Bean
    public String runJob(JobLauncher jobLauncher, Job stopJob) throws JobParametersInvalidException, JobExecutionAlreadyRunningException, JobRestartException, JobInstanceAlreadyCompleteException {
    
    
        jobLauncher.run(stopJob, new JobParametersBuilder().addDate("date", new Date()).toJobParameters());
        return "";
    }

    @Bean
    public JobLauncher jobLauncher(JobRepository jobRepository) {
    
    
        SimpleJobLauncher jobLauncher = new SimpleJobLauncher();
        jobLauncher.setJobRepository(jobRepository);
        jobLauncher.setTaskExecutor(new ConcurrentTaskExecutor());
        return jobLauncher;
    }

然后将JobOperator暴露为jmx操作

    @Bean
    public MBeanExporter mBeanExporter(JobOperator jobOperator) {
    
    
        MBeanExporter exporter = new MBeanExporter();
        exporter.setBeans(Map.of("com.study.study4.job:name=jobOperator", jobOperator));
        return exporter;
    }

接着启动

image-20201114150410786

然后在控制台启动jconsole

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-faJK66J7-1605339002105)(C:\Users\star10008377001\AppData\Roaming\Typora\typora-user-images\image-20201114150500044.png)]image-20201114153932286

并连接我们的应用

image-20201114150514267

接着在数据库中找到我们的job的execution

image-20201114150544528

等待程序执行到第30~60秒期间

调用jobOperatoer的stop操作,传入job_execution_id

image-20201114150638727

此时程序会将第30~60秒的stopStep2执行完毕,但是不会执行stopStep3

image-20201114150730297

image-20201114150737608

此时在查询数据库

image-20201114150758617

job_execution就是stop状态了。

业务终止

当业务代码出现异常时,通过业务判断,job不能再继续执行下去(越执行,错误越多)

此时业务代码通过调用StepExecution.setTerminateOnly(),发送一个停止消息给框架,一旦spring batch框架接收到停止消息,并且框架获取到作业的控制权,spring batch就会终止job.

我们继续以代码中终止的job为例

@EnableBatchProcessing
@Configuration
public class BusJobConf {
    
    

    @Bean
    public String runJob(JobLauncher jobLauncher, Job stopJob) throws JobParametersInvalidException, JobExecutionAlreadyRunningException, JobRestartException, JobInstanceAlreadyCompleteException {
    
    
        jobLauncher.run(stopJob, new JobParametersBuilder().addDate("date", new Date()).toJobParameters());
        return "";
    }

    @Bean
    public JobLauncher jobLauncher(JobRepository jobRepository) {
    
    
        SimpleJobLauncher jobLauncher = new SimpleJobLauncher();
        jobLauncher.setJobRepository(jobRepository);
        jobLauncher.setTaskExecutor(new ConcurrentTaskExecutor());
        return jobLauncher;
    }

    @Bean
    public Job stopJob(JobBuilderFactory jobBuilderFactory, Step stopStep1, Step stopStep2, Step stopStep3) {
    
    
        return jobBuilderFactory.get("study4-jmx-job")
                .start(stopStep1)
                .next(stopStep2)
                .next(stopStep3)
                .build();
    }


    @Bean
    public Step stopStep1(StepBuilderFactory stepBuilderFactory) {
    
    
        return stepBuilderFactory.get("study4-jmx-step-1")
                .tasklet(new StopTasklet())
                .build();
    }

    @Bean
    public Step stopStep2(StepBuilderFactory stepBuilderFactory) {
    
    
        return stepBuilderFactory.get("study4-jmx-step-2")
                .tasklet(new StopTasklet())
                .build();
    }

    @Bean
    public Step stopStep3(StepBuilderFactory stepBuilderFactory) {
    
    
        return stepBuilderFactory.get("study4-jmx-step-3")
                .tasklet(new StopTasklet())
                .build();
    }

    class StopTasklet implements Tasklet {
    
    

        @Override
        public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
    
    
            int sum = 30;
            if (chunkContext.getStepContext().getStepName().equals("study4-jmx-step-2")) {
    
    
                chunkContext.getStepContext().getStepExecution().setTerminateOnly();
            }
            while (sum > 0) {
    
    
                TimeUnit.SECONDS.sleep(1);
                System.out.println("stop step " + chunkContext.getStepContext().getStepName() + " exec " + LocalDateTime.now());
                sum--;
            }
            return RepeatStatus.FINISHED;
        }
    }

}

但是做了一个小改动,在执行步的操作中,业务代码中,判断当前如果是第二个执行步,那么调用停止操作,发送停止消息给spring batch框架。

需要记住的是,spring batch调度的单位是执行步,而且需要框架得到作业的控制权。

我们在第二个执行步中发送消息,但是第二个执行步已经进入,所以,需要等第二个执行步执行完成,框架得到控制权,就会终止job,不会执行第三个执行步。

启动

image-20201114152832209

在这里插入图片描述

第三个执行步不会执行,而且日志中也显示,得到了停止消息。

我们可以在异常拦截器中,发送停止消息,从而终止job.

猜你喜欢

转载自blog.csdn.net/a18792721831/article/details/109691602