SpringBoot3 基础教程之七场景整合

大纲

Swagger 整合

本章节所需的案例代码,可以直接从 GitHub 下载对应章节 spring-boot3-12

概念介绍

  • springdoc-openapi 用于 Spring 项目快速自动生成 API 文档,通过在运行时检查应用程序来根据 Spring 配置、类结构和各种注解推断 API 语义,详细介绍请阅读 SpringDoc 官方文档
  • swagger-ui 可以快速生成实时接口文档,包括自动生成 JSON、YAML 和 HTML 格式 API 的文档,并遵循 OpenAPI 规范,详细介绍请查阅 Swagger 官网Swagger UI 项目
  • springdoc-openapi 支持以下功能:
    • OAuth 2
    • OpenAPI 3
    • Swagger UI
    • GraalVM native images
    • SpringBoot v3 (Java 17 & Jakarta EE 9)
    • JSR-303,specifically for @NotNull@Min@Max and @Size
  • springdoc-openapi 的架构图如下:

整合案例

引入依赖

1
2
3
4
5
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.2.0</version>
</dependency>

若使用的是基于 WebFlux 的响应式 Web 开发,则需要改为引入以下依赖

1
2
3
4
5
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webflux-ui</artifactId>
<version>2.2.0</version>
</dependency>

访问文档

提示

项目引入上述的 Maven 依赖后,SpringBoot 会自动将 swagger-ui 整合到项目中,一般情况下无需任何配置。

  • API 文档将默认以 HTML 格式提供,底层使用的是 swagger-ui
  • Swagger UI 的页面可以通过 http://server:port/context-path/swagger-ui.html 访问
  • OpenAPI 的 JSON 接口可以通过 http://server:port/context-path/v3/api-docs 访问
  • OpenAPI 的接口还支持通过 YAML 格式提供,访问路径是 http://server:port/context-path/v3/api-docs.yaml

自定义配置

1
2
3
4
5
6
7
# 自定义 OpenAPI 的访问路径,默认是 /v3/api-docs
springdoc.api-docs.path=/api-docs

# 自定义 Swagger UI 的访问路径,默认是 /swagger-ui.html
springdoc.swagger-ui.path=/swagger.html

springdoc.show-actuator=true

常用注解

注解介绍

使用案例

  • 实体类使用 Swagger 注解
1
2
3
4
5
6
7
8
9
10
11
@Schema(title = "部门信息")
@Data
public class Dept {

@Schema(title = "部门id")
private Long id;

@Schema(title = "部门名字")
private String deptName;

}
  • 控制器类使用 Swagger 注解
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
@Tag(name = "部门", description = "部门的CRUD")
@RestController
public class DeptController {

@Autowired
DeptService deptService;

@Operation(summary = "查询", description = "按照id查询部门")
@GetMapping("/dept/{id}")
public Dept getDept(@PathVariable("id") Long id) {
return deptService.getDeptById(id);
}

@Operation(summary = "查询所有部门")
@GetMapping("/depts")
public List<Dept> getDept() {
return deptService.getDepts();
}

@Operation(summary = "保存部门", description = "必须提交json")
@PostMapping("/dept")
public String saveDept(@RequestBody Dept dept) {
deptService.saveDept(dept);
return "ok";
}

@Operation(summary = "按照id删除部门", description = "必须提交json")
@DeleteMapping("/dept/{id}")
public String deleteDept(@PathVariable("id") @Parameter(description = "部门id") Long id) {
deptService.deleteDept(id);
return "ok";
}

}
  • Swagger UI 的页面内容

Docket 配置

Docket 可用于对多个 API 进行分组。

使用案例

如果有多个 Docket (分组) 存在,配置示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
public class ApiUiConfiguration {

@Bean
public GroupedOpenApi empApi() {
return GroupedOpenApi.builder().group("员工管理").pathsToMatch("/emp/**", "/emps").build();
}

@Bean
public GroupedOpenApi deptApi() {
return GroupedOpenApi.builder().group("部门管理").pathsToMatch("/dept/**", "/depts").build();
}

}

若只有一个 Docket (分组) 存在,那么可以简化为以下配置内容:

1
2
springdoc.packagesToScan=package1, package2
springdoc.pathsToMatch=/v1, /api/balance/**

配置效果

OpenAPI 配置

使用下述的配置代码,可以指定 Swagger 文档的页面描述内容,如文档标题、License、外部文档链接等。

使用案例

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class ApiUiConfiguration {

@Bean
public OpenAPI docsOpenAPI() {
return new OpenAPI().info(
new Info().title("SpringBoot3 CRUD API").description("CRUD 接口文档").version("V0.0.1")
.license(new License().name("Apache 2.0").url("https://springdoc.org"))).externalDocs(
new ExternalDocumentation().description("Wiki Documentation")
.url("https://springshop.wiki.github.org/docs"));
}

}

配置效果

Springfox 迁移

若项目从 Springfox 迁移到 Swagger,那么需要注意以下的变化。

注解使用变化

原注解现注解作用
@Api@Tag 描述 Controller
@ApiIgnore@Parameter(hidden = true),@Operation(hidden = true),@Hidden 描述忽略操作
@ApiImplicitParam@Parameter 描述参数
@ApiImplicitParams@Parameters 描述参数
@ApiModel@Schema 描述对象
@ApiModelProperty(hidden = true)@Schema(accessMode = READ_ONLY) 描述对象属性
@ApiModelProperty@Schema 描述对象属性
@ApiOperation(value = “foo”, notes = “bar”)@Operation(summary = “foo”, description = “bar”) 描述方法
@ApiParam@Parameter 描述参数
@ApiResponse(code = 404, message = “foo”)@ApiResponse(responseCode = “404”, description = “foo”) 描述响应

Docket 配置变化

  • 旧的写法
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
@Configuration
public class ApiUiConfiguration {

@Bean
public Docket publicApi() {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.basePackage("org.github.springshop.web.public"))
.paths(PathSelectors.regex("/public.*"))
.build()
.groupName("springshop-public")
.apiInfo(apiInfo());
}

@Bean
public Docket adminApi() {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.basePackage("org.github.springshop.web.admin"))
.paths(PathSelectors.regex("/admin.*"))
.apis(RequestHandlerSelectors.withMethodAnnotation(Admin.class))
.build()
.groupName("springshop-admin")
.apiInfo(apiInfo());
}

}
  • 新的写法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Configuration
public class ApiUiConfiguration {

@Bean
public GroupedOpenApi publicApi() {
return GroupedOpenApi.builder()
.group("springshop-public")
.pathsToMatch("/public/**")
.build();
}

@Bean
public GroupedOpenApi adminApi() {
return GroupedOpenApi.builder()
.group("springshop-admin")
.pathsToMatch("/admin/**")
.addOpenApiMethodFilter(method -> method.isAnnotationPresent(Admin.class))
.build();
}

}

远程过程调用整合

概念介绍

RPC(Remote Procedure Call) 是指远程过程调用,也就是说两台服务器 A,B,一个应用部署在 A 服务器上,想要调用 B 服务器上应用提供的函数 / 方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。RPC 的主要功能目标是让构建分布式计算(应用)更容易,在提供强大的远程调用能力时不损失本地调用的语义简洁性。为实现该目标,RPC 框架需提供一种透明调用机制让使用者不必显式的区分本地调用和远程调用。

  • 本地过程调用
    • a() 方法调用了 b() 方法,这些方法都在同一个 JVM 中运行
  • 远程过程调用
    • 服务提供者
    • 服务消费者
    • 服务消费者通过连接服务提供者的服务器进行请求 / 响应交互,来实现调用效果

API 与 SDK

API 与 SDK 的区别:

  • API:接口(Application Programming Interface)
    • 远程提供功能
  • SDK:工具包(Software Development Kit)
    • 导入 Jar 包,直接调用功能即可

在开发过程中,往往需要调用别人写的功能

  • 如果调用的是内部微服务,可以通过依赖注册中心、OpenFeign 等进行调用
  • 如果调用的是外部提供的服务,可以发送 HTTP 请求、或遵循外部协议进行调用

SpringBoot 提供了很多种方式进行远程调用

  • 轻量级客户端方式
    • RestTemplate: 普通开发
    • WebClient: 响应式开发
    • Http Interface: 声明式开发
  • Spring Cloud 分布式解决方案方式
    • Spring Cloud OpenFeign
    • Thrift
    • Dubbo
    • gRPC

WebClient 整合

SpringBoot 的 WebClient 是非阻塞、响应式 HTTP 客户端,需要是 WebFlux 场景(响应式编程)才可以使用。

历史背景

  • 在 Spring 5 之前,如果想要调用其他系统提供的 HTTP 服务,通常可以使用 Spring 提供的 RestTemplate 来访问。不过由于 RestTemplate 是 Spring 3 中引入的同步阻塞式 HTTP 客户端,因此存在一定的性能瓶颈。
  • 根据 Spring 官方文档介绍,在未来的版本中 RestTemplate 可能会被弃用。为了新的替代方案,Spring 官方已在 Spring 5 中引入了 WebClient 作为非阻塞式 (Reactive) HTTP 客户端。
  • 由于 WebClient 的请求模式属于异步非阻塞,所以它能够以少量固定的线程处理高并发的 HTTP 请求。

使用介绍

创建 WebClient 非常简单:

  • WebClient.create()
  • WebClient.create(String baseUrl)

还可以使用 WebClient.builder() 配置更多参数项:

  • uriBuilderFactory: 自定义 UriBuilderFactory ,定义 baseurl
  • defaultUriVariables: 默认 Uri 变量
  • defaultHeader: 每个请求的默认 HTTP 头
  • defaultCookie: 每个请求的默认 Cookie
  • defaultRequest: 自定义每个请求
  • filter: 过滤 Client 发送的每个请求
  • exchangeStrategies: HTTP 消息 reader/writer 自定义
  • clientConnector: HTTP Client 库设置

与 RestTemplate 相比,WebClient 有如下优势:

  • 支持同步和异步方案
  • 支持从服务器向上或向下流式传输
  • 提供基于 Java 8 Lambdas 的函数 API
  • 属于非阻塞、响应式的 HTTP 客户端,并支持更高的并发性能,占用更少的硬件资源

使用示例

  • 获取响应数据
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
// 创建客户端
WebClient client = WebClient.create("https://example.org");

Mono<ResponseEntity<Person>> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.toEntity(Person.class);

// 只获取Body
WebClient client = WebClient.create("https://example.org");

Mono<Person> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(Person.class);

// Stream数据
Flux<Quote> result = client.get()
.uri("/quotes").accept(MediaType.TEXT_EVENT_STREAM)
.retrieve()
.bodyToFlux(Quote.class);

// 定义错误处理
Mono<Person> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.onStatus(HttpStatus::is4xxClientError, response -> ...)
.onStatus(HttpStatus::is5xxServerError, response -> ...)
.bodyToMono(Person.class);
  • 定义请求体
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
// 1、响应式 - 单个数据
Mono<Person> personMono = ... ;

Mono<Void> result = client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.body(personMono, Person.class)
.retrieve()
.bodyToMono(Void.class);

// 2、响应式 - 多个数据
Flux<Person> personFlux = ... ;

Mono<Void> result = client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_STREAM_JSON)
.body(personFlux, Person.class)
.retrieve()
.bodyToMono(Void.class);

// 3、普通对象
Person person = ... ;

Mono<Void> result = client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(person)
.retrieve()
.bodyToMono(Void.class);

整合案例

提示

下述案例代码,以调用阿里云的天气预报 API 为例子,演示 SpringBoot 项目如何整合 WebClient。完整的案例代码,可以直接从 GitHub 下载对应章节 spring-boot3-13

  • 引入依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- Webflux -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

<!-- Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
  • 服务类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class WeatherService {

public Mono<String> weather(String city) {
// 请求参数
Map<String, String> params = new HashMap<>();
params.put("area", city);

// 发送请求(异步)
WebClient client = WebClient.create();
return client.get()
.uri("https://ali-weather.showapi.com/area-to-weather-date?area={area}", params)
.header("Authorization", "APPCODE 93b7e19861a24c519a7548b17dc16d75")
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(String.class);
}

}
  • 控制器类
1
2
3
4
5
6
7
8
9
10
11
12
@RestController
public class WeatherController {

@Autowired
private WeatherService weatherService;

@GetMapping("/weather")
public Mono<String> weather(@RequestParam(name = "city") String city) {
return weatherService.weather(city);
}

}

Http Interface 整合

SpringBoot 3 内置了声明式 HTTP 客户端,允许通过定义接口的方式,给任意位置发送 HTTP 请求,以此简化 HTTP 的远程调用。由于 Http Interface 的底层是基于 Webflux 的 WebClient 实现的,因此必须是 WebFlux 场景(响应式编程)才可以使用。

整合案例

提示

下述案例代码,以调用阿里云的天气预报 API 为例子,演示 SpringBoot 项目如何整合 Http Interface,可以直接从 GitHub 下载对应章节 spring-boot3-14

  • 引入依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- Webflux -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

<!-- Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>

  • 创建 API 接口,用于定义需要调用的外部 API
1
2
3
4
5
6
7
@HttpExchange
public interface WeatherApi {

@GetExchange(value = "/area-to-weather-date", accept = "application/json")
Mono<String> weather(@RequestParam("area") String city);

}
  • 创建配置类,注入 API 接口的代理对象
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
@Configuration
public class AliYunApiConfiguration {

@Value("${aliyun.api.baseurl:}")
private String baseurl;

@Value("${aliyun.api.appcode:}")
private String appCode;

@Bean
public HttpServiceProxyFactory httpServiceProxyFactory() {
// 创建客户端
WebClient client = WebClient.builder()
.baseUrl(baseurl)
.defaultHeader("Authorization", "APPCODE " + appCode)
.codecs(clientCodecConfigurer -> {
// 响应数据量太大有可能会超出缓冲区,所以这里设置的大一点
clientCodecConfigurer.defaultCodecs().maxInMemorySize(256 * 1024 * 1024);
}).build();

// 创建代理工厂
return HttpServiceProxyFactory.builder(WebClientAdapter.forClient(client)).build();
}

@Bean
public WeatherApi weatherApi(HttpServiceProxyFactory httpServiceProxyFactory) {
// 获取代理对象
return httpServiceProxyFactory.createClient(WeatherApi.class);
}

}
  • 创建服务类,通过 API 接口的代理对象发送 HTTP 请求
1
2
3
4
5
6
7
8
9
10
11
12
@Service
public class WeatherService {

@Autowired
private WeatherApi weatherApi;

public Mono<String> weather(String city) {
// 发送请求(异步)
return weatherApi.weather(city);
}

}
  • 创建控制器类,暴露接口给调用方
1
2
3
4
5
6
7
8
9
10
11
12
@RestController
public class WeatherController {

@Autowired
private WeatherService weatherService;

@GetMapping("/weather")
public Mono<String> weather(@RequestParam(name = "city") String city) {
return weatherService.weather(city);
}

}
  • 创建 Properties 配置文件
1
2
aliyun.api.baseurl=https://ali-weather.showapi.com
aliyun.api.appcode=93b7e19861a24c519a7548b17dc16d75