# 配置和运行作业
# 配置和运行作业
XMLJavaBoth
在领域部分中,使用以下图表作为指导,讨论了总体架构设计:
图 1。批处理模式
虽然Job
对象看起来像是一个用于步骤的简单容器,但开发人员必须了解许多配置选项。此外,对于如何运行Job
以及在运行期间如何存储其元数据,有许多考虑因素。本章将解释Job
的各种配置选项和运行时关注点。
# 配置作业
[Job
](#configurejob)接口有多个实现方式。然而,构建者会抽象出配置上的差异。
@Bean
public Job footballJob() {
return this.jobBuilderFactory.get("footballJob")
.start(playerLoad())
.next(gameLoad())
.next(playerSummarization())
.build();
}
一个Job
(以及其中的任何Step
)需要一个JobRepository
。JobRepository
的配置是通过[BatchConfigurer
]来处理的。
上面的示例演示了由三个Step
实例组成的Job
实例。与作业相关的构建器还可以包含有助于并行(Split
)、声明性流控制(Decision
)和流定义外部化(Flow
)的其他元素。
无论你使用 Java 还是 XML,[Job
](#configurejob)接口都有多个实现。然而,名称空间抽象出了配置上的差异。它只有三个必需的依赖项:一个名称,JobRepository
,和一个Step
实例的列表。
<job id="footballJob">
<step id="playerload" parent="s1" next="gameLoad"/>
<step id="gameLoad" parent="s2" next="playerSummarization"/>
<step id="playerSummarization" parent="s3"/>
</job>
这里的示例使用父 Bean 定义来创建步骤。有关内联声明特定步骤详细信息的更多选项,请参见阶跃配置一节。XML 名称空间默认情况下引用一个 ID 为“JobRepository”的存储库,这是一个合理的默认值。但是,这可以显式地重写:
<job id="footballJob" job-repository="specialRepository">
<step id="playerload" parent="s1" next="gameLoad"/>
<step id="gameLoad" parent="s3" next="playerSummarization"/>
<step id="playerSummarization" parent="s3"/>
</job>
除了步骤之外,作业配置还可以包含有助于并行(<split>
)、声明性流控制(<decision>
)和流定义外部化(<flow/>
)的其他元素。
# 可重启性
执行批处理作业时的一个关键问题与Job
重新启动时的行为有关。如果对于特定的Job
已经存在JobExecution
,则将Job
的启动视为“重新启动”。理想情况下,所有的工作都应该能够在它们停止的地方启动,但是在某些情况下这是不可能的。* 完全由开发人员来确保在此场景中创建一个新的JobInstance
。* 但是, Spring 批处理确实提供了一些帮助。如果Job
永远不应该重新启动,而应该始终作为新的JobInstance
的一部分运行,那么可重启属性可以设置为“false”。
下面的示例展示了如何在 XML 中将restartable
字段设置为false
:
XML 配置
<job id="footballJob" restartable="false">
...
</job>
下面的示例展示了如何在 Java 中将restartable
字段设置为false
:
Java 配置
@Bean
public Job footballJob() {
return this.jobBuilderFactory.get("footballJob")
.preventRestart()
...
.build();
}
换一种说法,将 Restartable 设置为 false 意味着“thisJob
不支持重新启动”。重新启动不可重启的Job
会导致抛出JobRestartException
。
Job job = new SimpleJob();
job.setRestartable(false);
JobParameters jobParameters = new JobParameters();
JobExecution firstExecution = jobRepository.createJobExecution(job, jobParameters);
jobRepository.saveOrUpdate(firstExecution);
try {
jobRepository.createJobExecution(job, jobParameters);
fail();
}
catch (JobRestartException e) {
// expected
}
这段 JUnit 代码展示了如何在第一次为不可重启作业创建JobExecution
时尝试创建JobExecution
不会导致任何问题。但是,第二次尝试将抛出JobRestartException
。
# 拦截作业执行
在作业的执行过程中,通知其生命周期中的各种事件可能是有用的,以便可以执行自定义代码。通过在适当的时间调用JobListener
,SimpleJob
允许这样做:
public interface JobExecutionListener {
void beforeJob(JobExecution jobExecution);
void afterJob(JobExecution jobExecution);
}
JobListeners
可以通过在作业上设置侦听器来添加到SimpleJob
。
下面的示例展示了如何将 Listener 元素添加到 XML 作业定义中:
XML 配置
<job id="footballJob">
<step id="playerload" parent="s1" next="gameLoad"/>
<step id="gameLoad" parent="s2" next="playerSummarization"/>
<step id="playerSummarization" parent="s3"/>
<listeners>
<listener ref="sampleListener"/>
</listeners>
</job>
下面的示例展示了如何将侦听器方法添加到 Java 作业定义中:
Java 配置
@Bean
public Job footballJob() {
return this.jobBuilderFactory.get("footballJob")
.listener(sampleListener())
...
.build();
}
应该注意的是,无论afterJob
方法的成功或失败,都调用Job
方法。如果需要确定成功或失败,则可以从JobExecution
中获得如下:
public void afterJob(JobExecution jobExecution){
if (jobExecution.getStatus() == BatchStatus.COMPLETED ) {
//job success
}
else if (jobExecution.getStatus() == BatchStatus.FAILED) {
//job failure
}
}
与此接口对应的注释是:
@BeforeJob
@AfterJob
# 继承父作业
如果一组作业共享相似但不相同的配置,那么定义一个“父”Job
可能会有所帮助,具体的作业可以从该“父”中继承属性。与 Java 中的类继承类似,“child”Job
将把它的元素和属性与父元素和属性结合在一起。
在下面的示例中,“basejob”是一个抽象的Job
定义,它只定义了一个侦听器列表。Job
“job1”是一个具体的定义,它继承了“Basejob”的侦听器列表,并将其与自己的侦听器列表合并,以生成一个Job
,其中包含两个侦听器和一个Step
,即“步骤 1”。
<job id="baseJob" abstract="true">
<listeners>
<listener ref="listenerOne"/>
<listeners>
</job>
<job id="job1" parent="baseJob">
<step id="step1" parent="standaloneStep"/>
<listeners merge="true">
<listener ref="listenerTwo"/>
<listeners>
</job>
有关更多详细信息,请参见从父步骤继承一节。
# JobParametersValidator
在 XML 命名空间中声明的作业或使用AbstractJob
的任意子类可以在运行时为作业参数声明验证器。例如,当你需要断言一个作业是以其所有的强制参数启动时,这是有用的。有一个DefaultJobParametersValidator
可以用来约束简单的强制参数和可选参数的组合,对于更复杂的约束,你可以自己实现接口。
验证程序的配置通过 XML 命名空间通过作业的一个子元素得到支持,如下面的示例所示:
<job id="job1" parent="baseJob3">
<step id="step1" parent="standaloneStep"/>
<validator ref="parametersValidator"/>
</job>
验证器可以指定为引用(如前面所示),也可以指定为 bean 名称空间中的嵌套 Bean 定义。
通过 Java Builders 支持验证器的配置,如以下示例所示:
@Bean
public Job job1() {
return this.jobBuilderFactory.get("job1")
.validator(parametersValidator())
...
.build();
}
# Java 配置
Spring 3 带来了通过 Java 而不是 XML 配置应用程序的能力。从 Spring Batch2.2.0 开始,可以使用相同的 Java 配置配置来配置批处理作业。基于 Java 的配置有两个组件:@EnableBatchProcessing
注释和两个构建器。
@EnableBatchProcessing
的工作原理与 Spring 家族中的其他 @enable* 注释类似。在这种情况下,@EnableBatchProcessing
提供了用于构建批处理作业的基本配置。在这个基本配置中,除了许多可用于自动连线的 bean 之外,还创建了StepScope
实例:
JobRepository
: Bean 名称“jobrepository”JobLauncher
: Bean 名称“joblauncher”JobRegistry
: Bean 名称“jobregistry”PlatformTransactionManager
: Bean 名称“TransactionManager”JobBuilderFactory
: Bean name“jobbuilders”StepBuilderFactory
: Bean 名称“StepBuilders”
此配置的核心接口是BatchConfigurer
。默认的实现提供了上面提到的 bean,并且需要在要提供的上下文中提供一个DataSource
作为 Bean。这个数据源由 JobRepository 使用。你可以通过创建BatchConfigurer
接口的自定义实现来定制这些 bean 中的任何一个。通常,扩展DefaultBatchConfigurer
(如果没有找到BatchConfigurer
,则提供该扩展)并重写所需的吸气器就足够了。然而,可能需要从头开始实现自己的功能。下面的示例展示了如何提供自定义事务管理器:
@Bean
public BatchConfigurer batchConfigurer(DataSource dataSource) {
return new DefaultBatchConfigurer(dataSource) {
@Override
public PlatformTransactionManager getTransactionManager() {
return new MyTransactionManager();
}
};
}
只有一个配置类需要@EnableBatchProcessing 注释。一旦对一个类进行了注释,就可以使用上面的所有内容了。 |
---|
有了基本配置,用户就可以使用提供的生成器工厂来配置作业。下面的示例显示了配置了JobBuilderFactory
和StepBuilderFactory
的两步作业:
@Configuration
@EnableBatchProcessing
@Import(DataSourceConfiguration.class)
public class AppConfig {
@Autowired
private JobBuilderFactory jobs;
@Autowired
private StepBuilderFactory steps;
@Bean
public Job job(@Qualifier("step1") Step step1, @Qualifier("step2") Step step2) {
return jobs.get("myJob").start(step1).next(step2).build();
}
@Bean
protected Step step1(ItemReader<Person> reader,
ItemProcessor<Person, Person> processor,
ItemWriter<Person> writer) {
return steps.get("step1")
.<Person, Person> chunk(10)
.reader(reader)
.processor(processor)
.writer(writer)
.build();
}
@Bean
protected Step step2(Tasklet tasklet) {
return steps.get("step2")
.tasklet(tasklet)
.build();
}
}
# 配置 JobRepository
当使用@EnableBatchProcessing
时,将为你提供一个JobRepository
。本节讨论如何配置自己的配置。
如前面所述,[JobRepository
](#configurejob)用于 Spring 批处理中各种持久化域对象的基本增删改查操作,例如JobExecution
和StepExecution
。它是许多主要框架特性所要求的,例如JobLauncher
、Job
和Step
。
批处理名称空间抽象出了JobRepository
实现及其协作者的许多实现细节。然而,仍然有一些可用的配置选项,如以下示例所示:
XML 配置
<job-repository id="jobRepository"
data-source="dataSource"
transaction-manager="transactionManager"
isolation-level-for-create="SERIALIZABLE"
table-prefix="BATCH_"
max-varchar-length="1000"/>
除了id
之外,上面列出的配置选项都不是必需的。如果没有设置,将使用上面显示的默认值。以上所示是为了提高认识。max-varchar-length
默认为 2500,这是示例模式脚本中的长VARCHAR
列的长度。
当使用 Java 配置时,将为你提供JobRepository
。如果提供了DataSource
,则提供了基于 JDBC 的一个,如果没有,则提供基于Map
的一个。但是,你可以通过BatchConfigurer
接口的实现来定制JobRepository
的配置。
Java 配置
...
// This would reside in your BatchConfigurer implementation
@Override
protected JobRepository createJobRepository() throws Exception {
JobRepositoryFactoryBean factory = new JobRepositoryFactoryBean();
factory.setDataSource(dataSource);
factory.setTransactionManager(transactionManager);
factory.setIsolationLevelForCreate("ISOLATION_SERIALIZABLE");
factory.setTablePrefix("BATCH_");
factory.setMaxVarCharLength(1000);
return factory.getObject();
}
...
除了数据源和 TransactionManager 之外,上面列出的配置选项都不是必需的。如果没有设置,将使用上面显示的默认值。以上所示是为了提高认识。最大 VARCHAR 长度默认为 2500,这是VARCHAR
中的长示例模式脚本列的长度
# JobRepository 的事务配置
如果使用了名称空间或提供的FactoryBean
,则会在存储库周围自动创建事务建议。这是为了确保批处理元数据(包括在发生故障后重新启动所必需的状态)被正确地持久化。如果存储库方法不是事务性的,那么框架的行为就没有得到很好的定义。create*
方法属性中的隔离级别是单独指定的,以确保在启动作业时,如果两个进程试图同时启动相同的作业,则只有一个进程成功。该方法的默认隔离级别是SERIALIZABLE
,这是非常激进的。READ_COMMITTED
同样有效。如果两个过程不太可能以这种方式碰撞,READ_UNCOMMITTED
就可以了。然而,由于对create*
方法的调用相当短,所以只要数据库平台支持它,SERIALIZED
不太可能导致问题。然而,这一点可以被重写。
下面的示例展示了如何覆盖 XML 中的隔离级别:
XML 配置
<job-repository id="jobRepository"
isolation-level-for-create="REPEATABLE_READ" />
下面的示例展示了如何在 Java 中重写隔离级别:
Java 配置
// This would reside in your BatchConfigurer implementation
@Override
protected JobRepository createJobRepository() throws Exception {
JobRepositoryFactoryBean factory = new JobRepositoryFactoryBean();
factory.setDataSource(dataSource);
factory.setTransactionManager(transactionManager);
factory.setIsolationLevelForCreate("ISOLATION_REPEATABLE_READ");
return factory.getObject();
}
如果没有使用名称空间或工厂 bean,那么使用 AOP 配置存储库的事务行为也是必不可少的。
下面的示例展示了如何在 XML 中配置存储库的事务行为:
XML 配置
<aop:config>
<aop:advisor
pointcut="execution(* org.springframework.batch.core..*Repository+.*(..))"/>
<advice-ref="txAdvice" />
</aop:config>
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="*" />
</tx:attributes>
</tx:advice>
前面的片段可以按原样使用,几乎没有变化。还请记住包括适当的名称空间声明,并确保 Spring-tx 和 Spring- AOP(或整个 Spring)都在 Classpath 上。
下面的示例展示了如何在 Java 中配置存储库的事务行为:
Java 配置
@Bean
public TransactionProxyFactoryBean baseProxy() {
TransactionProxyFactoryBean transactionProxyFactoryBean = new TransactionProxyFactoryBean();
Properties transactionAttributes = new Properties();
transactionAttributes.setProperty("*", "PROPAGATION_REQUIRED");
transactionProxyFactoryBean.setTransactionAttributes(transactionAttributes);
transactionProxyFactoryBean.setTarget(jobRepository());
transactionProxyFactoryBean.setTransactionManager(transactionManager());
return transactionProxyFactoryBean;
}
# 更改表格前缀
JobRepository
的另一个可修改的属性是元数据表的表前缀。默认情况下,它们都以BATCH_
开头。BATCH_JOB_EXECUTION
和BATCH_STEP_EXECUTION
是两个例子。然而,有潜在的理由修改这个前缀。如果需要将模式名称前置到表名,或者如果同一模式中需要多个元数据表集合,则需要更改表前缀:
下面的示例展示了如何更改 XML 中的表前缀:
XML 配置
<job-repository id="jobRepository"
table-prefix="SYSTEM.TEST_" />
下面的示例展示了如何在 Java 中更改表前缀:
Java 配置
// This would reside in your BatchConfigurer implementation
@Override
protected JobRepository createJobRepository() throws Exception {
JobRepositoryFactoryBean factory = new JobRepositoryFactoryBean();
factory.setDataSource(dataSource);
factory.setTransactionManager(transactionManager);
factory.setTablePrefix("SYSTEM.TEST_");
return factory.getObject();
}
给定上述更改,对元数据表的每个查询都以SYSTEM.TEST_
作为前缀。BATCH_JOB_EXECUTION
被称为系统。TEST_JOB_EXECUTION
。
只有表前缀是可配置的。表和列名不是。 |
---|
# 内存存储库
在某些情况下,你可能不希望将域对象持久化到数据库。原因之一可能是速度;在每个提交点存储域对象需要额外的时间。另一个原因可能是,你不需要为一份特定的工作坚持现状。出于这个原因, Spring 批处理提供了作业存储库的内存Map
版本。
下面的示例显示了MapJobRepositoryFactoryBean
在 XML 中的包含:
XML 配置
<bean id="jobRepository"
class="org.springframework.batch.core.repository.support.MapJobRepositoryFactoryBean">
<property name="transactionManager" ref="transactionManager"/>
</bean>
下面的示例显示了在 Java 中包含MapJobRepositoryFactoryBean
:
Java 配置
// This would reside in your BatchConfigurer implementation
@Override
protected JobRepository createJobRepository() throws Exception {
MapJobRepositoryFactoryBean factory = new MapJobRepositoryFactoryBean();
factory.setTransactionManager(transactionManager);
return factory.getObject();
}
请注意,内存中的存储库是不稳定的,因此不允许在 JVM 实例之间重新启动。它也不能保证具有相同参数的两个作业实例同时启动,并且不适合在多线程作业或本地分区Step
中使用。因此,只要你需要这些特性,就可以使用存储库的数据库版本。
但是,它确实需要定义事务管理器,因为存储库中存在回滚语义,并且业务逻辑可能仍然是事务性的(例如 RDBMS 访问)。对于测试目的,许多人发现ResourcelessTransactionManager
很有用。
在 V4 中,MapJobRepositoryFactoryBean 和相关的类已被弃用,并计划在 V5 中删除。如果希望使用内存中的作业存储库,可以使用嵌入式数据库 ,比如 H2、 Apache Derby 或 HSQLDB。有几种方法可以创建嵌入式数据库并在 你的 Spring 批处理应用程序中使用它。一种方法是使用Spring JDBC (opens new window)中的 API: <br/>@Bean<br/>public DataSource dataSource() {<br/> return new EmbeddedDatabaseBuilder()<br/> .setType(EmbeddedDatabaseType.H2)<br/> .addScript("/org/springframework/batch/core/schema-drop-h2.sql")<br/> .addScript("/org/springframework/batch/core/schema-h2.sql")<br/> .build();<br/>}<br/> 一旦你在应用程序上下文中将嵌入式数据源定义为 Bean,如果你使用 @EnableBatchProcessing ,就应该自动选择。否则,你可以使用 基于 JobRepositoryFactoryBean 的 JDBC 手动配置它,如配置 JobRepository 部分所示。 |
---|
# 存储库中的非标准数据库类型
如果你使用的数据库平台不在受支持的平台列表中,那么如果 SQL 变量足够接近,则可以使用受支持的类型之一。要做到这一点,你可以使用 RAWJobRepositoryFactoryBean
而不是名称空间快捷方式,并使用它将数据库类型设置为最接近的匹配。
下面的示例展示了如何使用JobRepositoryFactoryBean
将数据库类型设置为 XML 中最接近的匹配:
XML 配置
<bean id="jobRepository" class="org...JobRepositoryFactoryBean">
<property name="databaseType" value="db2"/>
<property name="dataSource" ref="dataSource"/>
</bean>
下面的示例展示了如何使用JobRepositoryFactoryBean
将数据库类型设置为 Java 中最接近的匹配:
Java 配置
// This would reside in your BatchConfigurer implementation
@Override
protected JobRepository createJobRepository() throws Exception {
JobRepositoryFactoryBean factory = new JobRepositoryFactoryBean();
factory.setDataSource(dataSource);
factory.setDatabaseType("db2");
factory.setTransactionManager(transactionManager);
return factory.getObject();
}
(如果没有指定,JobRepositoryFactoryBean
会尝试从DataSource
中自动检测数据库类型,)平台之间的主要差异主要是由主键递增策略造成的,因此,通常可能还需要覆盖incrementerFactory
(使用 Spring 框架中的一个标准实现)。
如果连这都不起作用,或者你没有使用 RDBMS,那么唯一的选择可能是实现Dao
所依赖的各种SimpleJobRepository
接口,并以正常的方式手动连接。
# 配置一个 joblauncher
当使用@EnableBatchProcessing
时,将为你提供一个JobRegistry
。本节讨论如何配置自己的配置。
JobLauncher
接口的最基本实现是SimpleJobLauncher
。它唯一需要的依赖关系是JobRepository
,以便获得执行。
下面的示例显示了 XML 中的SimpleJobLauncher
:
XML 配置
<bean id="jobLauncher"
class="org.springframework.batch.core.launch.support.SimpleJobLauncher">
<property name="jobRepository" ref="jobRepository" />
</bean>
下面的示例显示了 Java 中的SimpleJobLauncher
:
Java 配置
...
// This would reside in your BatchConfigurer implementation
@Override
protected JobLauncher createJobLauncher() throws Exception {
SimpleJobLauncher jobLauncher = new SimpleJobLauncher();
jobLauncher.setJobRepository(jobRepository);
jobLauncher.afterPropertiesSet();
return jobLauncher;
}
...
一旦获得了工作执行,它就被传递给Job
的执行方法,最终将JobExecution
返回给调用者,如下图所示:
图 2。作业启动器序列
这个序列很简单,从调度程序启动时效果很好。然而,在尝试从 HTTP 请求启动时会出现问题。在这种情况下,启动需要异步完成,以便SimpleJobLauncher
立即返回其调用方。这是因为,在长时间运行的进程(如批处理)所需的时间内保持 HTTP 请求的开放状态是不好的做法。下图显示了一个示例序列:
图 3。异步作业启动器序列
可以通过配置TaskExecutor
将SimpleJobLauncher
配置为允许这种情况。
下面的 XML 示例显示了配置为立即返回的SimpleJobLauncher
:
XML 配置
<bean id="jobLauncher"
class="org.springframework.batch.core.launch.support.SimpleJobLauncher">
<property name="jobRepository" ref="jobRepository" />
<property name="taskExecutor">
<bean class="org.springframework.core.task.SimpleAsyncTaskExecutor" />
</property>
</bean>
下面的 Java 示例显示了配置为立即返回的SimpleJobLauncher
:
Java 配置
@Bean
public JobLauncher jobLauncher() {
SimpleJobLauncher jobLauncher = new SimpleJobLauncher();
jobLauncher.setJobRepository(jobRepository());
jobLauncher.setTaskExecutor(new SimpleAsyncTaskExecutor());
jobLauncher.afterPropertiesSet();
return jobLauncher;
}
Spring TaskExecutor
接口的任何实现都可以用来控制如何异步执行作业。
# 运行作业
至少,启动批处理作业需要两个条件:启动Job
和JobLauncher
。两者都可以包含在相同的上下文中,也可以包含在不同的上下文中。例如,如果从命令行启动一个作业,将为每个作业实例化一个新的 JVM,因此每个作业都有自己的JobLauncher
。但是,如果在HttpRequest
范围内的 Web 容器中运行,通常会有一个JobLauncher
,该配置用于异步作业启动,多个请求将调用以启动其作业。
# 从命令行运行作业
对于希望从 Enterprise 调度器运行作业的用户,命令行是主要的接口。这是因为大多数调度程序(Quartz 除外,除非使用 nativeJob)直接与操作系统进程一起工作,主要是通过 shell 脚本开始的。除了 shell 脚本之外,还有许多启动 Java 进程的方法,例如 Perl、Ruby,甚至是 Ant 或 Maven 之类的“构建工具”。但是,由于大多数人都熟悉 shell 脚本,因此本例将重点讨论它们。
# The CommandlineJobrunner
因为启动作业的脚本必须启动一个 Java 虚拟机,所以需要有一个具有 main 方法的类来充当主要入口点。 Spring 批处理提供了一种实现,它仅服务于此目的:CommandLineJobRunner
。需要注意的是,这只是引导应用程序的一种方法,但是启动 Java 进程的方法有很多,并且这个类绝不应该被视为确定的。CommandLineJobRunner
执行四项任务:
装入适当的
ApplicationContext
将命令行参数解析为
JobParameters
根据参数定位适当的作业
使用应用程序上下文中提供的
JobLauncher
来启动作业。
所有这些任务都是仅使用传入的参数来完成的。以下是必要的论据:
jobPath | 将用于 的 XML 文件的位置创建一个 ApplicationContext 。此文件应该包含运行完整 作业所需的所有内容 |
---|---|
jobName | 要运行的作业的名称。 |
这些参数必须首先传递路径,然后传递名称。在这些参数之后的所有参数都被认为是作业参数,被转换为一个 JobParameters 对象,并且必须是“name=value”的格式。
下面的示例显示了作为作业参数传递给 XML 中未定义的作业的日期:
<bash$ java CommandLineJobRunner endOfDayJob.xml endOfDay schedule.date(date)=2007/05/05
下面的示例显示了作为作业参数传递给 Java 中定义的作业的日期:
<bash$ java CommandLineJobRunner io.spring.EndOfDayJobConfiguration endOfDay schedule.date(date)=2007/05/05
默认情况下,CommandLineJobRunner 使用DefaultJobParametersConverter ,它隐式地将键/值对转换为标识作业参数。但是,在下面的示例中,可以通过分别使用 + 或- 前缀来显式地指定哪些作业参数是标识的,哪些不是标识的。 , schedule.date 是一个标识作业参数,而vendor.id 不是:<br/><bash$ java CommandLineJobRunner endOfDayJob.xml endOfDay \<br/> +schedule.date(date)=2007/05/05 -vendor.id=123<br/> <br/><bash$ java CommandLineJobRunner io.spring.EndOfDayJobConfiguration endOfDay \<br/> +schedule.date(date)=2007/05/05 -vendor.id=123<br/> 可以通过使用自定义 JobParametersConverter 来重写此行为。 |
---|
在大多数情况下,你可能希望使用清单在 JAR 中声明主类,但为了简单起见,直接使用了该类。这个示例使用的是来自DomainLanguageofBatch的相同的“Endofday”示例。第一个参数是“endofdayjob.xml”,这是 Spring 包含Job
的应用上下文。第二个参数“Endofday”表示工作名称。最后一个参数“schedule.date=2007/05/05”被转换为一个 JobParameters 对象。
下面的示例显示了在 XML 中endOfDay
的示例配置:
<job id="endOfDay">
<step id="step1" parent="simpleStep" />
</job>
<!-- Launcher details removed for clarity -->
<beans:bean id="jobLauncher"
class="org.springframework.batch.core.launch.support.SimpleJobLauncher" />
在大多数情况下,你希望使用清单在 JAR 中声明主类,但为了简单起见,直接使用了该类。这个示例使用的是来自DomainLanguageofBatch的相同的“Endofday”示例。第一个参数是“IO. Spring.EndofdayJobConfiguration”,它是包含该作业的配置类的完全限定类名称。第二个参数“Endofday”表示工作名称。最后一个参数’schedule.date=2007/05/05’被转换为JobParameters
对象。下面是 Java 配置的一个示例:
下面的示例显示了在 Java 中endOfDay
的示例配置:
@Configuration
@EnableBatchProcessing
public class EndOfDayJobConfiguration {
@Autowired
private JobBuilderFactory jobBuilderFactory;
@Autowired
private StepBuilderFactory stepBuilderFactory;
@Bean
public Job endOfDay() {
return this.jobBuilderFactory.get("endOfDay")
.start(step1())
.build();
}
@Bean
public Step step1() {
return this.stepBuilderFactory.get("step1")
.tasklet((contribution, chunkContext) -> null)
.build();
}
}
前面的示例过于简单,因为在 Spring 批处理中运行一个批处理作业通常有更多的需求,但是它用于显示CommandLineJobRunner
的两个主要需求:Job
和JobLauncher
。
# exitcodes
当从命令行启动批处理作业时,通常使用 Enterprise 调度器。大多数调度器都相当笨拙,只能在流程级别工作。这意味着他们只知道一些操作系统进程,比如他们正在调用的 shell 脚本。在这种情况下,将工作的成功或失败反馈给调度程序的唯一方法是通过返回代码。返回代码是进程返回给调度程序的一个数字,它指示运行的结果。在最简单的情况下:0 是成功,1 是失败。然而,可能有更复杂的情况:如果作业 A 返回 4,则启动作业 B,如果它返回 5,则启动作业 C。这种类型的行为是在计划程序级别上配置的,但是重要的是, Spring 批处理框架提供了一种方法来返回用于特定批处理作业的“退出代码”的数字表示。在 Spring 批处理中,这被封装在ExitStatus
中,这在第 5 章中有更详细的介绍。为了讨论退出代码,唯一需要知道的是ExitStatus
具有一个退出代码属性,该属性由框架(或开发人员)设置,并作为从JobLauncher
返回的JobExecution
的一部分返回。CommandLineJobRunner
使用ExitCodeMapper
接口将这个字符串值转换为一个数字:
public interface ExitCodeMapper {
public int intValue(String exitCode);
}
ExitCodeMapper
的基本契约是,给定一个字符串退出代码,将返回一个数字表示。Job Runner 使用的默认实现是SimpleJvmExitCodeMapper
,它返回 0 表示完成,1 表示泛型错误,2 表示任何 Job Runner 错误,例如无法在提供的上下文中找到Job
。如果需要比上述 3 个值更复杂的值,则必须提供ExitCodeMapper
接口的自定义实现。因为CommandLineJobRunner
是创建ApplicationContext
的类,因此不能“连线在一起”,所以需要重写的任何值都必须是自动连线的。这意味着,如果在BeanFactory
中找到了ExitCodeMapper
的实现,则将在创建上下文后将其注入到运行器中。要提供你自己的ExitCodeMapper
,需要做的就是将实现声明为根级别 Bean,并确保它是由运行器加载的ApplicationContext
的一部分。
# 在 Web 容器中运行作业
从历史上看,离线处理(如批处理作业)是从命令行启动的,如上文所述。然而,在许多情况下,从HttpRequest
发射是更好的选择。许多这样的用例包括报告、临时作业运行和 Web 应用程序支持。因为按定义,批处理作业是长时间运行的,所以最重要的问题是确保异步启动该作业:
图 4。来自 Web 容器的异步作业启动器序列
在这种情况下,控制器是 Spring MVC 控制器。关于 Spring MVC 的更多信息可以在这里找到:的Job
启动Job
,该控制器立即返回JobExecution
。Job
可能仍在运行,但是,这种非阻塞行为允许控制器立即返回,这是处理HttpRequest
时所需的。以下是一个例子:
@Controller
public class JobLauncherController {
@Autowired
JobLauncher jobLauncher;
@Autowired
Job job;
@RequestMapping("/jobLauncher.html")
public void handle() throws Exception{
jobLauncher.run(job, new JobParameters());
}
}
# 高级元数据使用
到目前为止,JobLauncher
和JobRepository
接口都已经讨论过了。它们一起表示作业的简单启动,以及批处理域对象的基本操作:
图 5。作业存储库
aJobLauncher
使用JobRepository
来创建新的JobExecution
对象并运行它们。Job
和Step
实现稍后将使用相同的JobRepository
用于运行作业期间相同执行的基本更新。对于简单的场景,基本的操作就足够了,但是在具有数百个批处理任务和复杂的调度需求的大批处理环境中,需要对元数据进行更高级的访问:
图 6。高级作业存储库访问
下面将讨论JobExplorer
和JobOperator
接口,它们添加了用于查询和控制元数据的附加功能。
# 查询存储库
在任何高级特性之前,最基本的需求是查询存储库中现有执行的能力。此功能由JobExplorer
接口提供:
public interface JobExplorer {
List<JobInstance> getJobInstances(String jobName, int start, int count);
JobExecution getJobExecution(Long executionId);
StepExecution getStepExecution(Long jobExecutionId, Long stepExecutionId);
JobInstance getJobInstance(Long instanceId);
List<JobExecution> getJobExecutions(JobInstance jobInstance);
Set<JobExecution> findRunningJobExecutions(String jobName);
}
从上面的方法签名中可以明显看出,JobExplorer
是JobRepository
的只读版本,并且,像JobRepository
一样,可以通过使用工厂 Bean 轻松地对其进行配置:
下面的示例展示了如何在 XML 中配置JobExplorer
:
XML 配置
<bean id="jobExplorer" class="org.spr...JobExplorerFactoryBean"
p:dataSource-ref="dataSource" />
下面的示例展示了如何在 Java 中配置JobExplorer
:
Java 配置
...
// This would reside in your BatchConfigurer implementation
@Override
public JobExplorer getJobExplorer() throws Exception {
JobExplorerFactoryBean factoryBean = new JobExplorerFactoryBean();
factoryBean.setDataSource(this.dataSource);
return factoryBean.getObject();
}
...
在本章的前面,我们注意到JobRepository
的表前缀可以进行修改以允许不同的版本或模式。因为JobExplorer
与相同的表一起工作,所以它也需要设置前缀的能力。
下面的示例展示了如何在 XML 中设置JobExplorer
的表前缀:
XML 配置
<bean id="jobExplorer" class="org.spr...JobExplorerFactoryBean"
p:tablePrefix="SYSTEM."/>
下面的示例展示了如何在 Java 中设置JobExplorer
的表前缀:
Java 配置
...
// This would reside in your BatchConfigurer implementation
@Override
public JobExplorer getJobExplorer() throws Exception {
JobExplorerFactoryBean factoryBean = new JobExplorerFactoryBean();
factoryBean.setDataSource(this.dataSource);
factoryBean.setTablePrefix("SYSTEM.");
return factoryBean.getObject();
}
...
# JobRegistry
aJobRegistry
(及其父接口JobLocator
)不是强制性的,但如果你想跟踪上下文中哪些作业可用,它可能会很有用。当工作在其他地方创建时(例如,在子上下文中),它对于在应用程序上下文中集中收集工作也很有用。还可以使用自定义JobRegistry
实现来操作已注册作业的名称和其他属性。该框架只提供了一个实现,它基于从作业名称到作业实例的简单映射。
下面的示例展示了如何为 XML 中定义的作业包含JobRegistry
:
<bean id="jobRegistry" class="org.springframework.batch.core.configuration.support.MapJobRegistry" />
下面的示例展示了如何为 Java 中定义的作业包含JobRegistry
:
当使用@EnableBatchProcessing
时,将为你提供一个JobRegistry
。如果你想配置自己的:
...
// This is already provided via the @EnableBatchProcessing but can be customized via
// overriding the getter in the SimpleBatchConfiguration
@Override
@Bean
public JobRegistry jobRegistry() throws Exception {
return new MapJobRegistry();
}
...
有两种方法可以自动填充JobRegistry
:使用 Bean 后处理器和使用注册商生命周期组件。这两种机制在下面的部分中进行了描述。
# jobregistrybeanpostprocessor
这是一个 Bean 后处理器,它可以在创建所有作业时注册它们。
下面的示例展示了如何为 XML 中定义的作业包括JobRegistryBeanPostProcessor
:
XML 配置
<bean id="jobRegistryBeanPostProcessor" class="org.spr...JobRegistryBeanPostProcessor">
<property name="jobRegistry" ref="jobRegistry"/>
</bean>
下面的示例展示了如何为在 Java 中定义的作业包括JobRegistryBeanPostProcessor
:
Java 配置
@Bean
public JobRegistryBeanPostProcessor jobRegistryBeanPostProcessor() {
JobRegistryBeanPostProcessor postProcessor = new JobRegistryBeanPostProcessor();
postProcessor.setJobRegistry(jobRegistry());
return postProcessor;
}
虽然这不是严格必要的,但是在示例中的后处理器已经被赋予了一个 ID,以便它可以被包括在子上下文中(例如作为父 Bean 定义),并导致在那里创建的所有作业也被自动注册。
# AutomaticJobRegistrar
这是一个生命周期组件,它创建子上下文,并在创建这些上下文时从这些上下文注册作业。这样做的一个好处是,虽然子上下文中的作业名称在注册表中仍然必须是全局唯一的,但它们的依赖项可能具有“自然”名称。因此,例如,你可以创建一组 XML 配置文件,每个配置文件只具有一个作业,但所有配置文件都具有具有具有相同 Bean 名称的ItemReader
的不同定义,例如“reader”。如果将所有这些文件导入到相同的上下文中,则读写器定义将发生冲突并相互覆盖,但是使用自动注册器可以避免这种情况。这使得集成来自应用程序的独立模块的作业变得更加容易。
下面的示例展示了如何为 XML 中定义的作业包括AutomaticJobRegistrar
:
XML 配置
<bean class="org.spr...AutomaticJobRegistrar">
<property name="applicationContextFactories">
<bean class="org.spr...ClasspathXmlApplicationContextsFactoryBean">
<property name="resources" value="classpath*:/config/job*.xml" />
</bean>
</property>
<property name="jobLoader">
<bean class="org.spr...DefaultJobLoader">
<property name="jobRegistry" ref="jobRegistry" />
</bean>
</property>
</bean>
下面的示例展示了如何为在 Java 中定义的作业包括AutomaticJobRegistrar
:
Java 配置
@Bean
public AutomaticJobRegistrar registrar() {
AutomaticJobRegistrar registrar = new AutomaticJobRegistrar();
registrar.setJobLoader(jobLoader());
registrar.setApplicationContextFactories(applicationContextFactories());
registrar.afterPropertiesSet();
return registrar;
}
注册商有两个强制属性,一个是ApplicationContextFactory
的数组(这里是从方便的工厂 Bean 创建的),另一个是JobLoader
。JobLoader
负责管理子上下文的生命周期,并在JobRegistry
中注册作业。
ApplicationContextFactory
负责创建子上下文,最常见的用法是使用ClassPathXmlApplicationContextFactory
。这个工厂的一个特性是,默认情况下,它会将一些配置从父上下文复制到子上下文。因此,例如,如果它应该与父配置相同,则不必重新定义子配置中的PropertyPlaceholderConfigurer
或 AOP 配置。
如果需要,AutomaticJobRegistrar
可以与JobRegistryBeanPostProcessor
一起使用(只要DefaultJobLoader
也可以使用)。例如,如果在主父上下文和子位置中定义了作业,那么这可能是可取的。
# joboperator
如前所述,JobRepository
提供对元数据的增删改查操作,而JobExplorer
提供对元数据的只读操作。然而,当这些操作一起用来执行常见的监视任务时,它们是最有用的,例如停止、重新启动或汇总作业,就像批处理操作符通常做的那样。 Spring 批处理通过JobOperator
接口提供这些类型的操作:
public interface JobOperator {
List<Long> getExecutions(long instanceId) throws NoSuchJobInstanceException;
List<Long> getJobInstances(String jobName, int start, int count)
throws NoSuchJobException;
Set<Long> getRunningExecutions(String jobName) throws NoSuchJobException;
String getParameters(long executionId) throws NoSuchJobExecutionException;
Long start(String jobName, String parameters)
throws NoSuchJobException, JobInstanceAlreadyExistsException;
Long restart(long executionId)
throws JobInstanceAlreadyCompleteException, NoSuchJobExecutionException,
NoSuchJobException, JobRestartException;
Long startNextInstance(String jobName)
throws NoSuchJobException, JobParametersNotFoundException, JobRestartException,
JobExecutionAlreadyRunningException, JobInstanceAlreadyCompleteException;
boolean stop(long executionId)
throws NoSuchJobExecutionException, JobExecutionNotRunningException;
String getSummary(long executionId) throws NoSuchJobExecutionException;
Map<Long, String> getStepExecutionSummaries(long executionId)
throws NoSuchJobExecutionException;
Set<String> getJobNames();
}
上面的操作表示来自许多不同接口的方法,例如JobLauncher
、JobRepository
、JobExplorer
和JobRegistry
。由于这个原因,所提供的JobOperator
、SimpleJobOperator
的实现具有许多依赖性。
下面的示例显示了 XML 中SimpleJobOperator
的典型 Bean 定义:
<bean id="jobOperator" class="org.spr...SimpleJobOperator">
<property name="jobExplorer">
<bean class="org.spr...JobExplorerFactoryBean">
<property name="dataSource" ref="dataSource" />
</bean>
</property>
<property name="jobRepository" ref="jobRepository" />
<property name="jobRegistry" ref="jobRegistry" />
<property name="jobLauncher" ref="jobLauncher" />
</bean>
下面的示例显示了 Java 中SimpleJobOperator
的典型 Bean 定义:
/**
* All injected dependencies for this bean are provided by the @EnableBatchProcessing
* infrastructure out of the box.
*/
@Bean
public SimpleJobOperator jobOperator(JobExplorer jobExplorer,
JobRepository jobRepository,
JobRegistry jobRegistry) {
SimpleJobOperator jobOperator = new SimpleJobOperator();
jobOperator.setJobExplorer(jobExplorer);
jobOperator.setJobRepository(jobRepository);
jobOperator.setJobRegistry(jobRegistry);
jobOperator.setJobLauncher(jobLauncher);
return jobOperator;
}
如果你在作业存储库上设置了表前缀,请不要忘记在作业资源管理器上也设置它。 |
---|
# JobParametersIncrementer
关于JobOperator
的大多数方法都是不言自明的,更详细的解释可以在接口的 Javadoc (opens new window)上找到。然而,startNextInstance
方法是值得注意的。这个方法总是会启动一个作业的新实例。如果JobExecution
中存在严重问题,并且需要从一开始就重新开始工作,那么这将非常有用。与JobLauncher
不同,JobLauncher
需要一个新的JobParameters
对象,如果参数与以前的任何一组参数不同,则该对象将触发一个新的JobInstance
,startNextInstance
方法将使用绑定到JobParametersIncrementer
的Job
来强制将Job
转换为一个新实例:
public interface JobParametersIncrementer {
JobParameters getNext(JobParameters parameters);
}
JobParametersIncrementer
的约定是,给定一个工作参数对象,它将通过递增它可能包含的任何必要值来返回“next”JobParameters 对象。这个策略是有用的,因为框架无法知道JobParameters
的更改是什么,使它成为“下一个”实例。例如,如果JobParameters
中的唯一值是日期,并且应该创建下一个实例,那么该值应该增加一天吗?或者一周(例如,如果工作是每周一次的话)?对于有助于识别工作的任何数值,也可以这样说,如下所示:
public class SampleIncrementer implements JobParametersIncrementer {
public JobParameters getNext(JobParameters parameters) {
if (parameters==null || parameters.isEmpty()) {
return new JobParametersBuilder().addLong("run.id", 1L).toJobParameters();
}
long id = parameters.getLong("run.id",1L) + 1;
return new JobParametersBuilder().addLong("run.id", id).toJobParameters();
}
}
在本例中,使用带有“run.id”键的值来区分JobInstances
。如果传入的JobParameters
为空,则可以假定Job
以前从未运行过,因此可以返回其初始状态。但是,如果不是,则获得旧值,将其递增 1 并返回。
对于 XML 中定义的作业,Incrementer 可以通过名称空间中的’Incrementer’属性与Job
关联,如下所示:
<job id="footballJob" incrementer="sampleIncrementer">
...
</job>
对于在 Java 中定义的作业,增量程序可以通过构建器中提供的incrementer
方法与“作业”关联,如下所示:
@Bean
public Job footballJob() {
return this.jobBuilderFactory.get("footballJob")
.incrementer(sampleIncrementer())
...
.build();
}
# 停止工作
JobOperator
最常见的用例之一是优雅地停止一项工作:
Set<Long> executions = jobOperator.getRunningExecutions("sampleJob");
jobOperator.stop(executions.iterator().next());
关闭不是立即的,因为无法强制立即关闭,特别是如果当前执行的是框架无法控制的开发人员代码,例如业务服务。但是,一旦将控件返回到框架中,就会将当前StepExecution
的状态设置为BatchStatus.STOPPED
,保存它,然后在完成之前对JobExecution
执行相同的操作。
# 终止作业
可以重新启动FAILED
的作业执行(如果Job
是可重启的)。状态为ABANDONED
的作业执行将不会被框架重新启动。在步骤执行中,ABANDONED
状态也被用于在重新启动的作业执行中将其标记为可跳过的:如果作业正在执行,并且遇到了在上一个失败的作业执行中标记ABANDONED
的步骤,它将进入下一个步骤(由作业流定义和步骤执行退出状态决定)。
如果进程死了("kill -9"
或服务器故障),作业当然不在运行,但是JobRepository
无法知道,因为在进程死之前没有人告诉它。你必须手动告诉它,你知道执行失败或应该被视为已中止(将其状态更改为FAILED
或ABANDONED
)-这是一个业务决策,没有办法使其自动化。如果状态不是可重启的,或者你知道重新启动数据是有效的,则仅将状态更改为FAILED
。 Spring batch adminJobService
中有一个实用程序来中止作业执行。