Spring boot Batch 和Spring Batch 相比,用户都期望
- 可以根据exception 定义 退出码
 - 运行结束后可以直接按照退出码退出
 
退出码
但是spring boot batch 运行完之后,没有像spring batch 那样有显示的退出码。 但是他留有接口exit 方法里定义了 ExitCodeGenerator 可以根据context 里的exception 来得到。
    public static int exit(ApplicationContext context, ExitCodeGenerator... exitCodeGenerators) {
        Assert.notNull(context, "Context must not be null");
        int exitCode = 0;
        try {
            try {
                ExitCodeGenerators generators = new ExitCodeGenerators();
                Collection<ExitCodeGenerator> beans = context.getBeansOfType(ExitCodeGenerator.class).values();
                generators.addAll(exitCodeGenerators);
                generators.addAll(beans);
                exitCode = generators.getExitCode();
                if (exitCode != 0) {
                    context.publishEvent(new ExitCodeEvent(context, exitCode));
                }
            }
            finally {
                close(context);
            }
        }
        catch (Exception ex) {
            ex.printStackTrace();
            exitCode = (exitCode != 0) ? exitCode : 1;
        }
        return exitCode;
    }
那么我就可以定一个自己的ExitCodeGenerator,来进行exit code 和 exception 的mapping。
public class BatchJobExitCodeGenerator implements ApplicationListener<JobExecutionEvent>, ExitCodeGenerator {
  private static final Logger s_logger = LoggerFactory.getLogger(BatchJobExitCodeGenerator.class);
  private final List<JobExecution> executions = new ArrayList<>();
  private final Map<Class, Integer> springExceptionExitCodeMap = new ConcurrentHashMap<>();
  private final Map<String, IExitStatus> batchExitCodeMap = new ConcurrentHashMap<>();
  @Autowired
  private List<IExitStatus[]> allExitStatus;
    public BatchJobExitCodeGenerator() {
    springExceptionExitCodeMap.put(JobExecutionAlreadyRunningException.class,
        SystemExitStatus.FAILED_JOB_ALREADY_RUNNING.id());
    springExceptionExitCodeMap.put(JobRestartException.class, SystemExitStatus.FAILED_JOB_RESTART_FAILED.id());
    springExceptionExitCodeMap.put(JobInstanceAlreadyCompleteException.class,
        SystemExitStatus.FAILED_JOB_ALREADY_COMPLETED.id());
    springExceptionExitCodeMap.put(JobParametersInvalidException.class,
        SystemExitStatus.FAILED_JOB_PARAMETERS_INVALID.id());
    springExceptionExitCodeMap.put(JobInterruptedException.class, SystemExitStatus.FAILED_JOB_EXECUTION_FAILED.id());
    springExceptionExitCodeMap.put(JobParametersNotFoundException.class,
        SystemExitStatus.FAILED_REQUIRED_PARAMETER_MISSING.id());
  }
  /**
   * Put customized exit code into {@link #batchExitCodeMap}.
   */
  @PostConstruct
  public void initialExitCodeMap() {
    for (IExitStatus[] status : allExitStatus) {
      for (IExitStatus s : status) {
        batchExitCodeMap.put(s.name(), s);
      }
    }
  }
  @Override
  public void onApplicationEvent(JobExecutionEvent event) {
    this.executions.add(event.getJobExecution());
  }
  @Override
  public int getExitCode() {
    for (JobExecution execution : this.executions) {
      List<Throwable> failures = execution.getAllFailureExceptions();
      if (!failures.isEmpty()) {
        return getExitCodeFromException(failures.get(0));
      }
      int code = getExitCodeFromJobExecution(execution);
      if (code != 0) {
        return code;
      }
    }
    return 0;
  }
  private int getExitCodeFromJobExecution(JobExecution execution) {
    String name = execution.getExitStatus().getExitCode();
    IExitStatus code = batchExitCodeMap.get(name);
    int exitCode;
    if (code == null) {
        exitCode = SystemExitStatus.FAILED_UNKNOWN_EXIT_CODE.id();
    } else {
      exitCode = code.id();
    }
    return exitCode;
  }
  private int getExitCodeFromException(Throwable exception) {
    if (exception instanceof BatchRuntimeException) {
      return ((BatchRuntimeException) exception).getExitStatus().id();
    }
    for (Map.Entry<Class, Integer> entry : springExceptionExitCodeMap.entrySet()) {
      if (exception.getClass().isAssignableFrom(entry.getKey())) {
        return entry.getValue();
      }
    }
    return SystemExitStatus.FAILED_UNKNOWN_EXCEPTION.id();
  }
  public List<IExitStatus[]> getAllExitStatus() {
    return this.allExitStatus;
  }
}
这里面有一些概念
- 首先那拿到所有的jobExecution,因此就要 实现ApplicationListener<JobExecutionEvent> 这个接口,在下面这个方法里拿到jobExecution list。
 
  public void onApplicationEvent(JobExecutionEvent event) {
    this.executions.add(event.getJobExecution());
  }
- 其次 如果当系统发生异常, 进入getExitCodeFromException方法。看到是不是BatchRuntimeException, 如果是BatchRuntimeException, 那么直接就可以从BatchRuntimeException 拿到exit code。如果是普通的exception, 看看是不是default spring batch framework 的exception, 这个在构造函数里有声明。 如果找不到定义,就是FAILED_UNKNOWN_EXIT_CODE。 这就要求app 用户在抛exception 的时候必须抛出BatchRuntimeException。
 - 当系统没有异常时候,就走入getExitCodeFromJobExecution, 从定义的batchExitCodeMap ,取到 IExitStatus, 而这个map 是在initialExitCodeMap 构造的。
 
  @PostConstruct
  public void initialExitCodeMap() {
    for (IExitStatus[] status : allExitStatus) {
      for (IExitStatus s : status) {
        batchExitCodeMap.put(s.name(), s);
      }
    }
  }
首先看看IExitStatus 大概是什么样,通常来讲是个enum
public enum SystemExitStatus implements IExitStatus {
  /**
   * Value: 90
   *
   * <p>Exit Code used to report that the Batch Application initialization failed.
   */
  FAILED_INITIALIZATION_FAILED(ExitCodeType.System.getRange().min(), "Initialization Failed"),
  private final int statusId;
  private final String desc;
  SystemExitStatus(final int statusId, final String desc) {
    this.statusId = statusId;
    this.desc = desc;
  }
  @Override
  public ExitCodeType type() {
    return ExitCodeType.System;
  }
  @Override
  public int id() {
    return statusId;
  }
  @Override
  public String desc() {
    return desc;
  }
}
在正常使用中,如果要返回一下status 可以用下面类似代码
 public class MyJobExcutionListener implements JobExecutionListener {
    @Override
    public void beforeJob(JobExecution jobExecution) {
    }
    @Override
    public void afterJob(JobExecution jobExecution) {
        //Users could set expected exit code when a job about to finish
        jobExecution.setExitStatus(new ExitStatus(SystemExitStatus.FAILED_INITIALIZATION_FAILED.name()));
    }
}
上面这些就解决了用户自定义Exit code和如何从Exception 或者 正常的Spring boot ExitStatus 获得exit code方式。下面看看如何把这个exit code 返回。
按照退出码退出
我们可以自己写一个BatchSpringApplication 来复写
public final class BatchSpringApplication {
  public static void run(Class<?> primarySource, String... args) {
    int exitcode = -1;
    ConfigurableApplicationContext context = null;
    try {
      context = new SpringApplicationBuilder(primarySource).web(WebApplicationType.NONE).run(args);
      exitcode = SpringApplication.exit(context);
    } catch (Throwable e) { // NOSONAR
    
      exitcode = BatchSystemExitCodeGenerator.getExitCode(e);
    }
    
      System.exit(exitcode); 
  
  }
}
- 首先通过api 来前强行指定是
WebApplicationType.NONE - 调用
SpringApplication.exit(context)来获得exitcode - 
System.exit(exitcode)来和spring batch 的行为一样。 
Batch Job Validation
Spring Bath job 运行有个明显的问题,当参数传入一个不存在的job ,就会正常返回退出码,只是不运行而已。这样用户根本没有办法知道参数配错了。因此就有这个需求,当job 配了一个不存在的job 需要抛出exception。
因此,它需要在JobLauncherApplicationRunner 之前进,我们称之为JobLauncherApplicationRunner,它有如下feature
- 和JobLauncherApplicationRunner一样,受spring.batch.job.enabled" 控制
 - Order 要比JobLauncherApplicationRunner 前
 - 判断job 是否存在。
 
@Configuration
@ConditionalOnProperty(prefix = "spring.batch.job", name = "enabled", havingValue = "true", matchIfMissing = true)
public class BatchApplicationRunner implements ApplicationRunner, Ordered {
  private static final Logger s_logger = LoggerFactory.getLogger(BatchApplicationRunner.class);
  @Autowired
  private BatchProperties properties;
  private Collection<Job> jobs = Collections.emptySet();
  private JobRegistry jobRegistry;
  @Autowired(required = false)
  public void setJobs(Collection<Job> jobs) {
    this.jobs = jobs;
  }
  @Autowired(required = false)
  public void setJobRegistry(JobRegistry jobRegistry) {
    this.jobRegistry = jobRegistry;
  }
  /**
   * The method is the entrance which is used to validate given job names.
   */
  @Override
  public void run(ApplicationArguments args) {
    String jobNames = properties.getJob().getNames();
    List<String> realJobsToRun = new ArrayList<>();
    if (StringUtils.hasText(jobNames)) {
      String[] jobsToRun = jobNames.split(",");
      for (Job job : jobs) {
        if (PatternMatchUtils.simpleMatch(jobsToRun, job.getName())) {
          realJobsToRun.add(job.getName());
        }
      }
      if (this.jobRegistry != null && jobRegistry.getJobNames() != null) {
        for (String definedJobName : jobsToRun) {
          if (jobRegistry.getJobNames().contains(definedJobName) && !realJobsToRun.contains(definedJobName)) {
            realJobsToRun.add(definedJobName);
          }
        }
      }
    } else {
      for (Job job : jobs) {
        realJobsToRun.add(job.getName());
      }
    }
    if (realJobsToRun.isEmpty()) {
      if (StringUtils.hasText(jobNames)) {
        throw new BeanCreationException("Given job name [" + jobNames + "] is not able to load job definition");
      } else {
        s_logger.warn("No job is defined as Bean ");
      }
    } else {
      System.out.println("Following jobs will be triggerd" + realJobsToRun.toString()); //NOSONAR
    }
  }
  /* (non-Javadoc)
   * @see org.springframework.core.Ordered#getOrder()
   */
  @Override
  public int getOrder() {
    return Ordered.HIGHEST_PRECEDENCE;
  }
}
这样对Spring boot batch 一个简单的封装就完成了。