SpringBatch从入门到精通-11 同步执行但提前返回结果【掘金日新计划】

246 阅读5分钟

\

持续创作,加速成长,6月更文活动来啦!| 掘金·日新计划

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第20天,点击查看活动详情

SpringBatch从入门到精通-1【掘金日新计划】

SpringBatch从入门到精通-2-StepScope作用域和用法【掘金日新计划】

SpringBatch从入门到精通-3-并行处理【掘金日新计划】

SpringBatch从入门到精通-3.2-并行处理-远程分区【掘金日新计划】

SpringBatch从入门到精通-3.3-并行处理-远程分区(消息聚合)【掘金日新计划】

SpringBatch从入门到精通-4 监控和指标【掘金日新计划】

SpringBatch从入门到精通-4.2 监控和指标-原理【掘金日新计划】

SpringBatch从入门到精通-5 数据源配置相关【掘金日新计划】

SpringBatch从入门到精通-5.2 数据源配置相关-原理【掘金日新计划】

SpringBatch从入门到精通-6 读和写处理【掘金日新计划】

SpringBatch从入门到精通-6.1 读和写处理-实战【掘金日新计划】

SpringBatch从入门到精通-6.2 读和写处理-实战2【掘金日新计划】

SpringBatch从入门到精通-6.3 读和写处理-实战3【掘金日新计划】

SpringBatch从入门到精通-6.4 读和写处理-实战4【掘金日新计划】

SpringBatch从入门到精通-7 常见的批处理的一些模式【掘金日新计划】

SpringBatch从入门到精通-8 数据处理【掘金日新计划】

SpringBatch从入门到精通-9 重复处理【掘金日新计划】

SpringBatch从入门到精通-10 重试处理【掘金日新计划】

背景

有个需求:领导需要job执行的时候,提前返回jobId相关信息。

目的:job执行的时候如果发生宕机事件,jobId还能记录在任务表中。

储备知识: 知道原生JobLanucher机制,原生如果不满足,知道如何扩展。

JobLauncher

public interface JobLauncher {
    /**
     为给定的{@link job}和{@link JobParameters}启动作业执行如果{@link JobExecution}能够成功创建,它将
无论执行成功。如果有过去的{@link JobExecution}已暂停,则返回相同的{@link JobExecution},而不是新的
已创建一个。只有在以下操作失败时才会引发异常开始作业。如果作业在处理过程中遇到错误,则
将返回JobExecution,并需要检查其状态
     */
    public JobExecution run(Job job, JobParameters jobParameters) throws JobExecutionAlreadyRunningException,
            JobRestartException, JobInstanceAlreadyCompleteException, JobParametersInvalidException;
​
}

解释: 给一个job和对应的jobParameters 将job运行起来。

该接口现在给了一个默认实现SimpleJobLauncher

image-20220625232102721

该接口可以使用的时候。可以直接用new的方式。也可以是用把他当作一个bean的形式。

因为在代码里已经做注入了。

如果使用new的方式需要将jobRepository 传进来。

默认如果不传入taskExecutor的时候会创建一个同步执行器。

@Override
public void afterPropertiesSet() throws Exception {
   Assert.state(jobRepository != null, "A JobRepository has not been set.");
   if (taskExecutor == null) {
      logger.info("No TaskExecutor has been set, defaulting to synchronous executor.");
      taskExecutor = new SyncTaskExecutor();
   }
}

所以还可以支持传入一个异步的执行器来异步执行,提前返回jobExecution。该提前量中包含了jobId,jobExeId等关键信息

JobExecution: id=131, version=1, startTime=Sat Jun 25 23:38:47 CST 2022, endTime=null, lastUpdated=Sat Jun 25 23:38:47 CST 2022, status=STARTED, exitStatus=exitCode=UNKNOWN;exitDescription=, job=[JobInstance: id=131, version=0, Job=[asyncJobLanucherJob]], jobParameters=[{date=1656171527032, rnd=1511d04b-70e1-4157-b447-b6ed51058ec7}]

可行性方案

现有方案不能直接又同步执行,又最大限度的减少数据库访问来达到提前知道结果。

方案1: 同步JobLauncher执行器+ 异步线程查询job执行id情况

方案2: 异步JobLauncher执行器(主动传入taskExecutor)+ 同步主线程轮询查询数据库情况。

方案3: 自定义同步JobLauncher执行器+CompletableFuture allOf 执行结果

方案4: 自定义同步JobLauncher执行器+同步调用接口(推荐方案)

对比之下:方案1和方案2 都不是很好。方案3 :代码如下

public class TaskAsyncJobLauncher  implements JobLauncher, InitializingBean {
​
    protected static final Log logger = LogFactory.getLog(SimpleJobLauncher.class);
​
    private JobRepository jobRepository;
​
    private TaskExecutor taskExecutor;
​
    private JobExecution jobExecutionCreate;
​
    
    @Override
    public JobExecution run(final Job job, final JobParameters jobParameters)
            throws JobExecutionAlreadyRunningException, JobRestartException, JobInstanceAlreadyCompleteException,
            JobParametersInvalidException {
​
        Assert.notNull(job, "The Job must not be null.");
        Assert.notNull(jobParameters, "The JobParameters must not be null.");
​
        final JobExecution jobExecution;
        JobExecution lastExecution = jobRepository.getLastJobExecution(job.getName(), jobParameters);
        if (lastExecution != null) {
            if (!job.isRestartable()) {
                throw new JobRestartException("JobInstance already exists and is not restartable");
            }
            /*
             * validate here if it has stepExecutions that are UNKNOWN, STARTING, STARTED and STOPPING
             * retrieve the previous execution and check
             */
            for (StepExecution execution : lastExecution.getStepExecutions()) {
                BatchStatus status = execution.getStatus();
                if (status.isRunning() || status == BatchStatus.STOPPING) {
                    throw new JobExecutionAlreadyRunningException("A job execution for this job is already running: "
                            + lastExecution);
                } else if (status == BatchStatus.UNKNOWN) {
                    throw new JobRestartException(
                            "Cannot restart step [" + execution.getStepName() + "] from UNKNOWN status. "
                                    + "The last execution ended with a failure that could not be rolled back, "
                                    + "so it may be dangerous to proceed. Manual intervention is probably necessary.");
                }
            }
        }
​
        // Check the validity of the parameters before doing creating anything
        // in the repository...
        job.getJobParametersValidator().validate(jobParameters);
​
        /*
         * There is a very small probability that a non-restartable job can be
         * restarted, but only if another process or thread manages to launch
         * <i>and</i> fail a job execution for this instance between the last
         * assertion and the next method returning successfully.
         */
        jobExecution = jobRepository.createJobExecution(job.getName(), jobParameters);
        setJobExecutionCreate(jobExecution);
        try {
            taskExecutor.execute(new Runnable() {
​
                @Override
                public void run() {
                    try {
                        if (logger.isInfoEnabled()) {
                            logger.info("Job: [" + job + "] launched with the following parameters: [" + jobParameters
                                    + "]");
                        }
                        job.execute(jobExecution);
                        if (logger.isInfoEnabled()) {
                            Duration jobExecutionDuration = BatchMetrics.calculateDuration(jobExecution.getStartTime(), jobExecution.getEndTime());
                            logger.info("Job: [" + job + "] completed with the following parameters: [" + jobParameters
                                    + "] and the following status: [" + jobExecution.getStatus() + "]"
                                    + (jobExecutionDuration == null ? "" : " in " + BatchMetrics.formatDuration(jobExecutionDuration)));
                        }
                    }
                    catch (Throwable t) {
                        if (logger.isInfoEnabled()) {
                            logger.info("Job: [" + job
                                    + "] failed unexpectedly and fatally with the following parameters: [" + jobParameters
                                    + "]", t);
                        }
                        rethrow(t);
                    }
                }
​
                private void rethrow(Throwable t) {
                    if (t instanceof RuntimeException) {
                        throw (RuntimeException) t;
                    }
                    else if (t instanceof Error) {
                        throw (Error) t;
                    }
                    throw new IllegalStateException(t);
                }
            });
        }
        catch (TaskRejectedException e) {
            jobExecution.upgradeStatus(BatchStatus.FAILED);
            if (jobExecution.getExitStatus().equals(ExitStatus.UNKNOWN)) {
                jobExecution.setExitStatus(ExitStatus.FAILED.addExitDescription(e));
            }
            jobRepository.update(jobExecution);
        }
​
        return jobExecution;
    }
​
    /**
     * Set the JobRepository.
     *
     * @param jobRepository instance of {@link JobRepository}.
     */
    public void setJobRepository(JobRepository jobRepository) {
        this.jobRepository = jobRepository;
    }
​
    /**
     * Set the TaskExecutor. (Optional)
     *
     * @param taskExecutor instance of {@link TaskExecutor}.
     */
    public void setTaskExecutor(TaskExecutor taskExecutor) {
        this.taskExecutor = taskExecutor;
    }
​
    /**
     * Ensure the required dependencies of a {@link JobRepository} have been
     * set.
     */
    @Override
    public void afterPropertiesSet() throws Exception {
        Assert.state(jobRepository != null, "A JobRepository has not been set.");
        if (taskExecutor == null) {
            logger.info("No TaskExecutor has been set, defaulting to synchronous executor.");
            taskExecutor = new SyncTaskExecutor();
        }
    }
​
    public JobExecution getJobExecutionCreate() {
        return jobExecutionCreate;
    }
​
    public void setJobExecutionCreate(JobExecution jobExecutionCreate) {
        this.jobExecutionCreate = jobExecutionCreate;
    }
}

主要改造点是增加了一个变量

private JobExecution jobExecutionCreate;
setJobExecutionCreate(jobExecution);

如何使用:

@Test
    public void asyncJobLanucherJobTest()
    {
        Job job =ctx.getBean("asyncJobLanucherJob", Job.class);
        JobParametersBuilder jobParametersBuilder =new JobParametersBuilder();
        jobParametersBuilder.addDate("date",new Date());
        jobParametersBuilder.addString("rnd", UUID.randomUUID().toString());
        TaskAsyncJobLauncher taskAsyncJobLauncher = asyncConfiguration.AsyncJobLauncher();
​
        CompletableFuture<JobExecution> jobExecutionCreate = CompletableFuture.supplyAsync(() -> {
            while (taskAsyncJobLauncher.getJobExecutionCreate()==null){
                try {
                    System.out.println("继续等待100ms");
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("提前获取到创建jobExecution:"+taskAsyncJobLauncher.getJobExecutionCreate());
            return  taskAsyncJobLauncher.getJobExecutionCreate();
        });
        CompletableFuture<JobExecution> jobRunning = CompletableFuture.supplyAsync(() -> {
            try {
//                throw new RuntimeException("11=========");
                return taskAsyncJobLauncher.run(job,jobParametersBuilder.toJobParameters());
            } catch (Throwable e) {
                e.printStackTrace();
            }
            System.out.println("运行失败==");
            return null;
        });
        CompletableFuture<Void> end = jobExecutionCreate.thenAcceptBoth(jobRunning, (a, b) -> {
//            System.out.println(a+"准备,下" + b);
            System.out.println("运行成功=="+b);
        });
        end.join();
    }

可以达到要求。

方案4改造:增加个同步调用接口。

public interface jobExecutionCreateService {
​
    void jobExecutionCreateOperate(JobExecution jobExecution);
}

如何使用

image-20220625235349918

jobExecutionCreateService.jobExecutionCreateOperate(jobExecution);

最后还是方案4:比较优雅一些。用户只需要实现接口即可。还是同步执行的。

代码位置: github.com/jackssybin/…