本篇继续学习在企业开发过程中经常会使用到的一些功能,主要是批处理、Swagger2和数据校验等。

批处理

Spring Batch简介

Spring Batch是一个开源的、全面的、轻量级的批处理框架,通过Spring Batch可以实现强大的批处理应用程序的开发。Spring Batch还提供记录/跟踪、事务管理、作业处理统计、作业重启以及资源管理等功能。Spring Batch结合定时任务可以发挥更大的作用。
Spring Batch提供了ItemReaderItemProcessorItemWriter来完成数据的读取、处理以及写出操作,并且可以将批处理的执行状态持久化到数据库中。接下来通过一个简单的数据复制来学习如何在SpringBoot中如何使用Spring Batch。

整合SpringBoot

案例说明,现在有一个data.csv文件,该文件保存了5条用户数据,现要求通过批处理框架来读取data.csv文件,进而将其插入到数据表中。data.csv文件的内容为:

1
2
3
4
5
6
id username address gender
1 张三 深圳 男
2 李四 广州 男
3 王五 广州 男
4 赵六 北京 女
5 钱七 上海 男

第一步,新建项目,并添加依赖。使用spring Initializr构建工具构建一个SpringBoot的Web应用,名称为springbatchdatas,然后添加如下依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<!--添加web依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--添加batch依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-batch</artifactId>
</dependency>
<!--添加数据库连接依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!--添加数据库驱动依赖-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--添加数据库连接池依赖-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.25</version>
</dependency>
<!--添加lombok依赖-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>

可以看到上面不仅添加了spring batch依赖,还有数据库相关依赖。

需求中也明确是将数据插入到数据库,因此添加数据库相关依赖就是为了将批处理的执行状态持久化到数据库中。新建batch数据库,并使用如下命令来创建数据表user:

1
2
3
4
5
6
7
8
use batch;
drop table if exists user;
create table user(
id int(8) primary key auto_increment,
username varchar(64) not null,
address varchar(64) not null,
gender varchar(64) not null
)engine=innodb,charset=utf8;

之后,在配置文件application.properties中新增数据库连接基本信息:

1
2
3
4
5
6
7
8
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/batch?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=1234
spring.datasource.schema=classpath:/org/springframework/batch/core/schema-mysql.sql

spring.batch.initialize-schema=always
spring.batch.job.enabled=false

前面4行配置是数据库的基本配置,这里就不再赘述。第5行配置是项目启动时创建数据表的SQL脚本,该脚本由Spring Batch提供。第6行配置表示在项目启动时执行建表SQL。第7行配置表示禁止Spring Batch自动执行。在SpringBoot中,默认情况下当项目启动时就会执行配置好的批处理操作,添加了第7行的配置后则不会自动执行,而需要用户手动触发执行,例如发送一个请求,在Controller的接口中触发批处理的执行。

接下来在项目启动类上添加@EnableBatchProcessing注解用于开启Spring Batch的支持,相应的代码为:

1
2
3
4
5
6
7
@SpringBootApplication
@EnableBatchProcessing
public class SpringbatchdatasApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbatchdatasApplication.class, args);
}
}

然后创建实体类User。新建一个pojo包,并在里面创建User类,里面的代码为:

1
2
3
4
5
6
7
8
9
@Data
public class User {
private Integer id;
private String username;
private String address;
private String gender;
//注意此处必须提供无参的构造方法
public User(){};
}

接下来配置批处理,新建一个config包,并在里面创建CsvBatchJobConfig类,里面的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@Configuration
public class CsvBatchJobConfig {
@Autowired
JobBuilderFactory jobBuilderFactory;
@Autowired
StepBuilderFactory stepBuilderFactory;
@Autowired
DataSource dataSource;
@Bean
@StepScope
FlatFileItemReader<User> itemReader(){
FlatFileItemReader<User> reader = new FlatFileItemReader<>();
reader.setLinesToSkip(1);
reader.setResource(new ClassPathResource("data.csv"));
reader.setLineMapper(new DefaultLineMapper<User>(){{
setLineTokenizer(new DelimitedLineTokenizer(){{
setNames("id","username","address","gender");
setDelimiter("\t");
}});
setFieldSetMapper(new BeanWrapperFieldSetMapper<User>(){{
setTargetType(User.class);
}});
}});
return reader;
}
@Bean
JdbcBatchItemWriter jdbcBatchItemWriter(){
JdbcBatchItemWriter writer = new JdbcBatchItemWriter();
writer.setDataSource(dataSource);
writer.setSql("insert into user(id,username,address,gender)"
+"values(:id,:username,:address,:gender)");
writer.setItemSqlParameterSourceProvider(
new BeanPropertyItemSqlParameterSourceProvider());
return writer;
}
@Bean
Step csvStep(){
return stepBuilderFactory.get("csvStep")
.<User,User>chunk(2)
.reader(itemReader())
.writer(jdbcBatchItemWriter())
.build();
}
@Bean
Job csvJob(){
return jobBuilderFactory.get("csvJob")
.start(csvStep())
.build();
}
}

解释一下上述代码的含义:

  • 创建CsvBatchJobConfig类用于进行Spring Batch的配置,同时注入JobBuilderFactoryStepBuilderFactory
    DataSource备用,其中JobBuilderFactory用来构建Job,StepBuilderFactory用来构建Step,而DataSource则用来支持持久化操作,这里使用的持久化方案是Spring-Jdbc。
  • itemReader方法用来配置一个ItemReader,Spring Batch提供了一些常用的ItemReader,如JdbcPagingItemReader用来读取数据库中的数据,StaxEventItemReader用来读取XML数据,本例子中的FlatFileItemReader则是一个加载普通文件的ItemReader。在FlatFileItemReader的配置过程中,由于data.csv文件第一行是标题,因此通过setLinesToSkip(1)方法设置跳过一行,然后通过setResource方法配置data.csv文件的位置,笔者的data.csv文件放在classpath目录下,然后通过setLineMapper方法设置每一行的数据信息,setNames方法配置了data.csv文件一共有4行,分别是id、username、address和gender,setDelimiter方法则是配置列与列之间的间隔符(将通过间隔符对每一行的数据进行切分),最后设置要映射的实体类属性即可。
  • jdbcBatchItemWriter方法用于配置ItemWriter,即数据的写出逻辑,Spring Batch也提供了多个ItemWriter的实例,常见的如FlatFileItemWriter,表示将数据写出为一个普通文件,StaxEventItemWriter表示将数据写出为XML。另外还有针对不同数据库提供的写出操作支持类,如MongoItemWriterJpaItemWriterNeo4jItemWriter以及HibernateItemWriter等,由于本例子使用的持久化方案是Spring-Jdbc,因此需要使用JdbcBatchItemWriter,它是通过JDBC将数据写出到一个关系型数据库中。JdbcBatchItemWriter主要配置数据以及数据插入SQL,注意占位符的写法是:属性名。最后通过BeanPropertyItemSqlParameterSourceProvider实例将实体类的属性和SQL中的占位符一一映射。
  • csvStep方法用于配置一个Step对象,Step通过stepBuilderFactory方法进行配置。首先通过get获取一个stepBuilder,get方法的参数就是该Step的name,然后调用chunk方法的参数2,表示每读取到两条数据就执行一次write操作,最后分别配置reader和writer。
  • csvJob方法用于配置一个Job对象,Job通过jobBuilderFactory方法构建一个Job,get方法的参数为Job的name,然后配置该Job的Step即可。

请注意笔者的data.csv文件放在了classpath,也就是和application.properties配置文件同一目录下。

接下来创建一个controller包,并在其中新建HelloController类,用于当用户发起一个请求时来触发批处理的执行,相应的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RestController
public class HelloController {
@Autowired
JobLauncher jobLauncher;
@Autowired
Job job;
@GetMapping("/hello")
public void hello(){
try{
jobLauncher.run(job,null);
}catch (Exception e){
e.printStackTrace();
}
}
}

请注意上面的JobLauncher是由框架提供的,而Job则是刚刚配置好的,通过调用JobLauncher中的run方法来启动一个批处理。

最后启动SpringBoot项目,并在浏览器中访问http://localhost:8080/hello链接,访问成功后,batch数据库中会自动创建出多个批处理相关的表,这些表用来记录批处理的执行状态,如下图所示:

同时也可以发现data.csv中的数据也都已经成功插入到user表中了,如下图所示:

Swagger 2

Swagger 2简介

在前后端分离开发中,为了减少与其他团队的沟通成本,一般会构建一份RESTful API文档,用来描述所有的接口信息,但是这种做法存在很大的弊端:(1)当接口众多的时候,编写RESTful API接口文档其实是一项比较繁重的工作,因为RESTful API接口文档不仅要包含接口的基本信息,如接口地址,接口请求参数,以及接口返回值等,还要包括HTTP请求类型、HTTP请求头、请求参数类型、返回值类型、所需权限等。(2)维护不方便,一旦某个接口发送变化,就需要去修改对应的文档描述。(3)对于接口测试非常不方便,一般只能借助于第三方工具,如Postman来进行测试。

Swagger 2是一个开源软件框架,可以帮助开发人员设计、构建、记录和使用RESTful Web服务,它将代码和文档融为一体,可以完美的解决上述问题,使得开发人员可以将大部分精力集中的业务中,而不是繁杂的文档中。

SpringBoot对于Swagger 2的整合也提供了良好的支持,接下来就学习如何在SpringBoot整合Swagger 2。

SpringBoot整合Swagger 2

第一步,创建项目。使用spring Initializr构建工具构建一个SpringBoot的Web应用,名称为swagger2springboot,然后在pom.xml文件中添加如下依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!--添加swagger2依赖-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--添加lombok依赖-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>

第二步,创建Swagger 2的配置类。新建一个config包,并在其中新建SwaggerConfig类,其中的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
Docket docket(){
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.basePackage("com.envy.swagger2springboot.controller"))
.paths(PathSelectors.any())
.build().apiInfo(
new ApiInfoBuilder().description("余思人事接口测试文档").contact(
new Contact("余思博客","https://github.com/envythink","envyzhan@aliyun.com"))
.version("v1.0")
.title("API测试文档")
.license("Apache2.0")
.licenseUrl("http://www.apache.org/licenses/LICENSe-2.0")
.build()
);
}
}

解释一下上述代码的含义:

  • 首先通过@EnableSwagger2注解来开启Swagger 2,然后最主要的就是配置一个Docket。
  • 通过apis方法配置需要扫描的controller的位置,通过paths方法来配置路径。
  • 在apiInfo方法中构建文档的基本信息,如描述、联系人信息、版本、标题等。

Swagger 2配置完成后,接下来新建一个用户实体类。新建一个pojo包,并在其中创建一个User实体类,相应的代码为:

1
2
3
4
5
6
7
8
@Data
@ApiModel(value = "用户实体类",description = "用户信息描述类")
public class User {
@ApiModelProperty(value = "用户名")
private String username;
@ApiModelProperty(value = "用户地址")
private String address;
}

@ApiModel注解,在实体类上使用,用于标记此实体类是swagger的解析类。提供有关swagger模型的其它信息,类将在操作中用作类型时自动内省。常用属性如下:

@ApiModelProperty注解,在被@ApiModel注解的实体类的属性上使用,表示对实体类属性的说明或者数据操作更改 ,一般是添加和操作模型属性的数据。常用属性如下:

然后就可以开发接口了。新建一个controller包,并在其中创建一个UserController类,其中的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@RestController
@Api(tags = "用户数据接口")
public class UserController {
@ApiOperation(value = "查询用户",notes = "根据id查询用户")
@ApiImplicitParam(paramType= "path",name = "id",value = "用户id",required = true)
@GetMapping("/user/{id}")
public String getUserById(@PathVariable Integer id){
return "/user/"+id;
}

@ApiResponses({
@ApiResponse(code=200,message = "删除成功!"),
@ApiResponse(code=500,message = "删除失败!")
})
@ApiOperation(value = "删除用户",notes = "根据id删除用户")
@DeleteMapping("/user/{id}")
public Integer deleteUserById(@PathVariable Integer id){
return id;
}

@ApiOperation(value = "添加一个用户",notes = "添加一个用户,传入用户名和地址")
@ApiImplicitParams({
@ApiImplicitParam(paramType = "query",name="username",value = "用户名",required = true,defaultValue = "envy"),
@ApiImplicitParam(paramType = "query",name="address",value = "用户地址",required = true,defaultValue = "shanghai"),
})
@PostMapping("/user")
public String addUser(@RequestParam String username,@RequestParam String address){
return username+ ":"+ address;
}

@ApiOperation(value = "修改用户",notes = "修改用户,传入用户信息")
@PutMapping("/user")
public String updateUser(@RequestBody User user){
return user.toString();
}

@GetMapping("/ignore")
@ApiIgnore
public void ignoreMethod(){
System.out.println("ignore!");
}
}

解释一下上述代码的含义:

  • @Api注解用在类上,用来描述整个Controller信息。
  • @ApiOperation注解用在开发方法上,用来描述一个方法的基本信息,value是对方法的一个简短描述,notes则用来备注该方法的详细作用。
  • @ApiImplicitParam注解用在方法上,用来描述方法的参数,paramType是指方法参数的类型,可选值有path(参数获取方式为@PathVariable)、query(参数获取方式为@RequestParam)、header(参数获取方式为@RequestHeader)、body以及form;name表示参数名称,和参数变量对应;value是参数的描述信息;required表示该字段是否必填;defaultValue表示该字段的默认值。注意这里的required和defaultValue等字段只是文档上的约束描述,并不能真正约束接口,约束接口还需要在@RequestParam中添加相关属性。如果方法有多个参数,可以将多个参数的@ApiImplicitParam注解放到@ApiImplicitParams中。
  • @ApiResponses注解是对响应结果的描述,code表示响应码,message表示相应的描述信息,若有多个@ApiResponse,则可以放在一个@ApiResponses中。
  • updateUser方法中,使用@RequestBody注解来接收数据,此时可以通过@ApiModel@ApiModelProperty注解配置User对象的描述信息。
  • @ApiIgnore注解用在方法上,表示不对某个接口生成文档。

接下来启动SpringBoot项目,在浏览器地址栏中输入http://localhost:8080/swagger-ui.html即可看到接口文档,如下图所示:

接下来展开用户数据接口,即可看到所有的接口描述,如下图所示:

接着展开一个接口描述,内容如下图所示,单击右侧的Try it out按钮,即可实现对该接口的测试:

数据校验

数据校验是开发过程中一个常见的环节,一般来说,为了提高系统运行效率,都会在前端进行数据校验,但是这并不意味着后端可以不做数据校验,因为用户还是可能在获取数据接口后手动传入非法数据,所以后端还是需要做数据校验。SpringBoot对此也提供了相关的自动化配置解决方案,下面分别予以介绍。

普通校验

普通校验是基础用法,非常容易,首先需要用户在SpringBoot Web项目中添加数据校验相关的依赖。

第一步,创建项目。使用spring Initializr构建工具构建一个SpringBoot的Web应用,名称为datavalidation,然后在pom.xml文件中添加如下依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!--添加数据验证validation依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--添加lombok依赖-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>

项目创建完成后,查看LocalValidatorFactoryBean类的源码可知,默认的ValidationMessageSource(校验出错时的提示文件)是resources目录下的ValidationMessages.properties文件,因此在resources目录下创建ValidationMessages.properties文件,相应的内容如下:

1
2
3
4
5
user.name.size=用户名长度介于5到10个字符之间
user.address.notnull=用户地址不能为空
user.age.size=年龄输入不正确
user.email.notnull=邮箱不能为空
user.email.pattern=邮箱格式不正确

接下来创建User类,并配置数据校验,相应的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Data
public class User {
private Integer id;
@Size(min = 5,max = 10,message = "{user.name.size}")
private String name;
@NotNull(message = "{user.address.notnull}")
private String address;
@DecimalMin(value = "1",message = "{user.age.size}")
@DecimalMax(value = "200",message = "{user.age.size}")
private Integer age;
@NotNull(message = "{user.email.notnull}")
@Email(message= "{user.email.pattern}")
private String email;
}

解释一下上述代码的含义:

  • @Size表示一个字符串的长度或者一个集合的大小,必须在某一个范围中;min参数表示范围的下限;max参数表示范围的上限;message表示校验失败时的提示信息。
  • @NotNull注解表示该字段不能为空。而 @DecimalMin注解表示对应属性值的下限,@DecimalMax注解表示对应属性值的上限。@Email注解表示对应属性格式是一个Email。

配置完成后,接下来新建UserController,其中的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
public class UserController {
@PostMapping("/user")
public List<String> addUser(@Validated User user, BindingResult result){
List<String> errors = new ArrayList<>();
if(result.hasErrors()){
List<ObjectError> allErrors = result.getAllErrors();
for(ObjectError error:allErrors){
errors.add(error.getDefaultMessage());
}
}
return errors;
}
}

解释一下上述代码的含义:

  • 给User参数添加@Validated注解,表示需要对该参数进行校验,紧接着的BindingResult参数表示在校验出错时保持的出错信息。
  • 如果BindingResult中的hasErrors方法返回true,说明有错误信息,此时遍历错误信息并将其返回给前端。

配置完成后,接下来使用Postman或者其他测试工具进行测试。先启动SpringBoot项目,然后直接访问/user接口,结果如下图所示:

如果文字出现乱码,请参考《SpringBoot学习(2):基础配置》一文中的解决办法进行操作。如果传入用户地址、一个非法邮箱地址以及一个格式不正确的用户名,结果如下图所示:

分组校验

有的时候,开发者在某一个实体类中定义了很多校验规则,但是在某一次业务处理中,并不需要这么多校验规则,此时就可以使用分组校验。分组校验的步骤如下所示:

第一步,创建两个分组接口。新建一个group包,然后创建两个接口,相应的代码为:

1
2
3
4
public interface ValidationGroup1 {
}
public interface ValidationGroup2 {
}

第二步,在实体类中添加分组信息。修改实体类User的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**分组校验对应的实体类信息*/
@Data
public class User {
private Integer id;
@Size(min = 5,max = 10,message = "{user.name.size}",groups = ValidationGroup1.class)
private String name;
@NotNull(message = "{user.address.notnull}", groups = ValidationGroup2.class)
private String address;
@DecimalMin(value = "1",message = "{user.age.size}")
@DecimalMax(value = "200",message = "{user.age.size}")
private Integer age;
@NotNull(message = "{user.email.notnull}",groups = {ValidationGroup1.class,ValidationGroup2.class})
@Email(message= "{user.email.pattern}")
private String email;
}

可以看到这次在部分注解中添加了groups属性,表示该校验规则所属的分组。
第三步,指定校验分组。修改UserController类中的代码,也就是在@Validated注解中指定校验的分组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
public class UserController {
@PostMapping("/user")
public List<String> addUser(@Validated(value = ValidationGroup2.class) User user, BindingResult result){
List<String> errors = new ArrayList<>();
if(result.hasErrors()){
List<ObjectError> allErrors = result.getAllErrors();
for(ObjectError error:allErrors){
errors.add(error.getDefaultMessage());
}
}
return errors;
}
}

可以看到@Validated(value = ValidationGroup2.class)表示这里的校验使用的是ValidationGroup2分组的校验规则,即只校验邮箱地址是否为空、用户地址是否为空。测试案例如下图所示。在测试案例中,虽然邮箱地址格式不正确,name长度也不满足要求,但是这些都不属于ValidationGroup2校验组的校验规则,这里仅仅只 校验邮箱地址是否为空、用户地址是否为空:

校验注解

前面学习了几个较为常见的校验注解,实际上校验注解远不止前面提到的几个,完整的校验注解可以参看下图所示:

企业开发常用功能小结

对于企业开发过程中的一些常用功能,笔者使用了两篇文章进行描述。其中第十八篇学习了邮件发送和定时任务,本篇学习了邮件发送、定时任务、批处理、Swagger 2和数据校验等,这些功能都有着非常广泛的使用场景,如用户注册、修改密码、定时备份、接口文档等。除了Swagger 2以外,其余4个功能在SpringBoot中都提供了相关的starter启动器,极大的简化了开发者的使用步骤,提高了开发效率,但是这并不影响大家对于Swagger 2的使用热情。