Spring boot Batch Customization

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 一个简单的封装就完成了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容