基于SpringBoot的微服务接口封装与回调接口设计
基本要求
- 理解掌握SpringBoot框架基本知识
- 熟悉微服务间调用的基本流程和类型
- 掌握RestTemplate、FeignClient和WebClient等服务调用工具的使用方式
实验原理
服务接口封装
服务接口封装是指将业务逻辑封装在服务中,并通过接口暴露给其他模块或系统使用的过程。通过将业务逻辑封装在服务中,并提供统一的接口给外部调用,实现了业务逻辑的模块化、解耦合,是构建可扩展、可维护的应用的重要手段。
以业务服务安全调用用户服务为例,通过封装用户身份验证的接口,可以实现统一的安全性和权限控制策略。其他服务在调用该接口时,可以统一通过用户服务登录接口进行身份验证,确保系统的安全性和数据的完整性。
此外,从服务解耦的角度看,接口封装可以将声明与实现解耦,服务间只需要通过接口声明来进行业务操作,无需了解服务具体实现细节。单个服务内部实现变化不会影响其他服务正常运行,从而实现服务之间的解耦。
服务调用
https://sookocheff.com/post/api/marrying-restful-http-with-asynchronous-design/
下面服务调用图示中,横轴表示同步或异步,纵轴表示单个接收者或多个接收者,横纵轴将服务调用类型划分为以下四类,请求调用方式包括 HTTP(同步/异步、单接收者)、RPC(异步单接收者)、P2P(同步单接收者)、Webhooks(异步回调多接收者)、消息队列(同步/异步、多接收者)等。
正向调用
正向调用是指一个服务同步调用另一个服务的过程,即调用方发出请求后并等待被调用方的响应,被调用方没有响应前调用方参与阻塞状态。正向调用方式是一种常见的服务间通信方式,在分布式系统和微服务架构中被广泛应用。在正向调用过程中,通常包括以下几个步骤:
a. 调用方发起请求:调用方向被调用方发送请求,请求通常包含了所需的参数、方法名等信息。
b. 请求到达被调用方:请求被传递到被调用方所在的服务,被调用方根据请求的内容进行处理。
c. 被调用方处理请求:被调用方根据请求的内容执行相应的操作,可能包括数据查询、业务逻辑处理等。
d. 被调用方返回响应:被调用方处理完请求后,将处理结果封装成响应返回给调用方。
e. 调用方接收响应:调用方接收到被调用方的响应,根据响应内容进行相应的处理,可能是数据解析、错误处理等。
服务回调
回调是一种常见的编程模式,通常用于异步操作的处理。在这种模式下,一个函数(回调函数)作为参数传递给另一个函数,当特定的事件发生时,另一个函数会调用传递的回调函数来处理事件。
服务回调在分布式系统和微服务架构中经常用到,用于处理服务之间的通信和协作。例如,一个服务可能需要调用另一个服务来获取数据,但由于网络延迟等原因,获取数据的过程是异步的。在这种情况下,调用方可以传递一个回调函数给被调用方,在数据准备好之后,被调用方会调用回调函数来将数据返回给调用方。
服务回调的优点包括异步处理以提供系统并发性能、解耦处理逻辑与触发逻辑、支持复杂业务逻辑处理等,存在的问题则是代码可读性差、调试困难等。
异步调用
异步调用是指调用方不必等待被调用方的返回响应就可以继续执行其他操作。实现异步调用的方式有很多,以下是常见的几种类型:
- 消息队列:使用消息队列作为异步通信的中间件。当一个微服务需要调用另一个微服务时,它将请求封装成消息并发送到消息队列中,然后继续处理其他任务。接收方微服务从消息队列中获取消息并进行处理。这种方式可以实现解耦和异步处理。
- 异步HTTP调用:在微服务之间使用异步的HTTP调用。调用方微服务发送HTTP请求给目标微服务,但不等待响应,而是继续处理其他任务。目标微服务接收到请求后进行处理,并将响应返回给调用方微服务。这种方式可以提高并发性能和系统的可伸缩性。
- 事件驱动架构:使用事件驱动的方式进行异步调用。当一个微服务发生某个事件时,它将事件发布到事件总线或消息队列中。其他订阅了该事件的微服务将接收到事件并进行相应的处理。这种方式可以实现松耦合和异步处理。
服务调用工具
https://docs.spring.io/spring-framework/reference/integration/rest-clients.html#rest-http-interface
RestTemplate
在 Spring Framework 中,RestTemplate 是一个用于进行 RESTful 服务调用的模板类。它封装了大量的 HTTP 请求和响应操作,使得在 Java 应用程序中进行 HTTP 调用变得更加简单和便捷。RestTemplate 有如下特点:
- 支持多种 HTTP 方法:RestTemplate 支持常见的 HTTP 方法,如 GET、POST、PUT、DELETE 等,以及对应的请求体和响应体处理。
- 灵活的请求响应处理:RestTemplate 支持多种请求和响应处理方式,包括将请求参数映射为 URI 模板、将请求体映射为对象、将响应体映射为对象等。
- 集成 HTTP 客户端:RestTemplate 集成了 Apache HttpClient、OkHttp 或者 HttpComponents Client 等 HTTP 客户端实现,可以通过设置选择不同的客户端。
RestTemplate 集成在 spring-boot-starter-web 依赖中,以下是 RestTemplate 的示例代码:
public class RestTemplateExample {
public static void main(String[] args) {
RestTemplate restTemplate = new RestTemplate(); // 创建 RestTemplate 实例
String url = "https://www.api.com/examples"; // 定义要请求的 URL
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(url)
.queryParam("name", "Alice") // 构建请求参数
.queryParam("id", "4873423211");
HttpHeaders headers = new HttpHeaders(); // 构建请求头
headers.add("Content-Type", "application/json");
// 发起带参数和 header 的 GET 请求,并将响应结果转换为指定类型的对象
String response = restTemplate.getForObject(builder.toUriString(), String.class, headers);
// 输出响应结果
System.out.println("Response: " + response);
}
}
WebClient
WebClient 是 Spring Web Reactive 模块的一个非阻塞的响应式 Web 请求客户端,用于取代经典的 RestTemplate
。当前 Spring Framework 服务调用的最佳实践形式是利用 WebClient 和 Spring 6.0 提出的 HTTP Interface 协同提供服务调用功能。WebClient 也可以独立执行 HTTP 调用。
在 Spring Boot 应用中,只需要添加 spring-boot-starter-webflux
依赖即可获得响应式 Web 的支持。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
创建和使用 WebClient 实例的方式如下:
WebClient client = webClient.builder()
.baseUrl("https://www.api.com/examples")
.defaultCookie("cookieKey", "cookieValue")
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.build();
// 执行 GET 请求
UriSpec<RequestBodySpec> uriSpec = client.get();
// 获取响应数据
String response = uriSpec.retrieve().bodyToMono(String.class).block();
WebClient 和 HTTP Interface 协同使用时能够实现声明式调用 API,以下示例展示了具体用法:
- 定义接口,在接口中声明 HTTP API。
@Component
public interface UserClient {
// 与 Controller 编写 HTTP API 格式相同
@GetMapping("/users")
List<User> queryUsers(@RequestParam(required = false) String id,
@RequestParam(required = false) String name);
}
- 创建 WebClient 示例,通过 WebClient 注册接口,之后该接口就可以注入使用了。
@Bean
UserClient initWebClient() {
WebClient client = WebClient.builder().baseUrl("http://localhost:8080/").build();
HttpServiceProxyFactory factory =
HttpSessionProxyFactory.builderFor(WebClientAdapter.create(client)).build();
return factory.createClient(UserClient.class);
}
Spring Cloud Netflix 提供的 Feign HTTP 客户端,也是声明式 Web 服务客户端,其工作方式与 HTTP Interface 类似,是 Spring Cloud 中用于简化服务间通信的重要组件。
MyBatis-Plus
MyBatis&MyBatis-Plus框架介绍
MyBatis 是一个基于 Java 的持久层框架,它通过 XML 描述符或注解将对象与 SQL 语句进行映射,帮助开发者进行数据库操作。MyBatis 的核心思想是通过 SQL 语句完成数据库操作,同时提供了一系列映射配置,将数据库表的记录映射为 Java 对象。MyBatis-Plus 是 MyBatis 的增强工具包,提供了一系列更便捷、更强大的功能,使得使用 MyBatis 更加高效。MyBatis-Plus 的目标是简化 MyBatis 的开发,提供更多的便利特性。
MyBatis-Plus 主要特点和优点:
- 自动生成代码:提供了代码生成器,可以根据数据库表生成实体类、Mapper 接口和 XML 映射文件。
- 内置通用 CRUD 方法:提供了通用的增删改查方法,减少了手写基础 CRUD 操作的工作。
- 支持 Lambda 查询:提供了 Lambda 表达式的支持,使得查询条件更加灵活。
- 内置分页插件:支持简单的分页查询,方便进行分页操作。
- 自动注入:提供了自动注入的功能,无需手动实例化 Mapper 接口。
MyBatis 和 MyBatis-Plus 在 Java 后端开发项目中得到广泛应用,为开发者提供了便捷、灵活的数据库操作方式。
MyBatis-Plus接入方式
在接入 MySQL 的基础上,Spring Boot 需要使用 mybatis-plus-boot-starter 依赖支持 MyBatis-Plus 正常工作。
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>latest_version</version>
</dependency>
使用MyBatis-Plus步骤如下:
- 引入MyBatis-Plus及MySQL依赖,配置数据源;
- 编写实体类和Mapper接口,实体类需要使用@TableName和具体数据库表名关联,Mapper接口继承BaseMapper。
- 在Service层注入Mapper接口,执行BaseMapper提供的数据表操作方法,如selectOne()、selectAll()、selectById()等。
实验过程
服务交互流程介绍
本实验通过构建简易的用户管理服务和业务服务来说明服务间调用的工具及方式。用户管理服务使用数据库,提供用户管理接口,包括查询用户和新增用户的接口。业务服务仅调用用户管理服务的接口,不使用数据库。
构建用户管理服务
数据库设计实现
为简单起见,用户管理服务的数据库中仅包括系统用户表一张数据表,数据库和数据表命名分别为 db_user、tbl_user。用户表结构如下:
tbl_user:
字段 | 类型 | 说明 |
---|---|---|
id | varchar(20) | 用户编号,采用雪花算法 ID |
mobile_phone | varchar(11) | 手机号 |
name | varchar(50) | 姓名 |
- 在 Linux 或 Windows 系统中安装并启动 MySQL,安装过程参考 https://dev.mysql.com/doc/mysql-installation-excerpt/8.0。
[!NOTE]
(Windows)如果安装过其他版本的MySQL,先确保完全卸载后再安装!
附:MySQL彻底卸载教程:教你彻底卸载MySQL 并重装(保姆级教程 )_mysql怎么卸载干净重装-CSDN博客
MySQL8.0安装教程:2024 年 MySQL 8.0 安装 配置 教程 最简易(保姆级)_mysql安装-CSDN博客
下载安装包到本地安装即可。
注意配置环境变量!
- 基于以上用户表结构编写 SQL 语句,并导入至 MySQL 数据库,完成库表创建。
CREATE DATABASE db_user;
USE db_user;
Database changed
CREATE TABLE tbl_user (
id VARCHAR(20) NOT NULL,
mobile_phone VARCHAR(11) UNIQUE,
name VARCHAR(50),
PRIMARY KEY (id)
);
DESCRIBE tbl_user;
+--------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+--------+--------------+------+-----+---------+-------+
| id | varchar(20) | NO | PRI | NULL | |
| mobile_phone | varchar(11) | YES | UNI | NULL | |
| name | varchar(50) | YES | | NULL | |
+--------+--------------+------+-----+---------+-------+
3 rows in set (0.01 sec)
mysql>
搭建项目框架
- 下载maven在本地完成配置,教程:Maven 3.9.1下载安装配置一条龙(无压力)亲测_maven3.9-CSDN博客
- 基于 Spring Initializer 搭建 Spring Boot 项目 userService,在
pom.xml
中添加 web、mysql、mybatis-plus 及 Snowflake 等依赖。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.5</version>
</dependency>
<dependency>
<groupId>cc.synx</groupId>
<artifactId>snowflake</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
[!NOTE]
雪花组件我选择在之前搭建的私有仓库中导入,设置如下:
- 在
src/main/resources
目录下将application.properties
文件改名为application.yml
,添加连接 MySQL 的驱动配置。本次实验不配置额外的数据库连接池,使用 Spring Boot 默认提供的数据库连接池Hikari
。
server:
port: 8080
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://<server-id>:3306/db_user?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8
username: <mysql_username>
password: <mysql_password>
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
map-underscore-to-camel-case: true # 驼峰转化
call-setters-on-nulls: true
(记得修改成本地ip,自己的MySQL登录用户名root和密码)
- 搭建项目骨架,在启动类同级目录上创建 mapper、service、entity 及 controller 包。并在启动类上添加 MyBatis 扫描 Mapper 包的注解
@MapperScan("cc.synx.mapper")
。
$ tree
.
├── .mvn
├── src
│ ├── main
│ │ ├── java
│ │ │ └── cc
│ │ │ └── synx
│ │ │ ├── controller # 控制层
│ │ │ ├── mapper # 持久层
│ │ │ ├── service # 服务层
│ │ │ └── entity # 服务实体包
│ │ │ └── UserServiceApplication.java
│ │ └── resources
│ │ └── application.yml
│ └── test
│ ├── java
│ └── cc
│ └── synx
└── pom.xml
- 在 entity 包中创建 User 类,字段与数据表
tbl_user
保证一致。
package cc.synx.entity;
/**
* 用户类
* @version 1.0
* @ClassName User
**/
@Data
@TableName("tbl_user")
public class User {
// 用户标识,采用雪花算法
private String id;
// 电话号码
private String mobilePhone;
// 用户姓名
private String name;
public User(String id, String mobilePhone, String name) {
this.id = id;
this.mobilePhone = mobilePhone;
this.name = name;
}
}
- 在 mapper 包中创建 UserMapper 接口,实现 Mybatis-Plus 提供的 BaseMapper 接口后能够调用 MyBatis-plus 提供的数据库执行方法。
[!NOTE]
多多导入类!
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
服务接口编写
- 用户管理服务需要实现查询用户和新增用户两个接口,接口声明如下:
接口名称 | 方法名 | URI | 请求方式 |
---|---|---|---|
查询用户 | queryUsers | /users | GET |
新增用户 | addUsers | /users | POST |
- 在 service 包中创建 UserServiceImpl 类,实现查询用户和新增用户两个方法。
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> {
/**
* 查询用户,支持id精确查询、name模糊查询
*/
public List<User> queryUsers(String id, String name) {
LambdaQueryChainWrapper<User> queryWrapper = new LambdaQueryChainWrapper<>(User.class);
queryWrapper.eq(id != null && !id.isEmpty(), User::getId, id);
queryWrapper.like(name != null && !name.isEmpty(), User::getName, name);
return queryWrapper.list();
}
/**
* 新增用户
*/
public boolean addUsers(List<User> users) {
SnowflakeUtil snowflakeUtil = new SnowflakeUtil(1L);
for (User user : users) {
user.setId(String.valueOf(snowflakeUtil.nextId()));
}
return saveBatch(users);
}
}
- 在 controller 包中创建 UserController 类,在该类中编写服务对外暴露的两个接口。
@RestController
@RequestMapping("/users")
public class UserController {
private UserServiceImpl userService;
public UserController(UserServiceImpl userService) { // 构造器注入 userService
this.userService = userService;
}
/**
* 查询用户
*/
@GetMapping("")
public List<User> queryUsers(@RequestParam(required = false) String id, @RequestParam(required = false) String name) {
return userService.queryUsers(id, name);
}
/**
* 新增用户
*/
@PostMapping("")
public boolean addUsers(@RequestBody List<User> users) {
return userService.addUsers(users);
}
}
- 使用 Maven 打包或在 IDE 中直接启动服务。这里选择直接启动服务。Maven打包代码如下:
$ mvn clean package
$ java -jar target/userService-0.0.1-SNAPSHOT.jar
微服务同步调用
搭建业务服务框架
- 与搭建用户管理服务类似,新建业务服务 bizService,业务服务不使用数据库,仅添加 Web 依赖即可。在
application.yml
中将业务服务启动的接口改为 8081。
server:
port: 8081
- 在启动类同级目录创建 controller、entity 包,将用户管理服务的 User 类复制到 entity 包中,并在 controller 包中新建 BizController 类,类中唯一的接口用于组合调用用户管理服务的两个接口,实现新增用户并返回新增后的用户列表,内容如下:
@RestController
@RequestMapping("/biz")
public class BizController {
/**
* 新增用户并返回新增后的用户列表
* @param users
* @return
*/
@PostMapping
public Map<String, Object> addAndGetUsers(@RequestBody List<User> users) {
return null;
}
}
服务正向调用
本节将使用前文提出的 WebClient + HTTP Interfaces 声明式调用工具来完成上述接口的编写。通过使用 HTTP 接口声明创建客户端代理来执行 HTTP 请求。
- 在
pom.xml
中添加spring-boot-starter-webflux
。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
- 在启动类同级目录创建 api 包,新增 UserClient 接口,添加 User 服务的两个接口声明。
public interface UserClient {
@GetExchange("/users")
List<User> queryUsers(@RequestParam(required = false) String id, @RequestParam(required = false) String name);
@PostExchange("/users")
boolean addUsers(@RequestBody List<User> users);
}

- 在启动类中使用 WebClient 注册 UserClient Bean,ip地址和端口需要替换为启动用户管理服务的ip和port。
@Bean
UserClient initWebClient() {
WebClient client = WebClient.builder().baseUrl("http://localhost:8080/").build();
HttpServiceProxyFactory factory =
HttpServiceProxyFactory.builderFor(WebClientAdapter.create(client)).build();
return factory.createClient(UserClient.class);
}
- 补充 BizController 类的代码。
@RestController
@RequestMapping("/biz")
public class BizController {
private final UserClient userClient;
public BizController(UserClient userClient){
this.userClient = userClient;
}
/**
* 新增用户并返回新增后的用户列表
* @param users
* @return
*/
@PostMapping
public List<User> addAndGetUsers(@RequestBody List<User> users){
boolean b = userClient.addUsers(users);
if(b){
return userClient.queryUsers(null, null);
}
return null;
}
}
- 打包部署 BizService,使用接口访问工具请求 http://127.0.0.1:8081/biz 接口,传入新增用户信息,查看返回结果。
[!WARNING]
500
设计和实现服务回调
目前为确保用户信息的完整准确性,新增用户完成后,业务服务需要将这些用户的信息送往第三方数据校验中心进行姓名和电话号码校验,validate 服务基于数据中心的数据检测用户信息是否正常,检测完成后回调业务服务接口将用户 ID 传回,由业务服务进行处理。在正常工业场景中,业务服务调用 validate 服务的操作通常是异步非阻塞的,这里简化为同步回调模式。本小节简述上述场景实现思路,具体操作请自行完成。
- 构建 Validate 服务,提供 validateUsers 接口,接收用户列表、回调URL等参数,当该接口执行完成后,将正常用户ID和异常用户ID调用回调URL传回业务服务。
@PostMapping("/validate")
public boolean validateUsers(@RequestBody List<User> users, @RequestParam String url){
// 1. 遍历users,验证用户信息
// 2. 获取正常用户ID列表和异常用户ID列表
// 3. 请求URL将用户ID列表传回
return true;
}
- 在业务服务中需要提供对正常用户、异常用户的操作方法,该方法URL将作为业务服务调用 Validate 服务的 validateUsers 接口参数传入。以下方法需要添加 fastjson2 依赖。
@PutMapping("/callback-user-info")
public boolean updateUserInfo(@RequestBody String jsonString){
JSONObject jsonObject = new JSONObject(jsonString);
List<String> normalUserIds = jsonObject.getJSONArray("normal").toList(String.class);
List<String> abnormalUserIds = jsonObject.getJSONArray("abnormal").toList(String.class);
// 对正常用户和异常用户进行处理
return true;
}
- 在业务服务的 addAndGetUsers 方法中调用 ValidateUsers 接口,validateClient 与 userClient 实现方式类似。
@PostMapping
public List<User> addAndGetUsers(@RequestBody List<User> users) {
boolean b = userClient.addUsers(users);
if (b) {
validateClient.validateUsers(users, "http://localhost:8081/biz/callback-user-info");
return userClient.queryUsers(null, null);
}
return null;
}
微服务异步调用
我们了解到,同步调用会阻塞请求方,HTTP 请求方只有获取到响应结果后方可继续执行。在微服务系统构建过程中,需要甄别哪些调用是必要同步的,哪些调用可以是异步的。如上所述,在业务系统同步调用第三方接口的场景中,当第三方接口执行时间长、报错、超时等情况会影响业务系统的正常运行,是否采用异步模式则取决于业务系统对第三方操作的容忍程度,如果业务系统严格要求每个用户的信息是准确无误的,采用同步模式,否则可以采用异步调用+重试机制加快请求效率。
在 Spring 应用中,实现异步调用的方式通常采用 AsyncTaskExecutor
线程池,Spring Boot 提供了快速标记异步方法的注解 @Async
,以业务服务异步调用 validate 服务接口为例,具体应用方式如下:
- 在启动类中添加
@EnableAsync
注解,开启异步执行方法的能力。
@EnableAsync
@SpringBootApplication
public class BizServiceApplication {
// ...
}
- 在 ValidateClient 接口的 validateUsers 方法声明上添加
@Async
注解。
public interface ValidateClient {
@Async
@PostMapping("/validate")
public boolean validateUsers(@RequestBody List<User> users, @RequestParam String url);
}
- 再次访问 addAndGetUsers 接口时,发现 validateClient.validateUsers 方法没有返回结果前,业务服务便已经执行 userClient.queryUsers 返回用户信息了。
[!NOTE]
@Async 使用注意事项
- 不要在同一个类中声明并使用异步方法,在同一个类中调用异步方法,无法通过代理机制工作,异步方法不生效。
- 生产环境中使用 @Async 时,推荐根据生产机器的性能使用 ThreadPoolTaskExecutor 实现自定义线程池,避免引起性能不足问题。