学习spring-batch(二)----------使用

本文通过一个综合的demo介绍了Spring-Batch的使用,包括基于tasklet和chunk的批处理方式。Tasklet方式中详细展示了如何定义作业和步骤,以及作业监听器和步骤监听器的使用。chunk方式则涉及读取器、处理器和写入器,用于文件和数据库的批处理操作。文章还提到了条件分支、作业步骤的执行状态和自定义状态决策。
摘要由CSDN通过智能技术生成

前一篇文章已经初步介绍了一下spring-batch的作用和使用场景,以及初步了解了一下怎么使用的,接下来就通过一个综合的demo来详细介绍一下spring-batch的用法,分为了两部分tasklet的方式和chunk的方式

前文连接

(206条消息) 学习Spring-batch(一)-------入门_t梧桐树t的博客-CSDN博客

Demo源码

文件下载-奶牛快传 Download |CowTransfer

Demo演示

基于tasktel的批处理

@Configuration
@Slf4j
public class TaskletJobConfig {

    private final JobBuilderFactory jobBuilderFactory;

    private final StepBuilderFactory stepBuilderFactory;

    public TaskletJobConfig(JobBuilderFactory jobBuilderFactory, StepBuilderFactory stepBuilderFactory) {
        this.jobBuilderFactory = jobBuilderFactory;
        this.stepBuilderFactory = stepBuilderFactory;
    }

    @Bean
    public Job taskletJob() {
        return jobBuilderFactory.get("taskletJob")
//                .preventRestart() //表示如果发生异常禁止重启
                .start(step1())     //步骤一
                .next(step2())      //步骤二
                .incrementer(new RunIdIncrementer())  //作业执行id
                .listener(jobExecutionListener())       //作业监听器
                .build();
    }

    /**
     * 步骤一
     */
    @Bean
    public Step step1() {
        return stepBuilderFactory.get("taskletStep1")
                .tasklet(new Tasklet() {
                    //执行内容
                    @Override
                    public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
                        log.info("=========================执行步骤一====================================");
                        //作业参数
                        Map<String, Object> jobParameters = chunkContext.getStepContext().getJobParameters();
                        Object name = jobParameters.get("name");
                        log.info("=================作业参数name[{}]======================", name);

                        //作业执行上下文
                        ExecutionContext jobExecutionContext = chunkContext.getStepContext().getStepExecution().getJobExecution().getExecutionContext();
                        jobExecutionContext.put("taskletJob", "这是作业执行上下文内容!");

                        //步骤执行上下文
                        ExecutionContext stepExecutionContext = chunkContext.getStepContext().getStepExecution().getExecutionContext();
                        stepExecutionContext.put("taskletStep", "这是步骤执行上下文内容!");
                        //返回执行结果状态 FINISHED 表示执行完毕,如果返回 CONTINUABLE 步骤会重发执行
                        return RepeatStatus.FINISHED;
                    }
                })
                .listener(stepExecutionListener())      //步骤监听器
//                .startLimit(2)            //如果发生异常可以重启一次,注意首次启动也算其中一次
//                .allowStartIfComplete(true)       //如果发生异常后可以无限进行重启
                .build();
    }

    /**
     * 步骤二
     */
    @Bean
    public Step step2() {
        return stepBuilderFactory.get("taskletStep2")
                .tasklet(new Tasklet() {
                    @Override
                    public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
                        log.info("=========================执行步骤二====================================");
                        //作业执行上下文,此种方式只可读不可写
                        Map<String, Object> jobExecutionContext = chunkContext.getStepContext().getJobExecutionContext();
                        Object taskletJob = jobExecutionContext.get("taskletJob");
                        log.info("=======================作业执行上下文taskletJob[{}]========================",taskletJob);

                        //步骤执行上下文,此种方式只可读不可写
                        Map<String, Object> stepExecutionContext = chunkContext.getStepContext().getStepExecutionContext();
                        Object taskletStep = stepExecutionContext.get("taskletStep");
                        log.info("=======================作业执行上下文taskletStep[{}]========================",taskletStep);

                        return RepeatStatus.FINISHED;
                    }
                })
                .listener(stepExecutionListener())      //步骤监听器
                .build();
    }

    /**
     * 作业监听器
     */
    @Bean
    public TaskletJobListener jobExecutionListener() {
        return new TaskletJobListener();
    }

    /**
     * 步骤监听器
     */
    @Bean
    public TaskletStepListener stepExecutionListener() {
        return new TaskletStepListener();
    }
}

下面一步一步来介绍每个方法的作用

taskletJob

此方法是用来定义一个作业的,每一个不同的作业就需要有一个job方法,但需要注意不要出现相名称的方法,否则启动时会报错

get     设置JobName

preventRestart  表示如果发生异常禁止重启

start   执行步骤一

next   执行步骤二,如果有步骤三则next(step3),以此类推

incrementer   作业执行参数,spring-batch在执行作业的时候会根据执行参数(后面会提到)jobName和运行id来判断是否已经执行过当前作业.如果已经执行过则不会再执行,,所以需要传个一个RunIdIncrementer参数,此处也可以通过实现JobParametersValidator接口来自定义执行参数

listener     设置作业监听器

step1

该方法是执行步骤的定义

tasklet            具体执行内容的定义,由于Tasklet是一个接口,所以这里以匿名内部来实现,其中使用较多的ChunkContext chunkContext这个参数,其中可以获取到作业执行上下文和步骤执行上下文还有作业执行参数等,具体获取方法请看代码中,    注意:作业执行上下文在整个作业的执行内都可以获取,也就是说作业执行上下文内内容,在每一个步骤都可以访问,而步骤执行上下文只能在当前步骤中访问,其他的步骤访问不到

listener                       设置步骤监听器

startLimit(2)                如果发生异常可以重启1次,注意首次启动也算其中1次

allowStartIfComplete(true)         如果发生异常后可以无限进行重启

step2

与step1同理该方法是执行步骤的定义

注意:此方法中获取 作业执行上下文  与  步骤执行上下文  的方式获取到的map,只可读,不可以写,如果使用了put方法会报错

jobExecutionListener

该方法是作业监听器,可通过实现 JobExecutionListener 接口自定义监听器

@Slf4j
public class TaskletJobListener implements JobExecutionListener {
    @Override
    public void beforeJob(JobExecution jobExecution) {
        String exitCode = jobExecution.getExitStatus().getExitCode();
        log.error("===========执行TaskletJobListener 前,当前状态[{}]=================", exitCode);
    }

    @Override
    public void afterJob(JobExecution jobExecution) {
        String exitCode = jobExecution.getExitStatus().getExitCode();
        Object taskletJob = jobExecution.getExecutionContext().get("taskletJob");
        log.error("===========执行TaskletJobListener 后,当前状态[{}]=================作业参数[{}]", exitCode, taskletJob);
    }
}

这里可以获取作业执行上下文

stepExecutionListener

该方法是步骤监听器,可通过实现 StepExecutionListener 接口自定义监听器

@Slf4j
public class TaskletStepListener implements StepExecutionListener {
    @Override
    public void beforeStep(StepExecution stepExecution) {
        //作业执行上下文
        ExecutionContext jobExecutionContext = stepExecution.getJobExecution().getExecutionContext();
        //步骤执行上下文
        ExecutionContext stepExecutionContext = stepExecution.getExecutionContext();
        log.warn("=================执行TaskletStepListener 前==========================");
    }

    @Override
    public ExitStatus afterStep(StepExecution stepExecution) {
        log.warn("=================执行TaskletStepListener 后==========================");
        return ExitStatus.COMPLETED;
    }
}

   通过stepExecution可以获取作业执行上下文和步骤执行上下文

afterStep方法里有一个返回值,通过这个返回值我们可以改变步骤的执行结果,(具体有何作用下面会介绍)

执行作业

private final JobLauncher jobLauncher;

private final JobExplorer jobExplorer;

		 /**
     * 基于tasklet的简单操作
     */
		public void taskletDemo() {
        //配置作业参数
        JobParameters parameters = new JobParametersBuilder(new JobParameters(), jobExplorer)
                .addString("name","geigei")
                .getNextJobParameters(taskletJob).toJobParameters();
        try {
            //启动作业
            jobLauncher.run(taskletJob, parameters);
        } catch (Exception e) {
            log.error("基于tasklet的简单操作异常[{}]", e.getMessage(), e);
            throw new BusinessException("基于tasklet的简单操作异常!");
        }
    }

这里也就是上面提到的作业参数, addString这个方法是作业参数的设置,它会保存在作业之行上下文中,可以通过外部传入的方式来设置测试的参数,此处不止支持addString,

条件分支

以上的内容大概就是spring-batch的一个简单场景,但是一般我们面对的不是这么简单的逻辑,所以这里要引入一个概念---------条件分支

条件分支可以理解成java里的 if-else 判断,也就是说由步骤一的执行结果来判断接下来要执行那个步骤,比如说,(以下是为了方便理解写的伪代码)

public Job job() {
        return jobBuilderFactory.get("job")
                .start(step1())
                .on(ExitStatus.FAILED.getExitCode())    //如果步骤一执行返回结果为FAILED,表示失败
                .to(step3())    //失败后就执行步骤三
                .from(step1()).on(ExitStatus.COMPLETED.getExitCode())   //如果步骤一执行返回结果为COMPLETED成功了,表示成功了
                .to(step2())        //成功后就执行步骤二
                .end()				//表示执行结束,个人理解相当于java里的break;
                .build();
    }


    public Step step1() {
        //执行步骤一
    }

    public Step step2() {
        //执行步骤二,表示步骤一执行成功了,继续正常逻辑
    }

    public Step step3() {
        //执行步骤三,表示步骤一执行失败了.执行失败后的逻辑
    }

几个注意点:

1> on 方法表示条件, 上一个步骤返回值,匹配指定的字符串,满足后执行后续 to 步骤

2> * 为通配符,表示能匹配任意返回值

3> from 表示从某个步骤开始进行条件判断

4> 分支判断结束,流程以end方法结束,表示if/else逻辑结束

5> on 方法中字符串取值于 ExitStatus 类常量,当然也可以自定义。

作业步骤的执行状态

public class ExitStatus implements Serializable, Comparable<ExitStatus> {

    //未知状态
	public static final ExitStatus UNKNOWN = new ExitStatus("UNKNOWN");

    //执行中
	public static final ExitStatus EXECUTING = new ExitStatus("EXECUTING");

    //执行完成
	public static final ExitStatus COMPLETED = new ExitStatus("COMPLETED");

    //无效执行
	public static final ExitStatus NOOP = new ExitStatus("NOOP");

    //执行失败
	public static final ExitStatus FAILED = new ExitStatus("FAILED");

    //执行中断
	public static final ExitStatus STOPPED = new ExitStatus("STOPPED");
    ...
} 

一般来说,作业启动之后,这些状态皆为流程自行控制。顺利结束返回:COMPLETED, 异常结束返回:FAILED,无效执行返回:NOOP, 这是肯定有小伙伴说,能不能编程控制呢?答案是可以的。

Spring Batch 提供 3个方法决定作业流程走向:

end():作业流程直接成功结束,返回状态为:COMPLETED

fail():作业流程直接失败结束,返回状态为:FAILED

stopAndRestart(step) :作业流程中断结束,返回状态:STOPPED   再次启动时,从step位置开始执行 (注意:前提是参数与Job Name一样)

还可以通过实现JobExecutionDecider接口的方式来自定义状态值,可以理解为把一些复杂且重复性的的判断逻辑抽到外面,例:

public class MyStatusDecider implements JobExecutionDecider {
    @Override
    public FlowExecutionStatus decide(JobExecution jobExecution, StepExecution stepExecution) {
        if(ret == 0){
            return new FlowExecutionStatus("A");
        }else if(ret == 1){
            return new FlowExecutionStatus("B");
        }else{
            return new FlowExecutionStatus("C");
        }
    }
}

基于chunk的批处理

在执行批处理测试之前需要先进行一下数据准备工作,需要先执行一下controller里的这个方法

首先看核心类

@Configuration
@SuppressWarnings("unchecked")
public class CsvToDBJobConfig {

    private final JobBuilderFactory jobBuilderFactory;

    private final StepBuilderFactory stepBuilderFactory;

    private final CsvServiceImpl csvService;


    @Value("${job.data.path}")
    private String path;

    public CsvToDBJobConfig(JobBuilderFactory jobBuilderFactory,
                            StepBuilderFactory stepBuilderFactory,
                            CsvServiceImpl csvService) {
        this.jobBuilderFactory = jobBuilderFactory;
        this.stepBuilderFactory = stepBuilderFactory;
        this.csvService = csvService;
    }

    /**
     * 作业
     */
    @Bean
    public Job csvToDBJob() {
        return jobBuilderFactory.get("csvToDBJob")
                .start(step())
                .incrementer(new RunIdIncrementer())
//                .listener(jobExecutionListener())
                .build();
    }

    /**
     * 步骤
     */
    @Bean
    public Step step() {
        return stepBuilderFactory.get("csvToDBStep")
                .<Employee, Employee>chunk(10000)  //意思是读取10000条数据后才会写一次,可以防止IO次数过多
                .reader(csvToDBItemReader())        //读操作
                .processor(csvToDBItemProcessor())  //数据处理操作
                .writer(csvToDBItemWriter())        //写操作
                .taskExecutor(new SimpleAsyncTaskExecutor())    //开启异步执行
                .build();
    }


    /**
     * 多线程读取文件
     */
    @Bean
    public FlatFileItemReader<Employee> csvToDBItemReader() {
        return new FlatFileItemReaderBuilder<Employee>()
                .name("employCsvToDB")
                .saveState(false)   //防止状态被覆盖,由于我们使用的是多线程方式,防止数据失败后导致多个线程发生重试,导致结果异常的情况,所以此处要设置设置为不可以重试
                .resource(new PathResource(new File(path, "employee.csv").getAbsolutePath()))
                .delimited()
                .names("id", "name", "age", "sex")
                .targetType(Employee.class)
                .build();
    }

    /**
     * 处理数据
     */
    @Bean
    public ItemProcessor<Employee, Employee> csvToDBItemProcessor() {
        return new ItemProcessor<Employee, Employee>() {
            @Override
            public Employee process(Employee employee) throws Exception {
                employee.setName(employee.getName() + "你干嘛,诶呀~~~");
                return employee;
            }
        };
    }

    /**
     * 批量写入文件
     */
    @Bean
    public ItemWriter<Employee> csvToDBItemWriter() {
        return new ItemWriter<Employee>() {
            @Override
            public void write(List<? extends Employee> list) throws Exception {
                //使用mybatis-plus批量插入数据库,此处的10000是为了和chunk保持一致,不一致理论上应该也可以
                csvService.saveBatch((List<Employee>)list,10000);
            }
        };
    }

    /**
     * job监听器
     */
//    @Bean
//    public CsvToDBJobListener jobExecutionListener() {
//        return new CsvToDBJobListener();
//    }
}

这里注意最下面把作业监听器注释掉了,因为前一个demo已经注入过一个Bean了,再次注入一个会报错,运行这个类的时候记得改一下,或者直接new 到Job里面

基于chunk的的批处理与tasklet最不同的就是多了三个方法,分别对应了读取器,处理器,写入器 主要是用来对文件和数据库进行的批处理操作

与前面相同的点不在重复,我们看一下不同的地方

csvToDBItemReader

先看放回值FlatFileItemReader,此类型是spring-batch提供的专门的读取文件的类,此处也可以通过实现ItemReader接口来自定义逻辑

name        定义读取器的名称

saveState(false)          防止状态被覆盖,由于我们使用的是多线程方式,防止数据失败后导致多个线程发生重试,导致结果异常的情况,所以此处要设置设置为不可以重试

resource        要读取得资源路径

delimited    定义分割符,可配合delimiter使用,该文件不需要分割符,所以不需要配置

names        映射的实体属性

targetType        实体类型

csvToDBItemProcessor

通过实现ItemProcessor的方式自定义处理器ItemProcessor<处理前,处理后>的两个泛型分别代表处理前的类型和处理后的类型

csvToDBItemWriter

通过实现ItemWriter的方式,自定义批量写如数据库操作,这里使用的是我们框架提供的批量插入数据库的方法,原理基于mybatis-plus,也可以使用mybatis-plus原生的,当然也可以使用mybatis的批处理操作,个人认为比较麻烦,所以不再介绍 

此处需要注意的点是返回值的类型是继承自我们真正要返回的类型,所以这里需要强转一下

step

基于chunk的step与说前面还是有所不同的,此处没有了tasklet的调用,换成了三个执行器的调用

<Employee, Employee>chunk(10000)      意思是读取10000条数据后才会写一次,可以防止IO次数过多,注意前面的泛型,不要忘记

reader                读操作    

processor            数据处理操作

writer             写操作

taskExecutor        开启异步执行,此处也可以选择不开启多线程处理,如果不开启多线程那么csvToDBItemReader方法中的saveState,就可以设置为true,允许失败后重试

结尾

整个Demo的介绍到这里就结束了,但并不是说spring-batch就这些内容,这只是spring-batch应用的一小部分场景和用法,如果项目中用到了该部分技术,请举一反三!!!

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值