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