【译】Spring 官方教程:创建批处理服务

原文:Creating a Batch Service 译者:Mr.lzc 校对:lexburner

本指南将引导你完成创建基本的批处理驱动解决方案的过程。

你将构建什么

你将构建一个从CSV电子表格导入数据的服务,并使用自定义代码进行转换,并将最终结果存储在数据库中。

你需要准备什么

  • 大约15分钟
  • 一个自己喜欢的文本编辑器或者IDE
  • JDK 1.8 或以上版本
  • Gradle 2.3+ 或者 Maven 3.0+
  • 你也可以直接将代码导入到本地的IDE中:
  • Spring Tool Suite (STS)
  • IntelliJ IDEA

如何完成本指南

像大多数Spring入门指南一样,你可以从头开始,完成每一步,也可以绕过已经熟悉的基本设置步骤。不管采用哪种方式,最终都可以得到能够运行的代码。

如果是从零开始,则可以从使用Gradle构建项目小节开始。

如果想跳过基本的设置步骤,可以按照以下步骤执行:

  • 下载 并解压本指南相关的源文件,或者直接通过Git命令克隆到本地:
git clone https://github.com/spring-guides/gs-batch-processing.git
  • 进入到 gs-batch-processing/initial 目录
  • 直接跳到创建业务类小节。

当完成所有的编码以后,可以将你的代码与 gs-batch-processing/complete目录下的示例代码进行对比,以检查结果是否正确。

使用Gradle构建项目

首先需要设置一个基本的构建脚本。在使用Spring构建应用程序时,你可以使用任何自己喜欢的构建系统,这里准备了在使用Gradle和Maven构建项目时需要的代码。如果你对Gradle和Maven都不熟悉,可以参照使用Gradle构建Java项目或使用Maven构建Java项目。

创建目录结构

选择需要创建项目的目录,参照照以下目录结构创建子目录;如果使用的是UNIX/LINUX系统,可以使用 mkdir-p src/main/java/hello命令创建:

└── src
    └── main
        └── java
            └── hello

创建Gradle 的构建文件

下面是一个原始的Gradle build文件。

build.gradle

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:1.5.8.RELEASE")
    }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'
apply plugin: 'org.springframework.boot'

jar {
    baseName = 'gs-batch-processing'
    version =  '0.1.0'
}

repositories {
    mavenCentral()
}

sourceCompatibility = 1.8
targetCompatibility = 1.8

dependencies {
    compile("org.springframework.boot:spring-boot-starter-batch")
    compile("org.hsqldb:hsqldb")
    testCompile("junit:junit")
}

Spring Boot gradle 插件 提供了很多便捷的特性:

  • 该插件可以把类路径下所有的jar打包成一个可以运行的“über-jar”,给程序的执行和传输带来很大的方便。
  • 该插件会自动搜索程序中的 publicstaticvoidmain() 方法,把它作为程序运行的入口。
  • 它还提供了一个内置的依赖解析器,可以自动调整版本号与 Spring Boot 的依赖相一致。你可以覆盖其中的任何一个版本,但是默认情况下它会使用Spring Boot自身版本集中的版本。

使用Maven构建项目

首先,设置基本的构建脚本。在使用Spring构建应用程序时,你可以使用任何自己喜欢的构建系统,在这里为你提供了使用Maven构建项目时需要的代码。如果你对Maven不熟悉,可以参照使用maven构建JAVA项目工程 。

创建目录结构

选择需要创建项目的目录,参照以下目录结构创建子目录;如果使用的是UNIX/LINUX系统,可以使用 mkdir-p src/main/java/hello 命令创建:

└── src
    └── main
        └── java
            └── hello

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.springframework</groupId>
    <artifactId>gs-batch-processing</artifactId>
    <version>0.1.0</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.8.RELEASE</version>
    </parent>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-batch</artifactId>
        </dependency>
        <dependency>
            <groupId>org.hsqldb</groupId>
            <artifactId>hsqldb</artifactId>
        </dependency>
    </dependencies>


    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

Spring Boot maven 插件 提供了很多便捷的特性:

  • 该插件可以把类路径下所有的jar打包成一个可以运行的“über-jar”,给程序的执行和传输带来很大的方便。
  • 该插件会自动搜索程序中的 publicstaticvoidmain() 方法,作为程序运行的入口。
  • 它还提供了一个内置的依赖解析器,可以自动调整版本号与 Spring Boot 的依赖相一致。你可以覆盖其中的任何一个版本,但是默认情况下它会使用Spring Boot自身版本集中的版本。

使用IDE构建项目

  • 在Spring Tool Suite中构建项目,请参照 Spring Tool Suite。
  • 在IntelliJ IDEA中构建项目,请参照IntelliJ IDEA。

业务数据

通常你的客户或业务分析师提供电子表格。在这种情况下,你可以做到这一点。

src/main/resources/sample-data.csv

Jill,Doe
Joe,Doe
Justin,Doe
Jane,Doe
John,Doe

此电子表格每行包含名字和姓氏,用逗号分隔。 这是一个相当常见的模式,正如你将看到的那样,Spring会处理开箱即用的情况。

接下来,你编写一个SQL脚本来创建一个表存储数据。

src/main/resources/schema-all.sql

DROP TABLE people IF EXISTS;

CREATE TABLE people  (
    person_id BIGINT IDENTITY NOT NULL PRIMARY KEY,
    first_name VARCHAR(20),
    last_name VARCHAR(20)
);

Spring Boot在启动过程中自动运行 schema-@@platform@@.sql-all是所有平台的默认值。

创建业务类

现在看到数据输入和输出的格式,你编写代码来表示一行数据。

src/main/java/hello/Person.java

package hello;

public class Person {
    private String lastName;
    private String firstName;

    public Person() {

    }

    public Person(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    @Override
    public String toString() {
        return "firstName: " + firstName + ", lastName: " + lastName;
    }

}

你可以通过构造函数或通过设置属性来实例化具有名字和姓氏的Person类。

创建中间处理器

批处理中的一个常见范例是获取数据,转换数据,然后将其导出到其他位置。 在这里,你编写一个简单的变换器,将名字转换为大写。

src/main/java/hello/PersonItemProcessor.java

package hello;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.springframework.batch.item.ItemProcessor;

public class PersonItemProcessor implements ItemProcessor<Person, Person> {

    private static final Logger log = LoggerFactory.getLogger(PersonItemProcessor.class);

    @Override
    public Person process(final Person person) throws Exception {
        final String firstName = person.getFirstName().toUpperCase();
        final String lastName = person.getLastName().toUpperCase();

        final Person transformedPerson = new Person(firstName, lastName);

        log.info("Converting (" + person + ") into (" + transformedPerson + ")");

        return transformedPerson;
    }

}

PersonItemProcessor实现Spring Batch的 ItemProcessor接口。这样可以方便地将代码连接到本指南中进一步定义的批处理作业中。根据接口,你会收到一个传入的 Person对象,然后将其转换为大写形式的 Person

不要求输入和输出类型相同。事实上,在读取一个数据源之后,有时应用程序的数据流需要不同的数据类型。

将批处理作业集中到一起

现在,你把实际的批处理作业集中到一起。Spring Batch提供了许多实用程序类,可以减少编写自定义代码的需要。取而代之,你可以专注于业务逻辑。

src/main/java/hello/BatchConfiguration.java

package hello;

import javax.sql.DataSource;

import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobExecutionListener;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.batch.item.database.BeanPropertyItemSqlParameterSourceProvider;
import org.springframework.batch.item.database.JdbcBatchItemWriter;
import org.springframework.batch.item.file.FlatFileItemReader;
import org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper;
import org.springframework.batch.item.file.mapping.DefaultLineMapper;
import org.springframework.batch.item.file.transform.DelimitedLineTokenizer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.jdbc.core.JdbcTemplate;

@Configuration
@EnableBatchProcessing
public class BatchConfiguration {

    @Autowired
    public JobBuilderFactory jobBuilderFactory;

    @Autowired
    public StepBuilderFactory stepBuilderFactory;

    @Autowired
    public DataSource dataSource;

    // tag::readerwriterprocessor[]
    @Bean
    public FlatFileItemReader<Person> reader() {
        FlatFileItemReader<Person> reader = new FlatFileItemReader<Person>();
        reader.setResource(new ClassPathResource("sample-data.csv"));
        reader.setLineMapper(new DefaultLineMapper<Person>() {{
            setLineTokenizer(new DelimitedLineTokenizer() {{
                setNames(new String[] { "firstName", "lastName" });
            }});
            setFieldSetMapper(new BeanWrapperFieldSetMapper<Person>() {{
                setTargetType(Person.class);
            }});
        }});
        return reader;
    }

    @Bean
    public PersonItemProcessor processor() {
        return new PersonItemProcessor();
    }

    @Bean
    public JdbcBatchItemWriter<Person> writer() {
        JdbcBatchItemWriter<Person> writer = new JdbcBatchItemWriter<Person>();
        writer.setItemSqlParameterSourceProvider(new BeanPropertyItemSqlParameterSourceProvider<Person>());
        writer.setSql("INSERT INTO people (first_name, last_name) VALUES (:firstName, :lastName)");
        writer.setDataSource(dataSource);
        return writer;
    }
    // end::readerwriterprocessor[]

    // tag::jobstep[]
    @Bean
    public Job importUserJob(JobCompletionNotificationListener listener) {
        return jobBuilderFactory.get("importUserJob")
                .incrementer(new RunIdIncrementer())
                .listener(listener)
                .flow(step1())
                .end()
                .build();
    }

    @Bean
    public Step step1() {
        return stepBuilderFactory.get("step1")
                .<Person, Person> chunk(10)
                .reader(reader())
                .processor(processor())
                .writer(writer())
                .build();
    }
    // end::jobstep[]
}

对于初学者, @EnableBatchProcessing注释添加了许多支持作业的关键beans,并节省了大量的工作。 此示例使用基于内存的数据库(由 @EnableBatchProcessing提供),这意味着完成后,数据就会消失。

分解如下:

src/main/java/hello/BatchConfiguration.java

@Bean
    public FlatFileItemReader<Person> reader() {
        FlatFileItemReader<Person> reader = new FlatFileItemReader<Person>();
        reader.setResource(new ClassPathResource("sample-data.csv"));
        reader.setLineMapper(new DefaultLineMapper<Person>() {{
            setLineTokenizer(new DelimitedLineTokenizer() {{
                setNames(new String[] { "firstName", "lastName" });
            }});
            setFieldSetMapper(new BeanWrapperFieldSetMapper<Person>() {{
                setTargetType(Person.class);
            }});
        }});
        return reader;
    }

    @Bean
    public PersonItemProcessor processor() {
        return new PersonItemProcessor();
    }

    @Bean
    public JdbcBatchItemWriter<Person> writer() {
        JdbcBatchItemWriter<Person> writer = new JdbcBatchItemWriter<Person>();
        writer.setItemSqlParameterSourceProvider(new BeanPropertyItemSqlParameterSourceProvider<Person>());
        writer.setSql("INSERT INTO people (first_name, last_name) VALUES (:firstName, :lastName)");
        writer.setDataSource(dataSource);
        return writer;
    }

第一个代码块定义了输入,处理器和输出。- reader()创建一个 ItemReader类。它查找 sample-data.csv文件,并将每一行的内容转换成一个 Person类。- processor()创建一个我们先前定义的 PersonItemProcessor的一个实例,用于转换大写数据。- write(DataSource)创建一个ItemWriter类。这个目标是针对JDBC,并自动获取由 @EnableBatchProcessing创建的dataSource的副本。它包括插入由Java bean属性驱动的单个 Person所需的SQL语句。

下一个重点是实际的工作配置。

src/main/java/hello/BatchConfiguration.java

@Bean
    public Job importUserJob(JobCompletionNotificationListener listener) {
        return jobBuilderFactory.get("importUserJob")
                .incrementer(new RunIdIncrementer())
                .listener(listener)
                .flow(step1())
                .end()
                .build();
    }

    @Bean
    public Step step1() {
        return stepBuilderFactory.get("step1")
                .<Person, Person> chunk(10)
                .reader(reader())
                .processor(processor())
                .writer(writer())
                .build();
    }

第一个方法定义了作业,第二个方法定义了一个步骤。作业是从步骤构建的,每个步骤都可以涉及读取器,处理器和写入器。

在此作业定义中,你需要一个增量器,因为作业使用数据库来维护执行状态。然后你列出每个步骤,其中该作业只有一步。作业结束后,Java API生成完美配置的作业。

在步骤定义中,你可以定义一次写入的数据量。在这种情况下,它最多可以写入十条记录。接下来,你使用前面的注入位配置读取器,处理器和写入器。

chunk()由前缀 <Person,Person>修饰,因为它是一个泛型方法。 这表示处理的每个“块”的输入和输出类型,并与 ItemReader<Person>ItemWriter<Person>排列。

src/main/java/hello/JobCompletionNotificationListener.java

package hello;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.springframework.batch.core.BatchStatus;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.listener.JobExecutionListenerSupport;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Component;

@Component
public class JobCompletionNotificationListener extends JobExecutionListenerSupport {

    private static final Logger log = LoggerFactory.getLogger(JobCompletionNotificationListener.class);

    private final JdbcTemplate jdbcTemplate;

    @Autowired
    public JobCompletionNotificationListener(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @Override
    public void afterJob(JobExecution jobExecution) {
        if(jobExecution.getStatus() == BatchStatus.COMPLETED) {
            log.info("!!! JOB FINISHED! Time to verify the results");

            List<Person> results = jdbcTemplate.query("SELECT first_name, last_name FROM people", new RowMapper<Person>() {
                @Override
                public Person mapRow(ResultSet rs, int row) throws SQLException {
                    return new Person(rs.getString(1), rs.getString(2));
                }
            });

            for (Person person : results) {
                log.info("Found <" + person + "> in the database.");
            }

        }
    }
}

此代码侦听作业是否为 BatchStatus.COMPLETED状态时,然后使用 JdbcTemplate检查结果。

使应用程序可以执行

虽然批处理可以嵌入到Web应用程序和WAR文件中,但下面演示的更简单的方法创建了一个独立的应用程序。 你将所有内容都包装在一个可执行的JAR文件中,由一个好的旧的Java main()方法驱动。

src/main/java/hello/Application.java

package hello;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {

    public static void main(String[] args) throws Exception {
        SpringApplication.run(Application.class, args);
    }
}

@SpringBootApplication 是一个方便的注解,它会自动添加以下所有内容:

  • @Configuration将该类标记为应用程序上下文中bean定义的源。
  • @EnableAutoConfiguration指示Spring Boot根据类路径设置,其他bean和各种属性设置开始添加bean。
  • 通常,你将为Spring MVC 应用程序添加 @EnableWebMvc注解,但是当Spring Boot在类路径中发现spring-webmvc时会自动添加该注解。通过添加该注解将应用程序标记为Web应用程序,并进行一些关键操作,比如设置 DispatcherServlet
  • @ComponentScan通知Spring在 hello包中查找其他组件,配置和服务,允许Spring扫描到控制器。

main()方法使用Spring Boot的 SpringApplication.run()方法启动应用程序。你注意到我们没有写过一行XML代码吗?而且也没有web.xml配置文件。此Web应用程序是100%纯Java编写的,无需再配置其他基础设施。

为了演示的目的,创建一个 JdbcTemplate,查询数据库,并打印出批处理作业插入的人的名字的代码。

构建可执行的JAR

程序创建好以后,可以使用Gradle或Maven从命令行运行。或者,也可以将所有必需的依赖项,类和资源打包成一个可执行的JAR文件,并运行该文件。这种方式使得在整个开发生命周期中,应用程序可以轻松地发布,更新版本和部署服务。

如果你使用的是Gradle,则可以使用 ./gradlew bootRun运行应用程序。或者使用 ./gradlew build来构建JAR文件。然后运行这个JAR文件:

java -jar build/libs/gs-soap-service-0.1.0.jar

如果你使用的是Maven,可以使用 ./mvnw spring-boot:run运行应用程序,或者使用 ./mvnw cleanpackage来构建JAR文件。然后运行这个JAR文件:

java -jar target/gs-soap-service-0.1.0.jar

上面的过程创建了一个可运行的JAR。也可以选择构建一个经典的WAR文件。

上述操作完成后,将会看到有日志信息输出,服务程序将会在几秒内启动并运行。

该作业为每个被转换成大写的人的信息打印出一行。作业运行后,你还可以查看查询数据库的输出。

Converting (firstName: Jill, lastName: Doe) into (firstName: JILL, lastName: DOE)
Converting (firstName: Joe, lastName: Doe) into (firstName: JOE, lastName: DOE)
Converting (firstName: Justin, lastName: Doe) into (firstName: JUSTIN, lastName: DOE)
Converting (firstName: Jane, lastName: Doe) into (firstName: JANE, lastName: DOE)
Converting (firstName: John, lastName: Doe) into (firstName: JOHN, lastName: DOE)
Found <firstName: JILL, lastName: DOE> in the database.
Found <firstName: JOE, lastName: DOE> in the database.
Found <firstName: JUSTIN, lastName: DOE> in the database.
Found <firstName: JANE, lastName: DOE> in the database.
Found <firstName: JOHN, lastName: DOE> in the database.

总结

恭喜!你构建了一个批处理作业,从电子表格中获取数据,对其进行处理,并将其写入数据库。

了解更多

以下指南也可能有帮助:

  • Building an Application with Spring Boot
  • Accessing Data with GemFire
  • Accessing Data with JPA
  • Accessing Data with MongoDB
  • Accessing data with MySQL

想写一个新的指南或贡献一个现有的?查看我们的贡献指南。

本文由spring4all.com翻译小分队创作,采用知识共享-署名-非商业性使用-相同方式共享 4.0 国际 许可 协议进行许可。

加入

加入翻译,复制打开下面地址

https://github.com/SpringForAll/spring-guides-translation

原文发布于微信公众号 - 程序猿DD(didispace)

原文发表时间:2017-12-20

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏coderhuo

虚拟内存探究 -- 第三篇:一步一步画虚拟内存图

这是虚拟内存系列文章的第三篇。 前面我们提到在进程的虚拟内存中可以找到哪些东西,以及在哪里去找。 本文我们将通过打印程序中不同元素内存地址的方式,一步一步细...

27640
来自专栏杨建荣的学习笔记

一些“简单”的linux命令(r2笔记46天)

有些linux命令看起来极其简单,只包含2个字符,但确有很强的功能性。看起来还是有些陌生的命令,不过在工作中别忘记它们的存在。 ab 这条命令式做为性能测试所...

31980
来自专栏xdecode

Spring MVC注解式开发

MVC注解式开发即处理器基于注解的类开发, 对于每一个定义的处理器, 无需在xml中注册. 只需在代码中通过对类与方法的注解, 即可完成注册. 定义处理器 @C...

30280
来自专栏阿杜的世界

Restful: Spring Boot with Mongodb

继续之前的dailyReport项目,今天的任务是选择mongogdb作为持久化存储。

9820
来自专栏黑泽君的专栏

day63_SpringMVC学习笔记_01

(1)使用eclipse,创建一个动态的web工程   其中Dynamic web module version版本选择 2.5,这样兼容性好一些;   Def...

10610
来自专栏Gaussic

Spring MVC绑定 List 对象参数 原

       最近做的一个小小的项目碰上了如何用 post 传递一整个 list 的问题,在解决这个问题的同时,也顺带升级一下 Spring 的版本,并精简一下...

24510
来自专栏hbbliyong

Spring Boot搭建Web项目常用功能

     首先要弄清楚为什么要包装统一结构结果数据,这是因为当任意的ajax请求超时或者越权操作时,系统能返回统一的错误信息给到前端,前端通过封装统一的ajax...

36720
来自专栏java工会

Java 必看的 Spring 知识汇总!有比这更全的算我输!

22820
来自专栏机器学习从入门到成神

临界区、互斥量、信号量

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/sinat_35512245/articl...

27420
来自专栏JackieZheng

学习SpringMVC——说说视图解析器

  各位前排的,后排的,都不要走,咱趁热打铁,就这一股劲我们今天来说说spring mvc的视图解析器(不要抢,都有位子~~~)   相信大家在昨天那篇如何获取...

273100

扫码关注云+社区

领取腾讯云代金券