Spring Batch Remote Partitioning(远程分区)简介
来源:互联网 发布:自动发卡对接淘宝 编辑:程序博客网 时间:2024/06/02 02:11
Spring Batch远程分区简介
- Spring Batch远程分区简介
- 关于Spring Batch Remote Partitioning
- 实现Spring Batch Remote Partitioning功能
- 1 通过官网参考文档的方法实现远程分区
- 2 改进的远程分区实现方法
- 完整代码
写此博客的缘由:
题外话:暑假期间实习参与了公司一个SaaS项目,由于是一个新立的项目,因此使用较多的开源框架。在这两个月期间,接触了微服务框架、定时任务调度Quartz、Spring Batch批处理等多个开源框架。由于在某些任务中需要进行批处理作业,使用了Spring batch批处理框架。接触Spring Batch重点学习了Spring Batch Remote Partitioning,完成实现相关功能的Demo。首先,博主本来对java不是太熟悉的,刚来公司的时候连maven都没有用过,更不用说什么微服务、spring框架等,很感谢实习导师教我很多入门的基础,后来自己能够自主学习并参与项目工作。
在学习Spring Batch远程分区的过程中,有一点就是这方面的相关资料或者分享的学习经验很少,最主要的资料就是官方的参考文档,但是参考文档中对Remote Partitioning的介绍也是相当简单,对于初学Spring Batch的人来说很不友好。写这篇博客,总结了自己学习Spring Batch远程分区功能的内容,也希望能够方便大家对Spring Batch远程分区功能有更好的了解。
说明:(1)本博客是针对Spring Batch Remote Partitioning的介绍,读者需要有Spring Batch的基础知识,安装好eclipse、maven、MySQL等;(2)对于想要入门Spring Batch的初学者,推荐阅读刘相的《Spring Batch批处理框架》一书,博客中讲解的例子也是以书中的例子为基础。书中例子的源码地址:https://github.com/jxtaliu/SpringBatchSample 。(3)本文主要关注如何实现远程分区功能,其关键点在于如何通过消息队列实现此功能,具体分区功能请参考《Spring Batch批处理框架》一书。
1. 关于Spring Batch Remote Partitioning
Spring batch是一个轻量级的、完善的批处理框架,对于大数据量和高性能的批处理任务,Spring Batch提供不少高级功能和特性来支持,比如并行step、多线程step、分区step、远程step等功能。其中,远程分区功能是分区step和远程step两者的结合,对于处理大数据量的批处理任务有着重要的作用。这里的远程功能需要使用消息队列接受和发送信息,主要通过Spring Integration实现。所以为了能够了解Remote Partitioning功能,除了Spring Batch,也需要接触到Spring Integration的相关知识。如下面Spring Batch Remote Partitioning的示意图,Master对任务进行分区,把各个分区后的任务交给多个Slave节点执行:
2. 实现Spring Batch Remote Partitioning功能
本节会详细介绍如何配置xml文件实现远程分区功能。这里通过例子详解Spring Batch远程分区功能的实现,以《Spring Batch批处理框架》书中的关于Remote Partitioning的例子为基础。读者们可以从作者的github中下载源代码。
关于书中实现Remote Partitioning的例子不再详细说明,读者有兴趣的话可以尝试运行例子的源代码。但这里需要说明的是,书中的远程分区例子并不完全正确。对于书中例子的配置方法,Master把分区后的任务信息发送到了Slave节点上,Slave收到信息后会执行任务,只是Master不会接收Slave完成任务的信息。所以,当Slave执行任务出错的时候,Master却依旧显示任务completed。
这个例子的远程分区处理如下图所示(来自《Spring Batch批处理框架》书中):
该例子的任务为读取三个文件里的账单记录,并把记录写入数据库中。处理过程为:(1)对任务进行分区,一个文件作为一个分区任务;(2)通过消息队列把文件信息发送给各个Slave节点;(3)Slave收到信息后,处理对应的文件,把记录写入数据库,并返回任务完成的信息;(4)Master收到信息后,结束任务。
所以接下来会给出以不同的方式实现上面远程分区例子,首先是通过官方参考文档的方法实现远程分区,接着会根据我们的需求给出改进的方式实现远程分区。
2.1 通过官网参考文档的方法实现远程分区
官方参考文档地址:https://docs.spring.io/spring-batch/trunk/reference/html/springBatchIntegration.html。参考文档中提供了实现远程分区的方法,但由于该文档写的过于简洁,并且把Master与Slave的配置混在一起,对于初学者来说比较难理解。所以,这里会详细介绍文档的方法,并会清晰地区分Master与Slave的配置,方便用于分别配置为Master、Slave两种不同的程序。
在该例子中,我们使用ActiveMQ作为消息中间件(其它支持jms的MQ都没问题)。读者可以在官网下载ActiveMQ并在本地启动,在 http://localhost:8161/admin 可以观察远程分区发送消息的信息统计。
下图给出了该远程分区方法使用的jms消息队列的示意图:
具体的过程如下:
1. 首先,Master对批处理任务job中需要远程分区执行的step进行分区,并将分区后的多条任务消息通过MasterRequestChannel发送到消息中间件的RequestQueue中,并开始监听ReplystQueue;
2. 多个Slave节点相互竞争从RequestQueue获取任务消息,并在收到相应的消息后开始执行任务;
3. Slave完成任务后,把任务完成的信息发送到消息中间件的ReplystQueue中;
4. Master节点收到各个Slave节点完成任务的信息,并把信息放入消息整合器(aggregator)中,统计是否所有分区的任务已经完成,最后判断该step是否成功完成。
为了实现上面的功能,配置文件: job-partition-remote-MasterSlave.xml
将该xml文件保存在《Spring Batch批处理框架》书中的源代码文件夹src\main\resources\ch11\ 中。
(1)修改源代码maven项目pom文件的jar包版本
由于书中源代码使用的版本比较低,为了能够使用新版本的功能,我们需要在pom文件中改为更高的版本。这里的例子使用如下的版本:
<properties> <spring.version>4.3.4.RELEASE</spring.version> <spring.batch.version>3.0.7.RELEASE</spring.batch.version> <spring.integration.version>4.3.4.RELEASE</spring.integration.version></properties>
其中: spring.version、spring.batch.version、spring.integration.version分别表示所有groupId为org.springframework、org.springframework.batch、org.springframework.integration的jar包版本。
(2)配置xml文件中的命名空间
<bean:beans xmlns="http://www.springframework.org/schema/batch" xmlns:bean="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:task="http://www.springframework.org/schema/task" xmlns:int="http://www.springframework.org/schema/integration" xmlns:int-jms="http://www.springframework.org/schema/integration/jms" xmlns:jms="http://www.springframework.org/schema/jms" xmlns:amq="http://activemq.apache.org/schema/core" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task.xsd http://www.springframework.org/schema/batch http://www.springframework.org/schema/batch/spring-batch.xsd http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration-4.3.xsd http://www.springframework.org/schema/integration/jms http://www.springframework.org/schema/integration/jms/spring-integration-jms-4.3.xsd http://www.springframework.org/schema/jms http://www.springframework.org/schema/jms/spring-jms.xsd http://activemq.apache.org/schema/core http://activemq.apache.org/schema/core/activemq-core.xsd">
注意:在命名空间中,spring-integration-jms和spring-integration的xsd文件需要对应于我们在pom文件中配置的版本,否则容易编译错误。
(3)配置连接的ActiveMQ服务器
<amq:connectionFactory id="connectionFactory" brokerURL="tcp://localhost:61616" />
(4)配置xml中的Master部分
- Master的Job、partitioner与partitionHandler
<job id="partitionRemoteJob"> <step id="partitionRemoteStep"> <partition partitioner="partitioner" handler="partitionHandler" /> </step></job><bean:bean id="partitioner" class="org.springframework.batch.core.partition.support.MultiResourcePartitioner"> <bean:property name="keyName" value="fileName"/> <bean:property name="resources" value="classpath:/ch11/data/*.csv"/></bean:bean><bean:bean id="partitionHandler" class="org.springframework.batch.integration.partition.MessageChannelPartitionHandler"> <bean:property name="messagingOperations"> <bean:bean class="org.springframework.integration.core.MessagingTemplate"> <bean:property name="defaultChannel" ref="MasterRequestChannel" /> <bean:property name="receiveTimeout" value="30000" /> </bean:bean> </bean:property> <bean:property name="replyChannel" ref="AggregatedChannel"/> <bean:property name="stepName" value="remoteStep" /> <bean:property name="gridSize" value="3" /></bean:bean>
在需要远程分区执行的step中,配置partition中使用的partitioner和handler。partitioner是自己定义的分区规则,这里使用书中的规则。
关于partitionHandler的配置:
defaultChannel: Master发送任务信息的Channel;
receiveTimeout: 接受Slave返回信息超时时间,超时直接返回任务失败。
replyChannel: 接受Slave返回信息的Channel,注意这是一个AggregatedChannel。因为Master需要把所有Slave返回的任务信息整合在一起,才能判断任务是否成功完成。
stepName: 需要在Slave上远程执行的step,Slave上需要有对应的step。
gridSize: 分区的个数,即把任务拆分为多少份。
- Master jms配置
配置Master使用的channel: MasterRequestChannel和MasterReplyChannel
<int:channel id="MasterRequestChannel"> <int:dispatcher task-executor="RequestPublishExecutor"/></int:channel><task:executor id="RequestPublishExecutor" pool-size="5-10" queue-capacity="0"/><int:channel id="MasteReplyChannel"/>
MasterRequestChannel使用了task-executor,用多线程加快分区任务的分发速度。
配置outbound-channel-adapter和message-driven-channel-adapter:
<int-jms:outbound-channel-adapter connection-factory="connectionFactory" destination-name="RequestQueue" channel="MasterRequestChannel"/><int-jms:message-driven-channel-adapter connection-factory="connectionFactory" destination-name="ReplyQueue" channel="MasterReplyChannel"/>
分别使用outbound-channel-adapter和message-driven-channel-adapter向消息中间件发送和接受消息。其中,destination-name为消息中间件对应的队列名称。Master向RequestQueue发送消息,接受ReplyQueue的消息。
配置消息整合器
<int:channel id="AggregatedChannel"> <int:queue/></int:channel><int:aggregator ref="partitionHandler" input-channel="MasterReplyChannel" output-channel="AggregatedChannel"/>
当Master收到多个Slave发送回来的信息,会通过消息整合器(aggregator)放入AggregatedChannel中,所以AggregatedChannel需要使用队列queue。当接收完所有Slave完成任务的信息,Master才会判断任务成功完成。
(5)配置xml的Slave部分
- Slave jms配置
配置Slave使用的channel:SlaveRequestChannel和SlaveReplyChannel
<int:channel id="SlaveRequestChannel"/><int:channel id="SlaveReplyChannel"/>
配置outbound-channel-adapter和message-driven-channel-adapter:
<int-jms:message-driven-channel-adapter connection-factory="connectionFactory" destination-name="RequestQueue" channel="SlaveRequestChannel"/><int-jms:outbound-channel-adapter connection-factory="connectionFactory" destination-name="ReplyQueue" channel="SlaveReplyChannel"/>
Slave接受RequestQueue的消息,向ReplyQueue发送消息。
配置service-activator
<int:service-activator ref="stepExecutionRequestHandler" input-channel="SlaveRequestChannel" output-channel="SlaveReplyChannel"/>
Slave从SlaveRequestChannel获取信息,并使用stepExecutionRequestHandler处理信息,最后向SlaveReplyChannel返回结果。
配置Slave执行的step
<step id="remoteStep"> <tasklet> <chunk reader="flatFileItemReader" writer="jdbcItemWriter" commit-interval="10"/> <listeners> <listener ref="partitionItemReadListener"></listener> </listeners> </tasklet></step> <bean:bean id="stepExecutionRequestHandler" class="org.springframework.batch.integration.partition.StepExecutionRequestHandler"> <bean:property name="jobExplorer" ref="jobExplorer"/> <bean:property name="stepLocator" ref="stepLocator"/></bean:bean>
注意: 这里step的id必须与Master中partitionHandler的stepName一致才能正常执行。
其余的flatFileItemReader、jdbcItemWriter、jobExplorer、stepLocator等等于原来源代码中的保持一致,这里不再说明。
根据上面配置完job-partition-remote-MasterSlave.xml后,根据书的源代码src\test\java\test\com\juxtapose\example\ch11中的例子自己写一个JobLaunch.java的文件就可以运行了。
因为这里已经清晰给出了Master和Slave的配置。所以读者很容易自己分别写出job-partition-remote-Master.xml和job-partition-remote-Slave.xml的配置文件,即使用不同的进程作为Master和Slave。(先启动一个或多个Slave,再启动Master分发任务)。读者可以自己尝试去实现。
2.2 改进的远程分区实现方法
上节提到官方参考文档使用的方法实现远程分区是可以正常执行的,但在一种情况下会出现问题,就是复用ActiveMQ的消息队列Queue。例如,上面的例子,如果同时启动两个以上的Master,读者会发现有Master显示任务失败,但实际上Slave已经完成所有的任务了。 为什么会有上面的情况呢?这是因为消息队列Queue中的信息只能被消费一次。当有多个Master同时监听ActiveMQ的ReplyQueue,Slave发送过来的信息只能被其中一个Master接收,所以Master就没有办法收到自己想要接受的信息了。
如何解决这个问题呢?第一种方法是使用多对不同的Queue,但是这并非好的方法。因为当我们需要执行不同的远程分区任务比较多,一般批处理任务都是定时每天或者每月执行的,每个任务使用不同的一对Queue,这是对资源的浪费。所以项目组希望能够找出实现消息队列复用的方式。
那么我们需要寻求另一种解决方法。远程分区的方法本质上是Spring Batch和Spring Integration的两者结合,当时为了解决这个问题,于是就开始学习Spring Integration方面关于jms的知识,也在那里找到的答案。官方参考文档地址:https://docs.spring.io/spring-integration/docs/4.3.12.RELEASE/reference/html/jms.html
为了实现消息队列复用,本质上是利用jms中的消息选择器(message selector)。下面给出了该远程分区方法使用的jms消息队列的示意图:
如上图所示,在Master和Slave上都取消了使用outbound-channel-adapter和message-driven-channel-adapter,改为了分别使用outbound-gateway和inbound-gateway。
- 配置Master的outbound-gateway
方式一:使用reply-listener
<int-jms:outbound-gateway connection-factory="connectionFactory" correlation-key="JMSCorrelationID" request-channel="MasterRequestChannel" request-destination-name="RequestQueue" receive-timeout="30000" reply-channel="MasterReplyChannel" reply-destination-name="ReplyQueue" async="true"> <int-jms:reply-listener /></int-jms:outbound-gateway>
correlation-key: 使Master发送出去的jms消息带有correlationId,并且在接受S回复信息时,会让消息中间件通过消息选择器进行筛选,只有带有与原来发送消息一致的correlationId的消息才会被接受。该值设置为JMSCorrelationID即可正常使用。
async: (注意该属性只有4.3以上的版本支持)是否使用异步的方式发送消息。如果设置为false,发送请求的线程在发送完消息后会挂起知道收到回复。当设置为true,发送请求的线程在发送完消息后会被释放,把监听回复的任务交给reply-listener处理,可以释放线程资源。
方式二:使用idle-reply-listener
<int-jms:outbound-gateway connection-factory="connectionFactory" correlation-key="JMSCorrelationID" request-channel="MasterRequestChannel" request-destination-name="RequestQueue" receive-timeout="30000" reply-channel="MasterReplyChannel" reply-destination-name="ReplyQueue" idle-reply-listener-timeout="5000"></int-jms:outbound-gateway>
注意:idle-reply-listener是从4.2版本开始支持。
首先reply-listener的生命周期是与gateway一致的,所以使用reply-listener会到时Master一致在监听replyQueue。在已经收到回复的情况下,reply-listener此时就变成idle的状态,除了占用系统资源,对于broker来说也是一种负担。而idle-reply-listener只有在被需要的时候才会启动,并且在接受完信息后等待一段时间(idle-reply-listener-timeout)会自动释放。
- 配置Slave的inbound-gateway
<int-jms:inbound-gateway connection-factory="connectionFactory" correlation-key="JMSCorrelationID" request-channel="SlaveRequestChannel" request-destination-name="RequestQueue" reply-channel="SlaveReplyChannel" default-reply-queue-name="ReplyQueue"/>
在Slave中同时把correlation-key的值设置为JMSCorrelationID,那么Slave在接受到带有correlationId的消息,回复的时候也会把该correlationId复制到回复的消息里,从而使得Master能偶收到自己对应的消息。
完整代码
job-partition-remote-MasterSlave.xml
<?xml version="1.0" encoding="UTF-8"?><bean:beans xmlns="http://www.springframework.org/schema/batch" xmlns:bean="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:task="http://www.springframework.org/schema/task" xmlns:int="http://www.springframework.org/schema/integration" xmlns:int-jms="http://www.springframework.org/schema/integration/jms" xmlns:jms="http://www.springframework.org/schema/jms" xmlns:amq="http://activemq.apache.org/schema/core" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/batch http://www.springframework.org/schema/batch/spring-batch.xsd http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task.xsd http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration-4.3.xsd http://www.springframework.org/schema/integration/jms http://www.springframework.org/schema/integration/jms/spring-integration-jms-4.3.xsd http://www.springframework.org/schema/jms http://www.springframework.org/schema/jms/spring-jms.xsd http://activemq.apache.org/schema/core http://activemq.apache.org/schema/core/activemq-core.xsd"> <bean:import resource="classpath:ch11/job-context.xml"/> <bean:import resource="classpath:ch11/job-context-db.xml"/> <!-- Master job --> <job id="partitionRemoteJob"> <step id="partitionRemoteStep"> <partition partitioner="partitioner" handler="partitionHandler" /> </step> </job> <bean:bean id="partitioner" class="org.springframework.batch.core.partition.support.MultiResourcePartitioner"> <bean:property name="keyName" value="fileName"/> <bean:property name="resources" value="classpath:/ch11/data/*.csv"/> </bean:bean> <bean:bean id="partitionHandler" class="org.springframework.batch.integration.partition.MessageChannelPartitionHandler"> <bean:property name="messagingOperations"> <bean:bean class="org.springframework.integration.core.MessagingTemplate"> <bean:property name="defaultChannel" ref="MasterRequestChannel" /> <bean:property name="receiveTimeout" value="30000" /> </bean:bean> </bean:property> <bean:property name="replyChannel" ref="AggregatedChannel"/> <bean:property name="stepName" value="remoteStep" /> <bean:property name="gridSize" value="3" /> </bean:bean> <!-- Master jms --> <int:channel id="MasterRequestChannel"> <int:dispatcher task-executor="RequestPublishExecutor"/> </int:channel> <task:executor id="RequestPublishExecutor" pool-size="5-10" queue-capacity="0"/><!-- <int-jms:outbound-channel-adapter connection-factory="connectionFactory" destination-name="RequestQueue" channel="MasterRequestChannel"/> --> <int:channel id="MasterReplyChannel"/><!-- <int-jms:message-driven-channel-adapter connection-factory="connectionFactory" destination-name="ReplyQueue" channel="MasterReplyChannel"/> --> <int-jms:outbound-gateway connection-factory="connectionFactory" correlation-key="JMSCorrelationID" request-channel="MasterRequestChannel" request-destination-name="RequestQueue" receive-timeout="30000" reply-channel="MasterReplyChannel" reply-destination-name="ReplyQueue" async="true"> <int-jms:reply-listener /> </int-jms:outbound-gateway> <int:channel id="AggregatedChannel"> <int:queue/> </int:channel> <int:aggregator ref="partitionHandler" input-channel="MasterReplyChannel" output-channel="AggregatedChannel"/> <!-- Slave jms --> <int:channel id="SlaveRequestChannel"/><!-- <int-jms:message-driven-channel-adapter connection-factory="connectionFactory" destination-name="RequestQueue" channel="SlaveRequestChannel"/> --> <int:channel id="SlaveReplyChannel"/><!-- <int-jms:outbound-channel-adapter connection-factory="connectionFactory" destination-name="ReplyQueue" channel="SlaveReplyChannel"/> --> <int-jms:inbound-gateway connection-factory="connectionFactory" correlation-key="JMSCorrelationID" request-channel="SlaveRequestChannel" request-destination-name="RequestQueue" reply-channel="SlaveReplyChannel" default-reply-queue-name="ReplyQueue"/> <int:service-activator ref="stepExecutionRequestHandler" input-channel="SlaveRequestChannel" output-channel="SlaveReplyChannel"/> <!-- RemoteStep for Slave --> <step id="remoteStep"> <tasklet> <chunk reader="flatFileItemReader" writer="jdbcItemWriter" commit-interval="10"/> <listeners> <listener ref="partitionItemReadListener"></listener> </listeners> </tasklet> </step> <bean:bean id="stepExecutionRequestHandler" class="org.springframework.batch.integration.partition.StepExecutionRequestHandler"> <bean:property name="jobExplorer" ref="jobExplorer"/> <bean:property name="stepLocator" ref="stepLocator"/> </bean:bean> <amq:broker useJmx="false" persistent="false" schedulerSupport="false"> <amq:transportConnectors> <amq:transportConnector uri="tcp://localhost:61616"/> </amq:transportConnectors> </amq:broker> <amq:connectionFactory id="connectionFactory" brokerURL="tcp://localhost:61616" trustAllPackages="true"/> <bean:bean class="org.springframework.beans.factory.config.CustomScopeConfigurer"> <bean:property name="scopes"> <bean:map> <bean:entry key="thread"> <bean:bean class="org.springframework.context.support.SimpleThreadScope" /> </bean:entry> </bean:map> </bean:property> </bean:bean> <bean:bean id="flatFileItemReader" scope="step" class="org.springframework.batch.item.file.FlatFileItemReader"> <bean:property name="resource" value="#{stepExecutionContext[fileName]}"/> <bean:property name="lineMapper" ref="lineMapper" /> </bean:bean> <bean:bean id="lineMapper" class="org.springframework.batch.item.file.mapping.DefaultLineMapper" > <bean:property name="lineTokenizer" ref="delimitedLineTokenizer" /> <bean:property name="fieldSetMapper" ref="creditBillFieldSetMapper"/> </bean:bean> <bean:bean id="delimitedLineTokenizer" class="org.springframework.batch.item.file.transform.DelimitedLineTokenizer"> <bean:property name="delimiter" value=","/> <bean:property name="names" value="id,accountID,name,amount,date,address" /> </bean:bean> <bean:bean id="creditBillFieldSetMapper" class="com.juxtapose.example.ch11.partition.CreditBillFieldSetMapper"> </bean:bean> <bean:bean id="jdbcItemWriter" class="org.springframework.batch.item.database.JdbcBatchItemWriter"> <bean:property name="dataSource" ref="dataSource"/> <bean:property name="sql" value="insert into t_destcredit (ID,ACCOUNTID,NAME,AMOUNT,DATE,ADDRESS) values (:id,:accountID,:name,:amount,:date,:address)"/> <bean:property name="itemSqlParameterSourceProvider"> <bean:bean class="org.springframework.batch.item.database.BeanPropertyItemSqlParameterSourceProvider"/> </bean:property> </bean:bean> <bean:bean id="creditBillProcessor" scope="step" class="com.juxtapose.example.ch11.partition.CreditBillProcessor"> </bean:bean> <bean:bean id="taskExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor"> <bean:property name="corePoolSize" value="5" /> <bean:property name="maxPoolSize" value="5" /> </bean:bean> <bean:bean id="partitionItemReadListener" class="com.juxtapose.example.ch11.partition.PartitionStepExecutionListener"> </bean:bean></bean:beans>
注意:在classpath:ch11/job-context-db.xml中,simpleJdbcTemplate可以注释掉,高版本不兼容。
- Spring Batch Remote Partitioning(远程分区)简介
- Spring Batch Partitioning example
- Spring Batch 简介(一)
- Spring Batch 简介
- Spring Batch 简介
- spring batch 1:简介
- Spring Batch 之 Spring Batch 简介(一)
- Spring Batch 之 Spring Batch 简介(一)
- Spring Batch之Spring Batch简介
- Spring Batch 之 Spring Batch 简介(一)
- Spring Batch 之 Spring Batch 简介(一)
- Spring Batch 之 Spring Batch 简介(一)
- Spring batch教程 之 spring batch简介
- Spring Batch 之 Spring Batch 简介
- Spring Batch 之 Spring Batch 简介
- Spring Batch 之 Spring Batch 简介
- Spring Batch学习之路- 简介(一)
- Spring Batch(一)
- 收藏网址
- luogu2341 [HAOI2006]受欢迎的牛
- MpAndroidChart坑之release发布版本动画不起作用
- Collection
- 在MAC中更改MySQL数据库密码
- Spring Batch Remote Partitioning(远程分区)简介
- 总结
- 前端学习(十六)元素的高度宽度决定
- 专用于SqlServer2005的高效分页存储过程(支持多字段任意排序,不要求排序字段唯一)
- LeetCode #475 Heaters
- java.io.IOException:FULL head
- 设计一个投入方案
- Gson解析问题一:按String读取0.0时,值为0的问题
- 石头剪刀布上线了