Spring Cloud 学习与实践(2):用户服务开发
文章目录
- Spring Cloud 学习与实践(2):用户服务开发
Spring Cloud 学习与实践(2):用户服务开发
本章目标:开发第一个独立运行的业务服务
cloud-user,接入 Spring Boot Web、MySQL 和 MyBatis-Plus,实现用户 CRUD 接口;同时通过一次分页失效故障演练,理解 MyBatis-Plus 分页插件的工作机制。
1. 本章目标
上一章已经完成了 Spring Cloud Alibaba 微服务项目的 Maven 多模块骨架,并在 cloud-common 中建立了统一返回、错误码、业务异常和全局异常处理等公共能力。
本章进入第一个业务服务:
cloud-user
主要完成以下内容:
1. 为 cloud-user 添加 Spring Boot Web、MyBatis-Plus 和 MySQL 依赖
2. 创建 cloud_demo 数据库和 t_user 用户表
3. 配置 application.yml
4. 创建 cloud-user 启动类
5. 创建 User 实体类
6. 创建 UserMapper
7. 创建 UserService 和 UserServiceImpl
8. 创建 UserController
9. 使用 IDEA HTTP Client 测试接口
10. 故意不配置分页拦截器,观察分页失效现象
11. 添加 MyBatisPlusConfig,修复分页问题
本章暂时只开发一个独立运行的 Spring Boot 服务。
暂时不接入:
Nacos
OpenFeign
Gateway
JWT
Sentinel
Redis
RabbitMQ
这些能力将在后续章节中逐步加入。
2. 为什么先开发用户服务
微服务架构并不是一开始就把所有组件全部接入。
如果同时引入注册中心、配置中心、网关、远程调用、限流熔断和消息队列,出现问题时很难判断故障来自哪一个环节。
因此,本课程采用逐步演进方式:
先开发一个可以独立运行的 Spring Boot 服务
↓
确认数据库访问、业务分层、统一返回正常
↓
再开发商品服务和订单服务
↓
再接入 Nacos 注册中心
↓
再接入 OpenFeign 完成服务间调用
本章选择用户服务作为第一个业务服务,因为它具备典型的 CRUD 特征,适合验证:
Spring Boot 启动
MySQL 数据源
MyBatis-Plus
Controller / Service / Mapper 分层
统一返回 Result<T>
分页插件
一句话总结:
先让一个服务独立跑通,再逐步增加微服务治理能力。
3. cloud-user 模块结构
本章完成后的 cloud-user 目录结构如下:
cloud-user
├── pom.xml
└── src
├── main
│ ├── java
│ │ └── com.example.cloud.user
│ │ ├── CloudUserApplication.java
│ │ ├── config
│ │ │ └── MyBatisPlusConfig.java
│ │ ├── controller
│ │ │ └── UserController.java
│ │ ├── entity
│ │ │ └── User.java
│ │ ├── mapper
│ │ │ └── UserMapper.java
│ │ └── service
│ │ ├── UserService.java
│ │ └── impl
│ │ └── UserServiceImpl.java
│ └── resources
│ └── application.yml
└── test
└── java
└── http
└── user.http
各目录职责如下:
| 目录 | 作用 |
|---|---|
| entity | 数据库表对应的实体类 |
| mapper | 数据访问层,负责操作数据库 |
| service | 业务逻辑接口 |
| service.impl | 业务逻辑实现 |
| controller | HTTP 接口入口 |
| config | Spring 和 MyBatis-Plus 配置 |
| resources | 配置文件 |
| test/http | IDEA HTTP Client 接口测试文件 |
4. 创建数据库和用户表
首先在本地 MySQL 中创建数据库:
CREATE DATABASE IF NOT EXISTS cloud_demo
DEFAULT CHARACTER SET utf8mb4;
切换数据库:
USE cloud_demo;
创建用户表:
DROP TABLE IF EXISTS t_user;
CREATE TABLE t_user (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '用户ID',
username VARCHAR(64) NOT NULL COMMENT '用户名',
nickname VARCHAR(64) DEFAULT NULL COMMENT '昵称',
phone VARCHAR(20) DEFAULT NULL COMMENT '手机号',
status TINYINT NOT NULL DEFAULT 1 COMMENT '状态:1启用,0禁用',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (id),
UNIQUE KEY uk_username (username)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
插入两条初始测试数据:
INSERT INTO t_user (username, nickname, phone, status)
VALUES
('zhangsan', '张三', '13800000001', 1),
('lisi', '李四', '13800000002', 1);
为了测试分页功能,再插入三条数据:
INSERT INTO t_user (username, nickname, phone, status)
VALUES
('wangwu', '王五', '13800000003', 1),
('zhaoliu', '赵六', '13800000004', 1),
('sunqi', '孙七', '13800000005', 1);
最终表中共有 5 条用户数据。

5. cloud-user 的 pom.xml
在 cloud-user/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>
<parent>
<groupId>com.example.cloud</groupId>
<artifactId>cloud-demo</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>cloud-user</artifactId>
<packaging>jar</packaging>
<dependencies>
<!-- 公共模块:统一返回、异常、错误码 -->
<dependency>
<groupId>com.example.cloud</groupId>
<artifactId>cloud-common</artifactId>
<version>${project.version}</version>
</dependency>
<!-- Spring Boot Web,用于提供 HTTP 接口 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MyBatis-Plus,Spring Boot 2 使用 mybatis-plus-boot-starter -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<!-- MySQL 驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>
依赖作用如下:
| 依赖 | 作用 |
|---|---|
| cloud-common | 使用统一返回、错误码和全局异常处理 |
| spring-boot-starter-web | 提供 Controller、Tomcat 和 Web 接口能力 |
| mybatis-plus-boot-starter | 简化 MyBatis 数据库操作 |
| mysql-connector-java | 连接 MySQL 数据库 |
| lombok | 减少 getter、setter 等样板代码 |
需要注意:
父工程 dependencyManagement 只负责统一版本。
cloud-user 仍然需要在 dependencies 中主动声明自己使用的依赖。
6. 配置 application.yml
在下面位置创建配置文件:
cloud-user
└── src
└── main
└── resources
└── application.yml
配置如下:
server:
port: 9200
spring:
application:
name: cloud-user
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/cloud_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false
username: root
password: your_password
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
id-type: auto
主要配置说明:
| 配置项 | 作用 |
|---|---|
| server.port | cloud-user 服务端口 |
| spring.application.name | 服务名称,后续注册到 Nacos 时会使用 |
| spring.datasource | MySQL 数据源 |
| mybatis-plus.configuration.log-impl | 在控制台打印 SQL |
| mybatis-plus.global-config.db-config.id-type | 主键使用数据库自增 |
本章将用户服务端口设置为:
9200
后续其他服务会使用不同端口:
Gateway:9000
Auth:9100
User:9200
Product:9300
Order:9400
7. 创建启动类
创建:
cloud-user
└── src/main/java
└── com.example.cloud.user
└── CloudUserApplication.java
代码如下:
package com.example.cloud.user;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication(scanBasePackages = "com.example.cloud")
public class CloudUserApplication {
public static void main(String[] args) {
SpringApplication.run(CloudUserApplication.class, args);
}
}
其中:
@SpringBootApplication(scanBasePackages = "com.example.cloud")
表示扫描:
com.example.cloud
包下的 Spring Bean。
这样不仅可以扫描 cloud-user 中的 Controller、Service 和配置类,也可以扫描 cloud-common 中的全局异常处理器。
如果只扫描默认路径:
com.example.cloud.user
那么 cloud-common 中的公共 Spring Bean 可能无法自动注册。
8. 创建 User 实体类
创建:
cloud-user
└── src/main/java
└── com.example.cloud.user.entity
└── User.java
代码如下:
package com.example.cloud.user.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("t_user")
public class User {
@TableId(type = IdType.AUTO)
private Long id;
private String username;
private String nickname;
private String phone;
private Integer status;
private LocalDateTime createTime;
}
关键注解:
| 注解 | 作用 |
|---|---|
| @Data | Lombok 自动生成 getter、setter、toString 等方法 |
| @TableName(“t_user”) | 指定实体类对应的数据库表 |
| @TableId(type = IdType.AUTO) | 指定主键由数据库自增生成 |
MyBatis-Plus 默认支持下划线与驼峰命名转换:
create_time
↓
createTime
因此不需要额外添加:
@TableField("create_time")
9. 创建 UserMapper
创建:
cloud-user
└── src/main/java
└── com.example.cloud.user.mapper
└── UserMapper.java
代码如下:
package com.example.cloud.user.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.cloud.user.entity.User;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
BaseMapper<User> 已经提供常见数据库操作:
selectById
selectList
insert
updateById
deleteById
因此,简单 CRUD 不需要手写 SQL,也不需要 XML 文件。
一句话总结:
Mapper 负责直接访问数据库。
10. 创建 Service
创建接口:
cloud-user
└── src/main/java
└── com.example.cloud.user.service
└── UserService.java
代码如下:
package com.example.cloud.user.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.example.cloud.user.entity.User;
public interface UserService extends IService<User> {
}
创建实现类:
cloud-user
└── src/main/java
└── com.example.cloud.user.service.impl
└── UserServiceImpl.java
代码如下:
package com.example.cloud.user.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.cloud.user.entity.User;
import com.example.cloud.user.mapper.UserMapper;
import com.example.cloud.user.service.UserService;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl
extends ServiceImpl<UserMapper, User>
implements UserService {
}
其中:
IService<User>
提供通用业务方法:
list
getById
save
updateById
removeById
page
而:
ServiceImpl<UserMapper, User>
将通用 Service 方法与 UserMapper 关联起来。
一句话总结:
Mapper 负责数据库访问。
Service 负责编排业务逻辑。
虽然当前示例中的业务逻辑比较简单,但仍然保留 Service 层。
原因是后续真实业务通常会出现:
参数校验
状态判断
事务控制
缓存操作
远程服务调用
消息发送
这些逻辑不适合全部堆在 Controller 中。
11. 创建 UserController
创建:
cloud-user
└── src/main/java
└── com.example.cloud.user.controller
└── UserController.java
代码如下:
package com.example.cloud.user.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.cloud.common.result.ErrorCode;
import com.example.cloud.common.result.Result;
import com.example.cloud.user.entity.User;
import com.example.cloud.user.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
/**
* 查询全部用户
*/
@GetMapping
public Result<List<User>> list() {
List<User> users = userService.list();
return Result.success(users);
}
/**
* 根据 ID 查询用户
*/
@GetMapping("/{id}")
public Result<User> getById(@PathVariable Long id) {
User user = userService.getById(id);
if (user == null) {
return Result.fail(ErrorCode.NOT_FOUND, "用户不存在");
}
return Result.success(user);
}
/**
* 新增用户
*/
@PostMapping
public Result<Void> create(@RequestBody User user) {
boolean success = userService.save(user);
if (!success) {
return Result.fail(ErrorCode.BIZ_ERROR, "新增用户失败");
}
return Result.success();
}
/**
* 修改用户
*/
@PutMapping
public Result<Void> update(@RequestBody User user) {
if (user.getId() == null) {
return Result.fail(ErrorCode.PARAM_ERROR, "用户 ID 不能为空");
}
boolean success = userService.updateById(user);
if (!success) {
return Result.fail(ErrorCode.NOT_FOUND, "用户不存在或修改失败");
}
return Result.success();
}
/**
* 删除用户
*/
@DeleteMapping("/{id}")
public Result<Void> delete(@PathVariable Long id) {
boolean success = userService.removeById(id);
if (!success) {
return Result.fail(ErrorCode.NOT_FOUND, "用户不存在或删除失败");
}
return Result.success();
}
/**
* 分页查询用户
*/
@GetMapping("/page")
public Result<Page<User>> page(
@RequestParam(defaultValue = "1") long current,
@RequestParam(defaultValue = "10") long size) {
Page<User> page = userService.page(new Page<>(current, size));
return Result.success(page);
}
}
接口列表如下:
| 请求方式 | 路径 | 作用 |
|---|---|---|
| GET | /users | 查询全部用户 |
| GET | /users/{id} | 根据 ID 查询用户 |
| POST | /users | 新增用户 |
| PUT | /users | 修改用户 |
| DELETE | /users/{id} | 删除用户 |
| GET | /users/page | 分页查询用户 |
Controller 中所有接口均返回:
Result<T>
统一返回格式如下:
{
"code": 0,
"message": "success",
"data": {}
}
查询接口需要返回数据:
Result<User>
Result<List<User>>
Result<Page<User>>
新增、修改和删除接口只需要表示执行成功或失败,因此返回:
Result<Void>
12. 故障演练:分页插件未配置导致分页失效
12.1 故意不添加分页拦截器
完成 Controller 后,暂时不要创建:
MyBatisPlusConfig
直接启动 CloudUserApplication,访问:
GET http://localhost:9200/users/page?current=1&size=2
请求参数表示:
current = 1:查询第 1 页
size = 2:每页最多返回 2 条数据
理论上,第一页应该只返回:
zhangsan
lisi
但是实际响应中返回了全部 5 条用户数据:
{
"code": 0,
"message": "success",
"data": {
"records": [
{
"id": 1,
"username": "zhangsan"
},
{
"id": 2,
"username": "lisi"
},
{
"id": 3,
"username": "wangwu"
},
{
"id": 4,
"username": "zhaoliu"
},
{
"id": 5,
"username": "sunqi"
}
],
"total": 0,
"size": 2,
"current": 1,
"pages": 0
}
}

可以观察到三个异常现象:
1. size 明明等于 2,但 records 返回了全部 5 条数据
2. total 等于 0
3. pages 等于 0
12.2 原因分析
Controller 中已经创建了分页对象:
new Page<>(current, size)
但是,Page 对象本身只是保存分页参数。
它记录:
当前页
每页数量
总记录数
总页数
分页结果
它不会自动修改 SQL。
真正负责改写 SQL 的是:
PaginationInnerInterceptor
没有配置分页拦截器时,数据库执行的 SQL 仍然接近:
SELECT id, username, nickname, phone, status, create_time
FROM t_user;
没有自动追加:
LIMIT 0, 2;
也不会执行统计总数的 SQL:
SELECT COUNT(*) AS total
FROM t_user;
一句话总结:
Page 负责保存分页参数,分页拦截器负责改写 SQL。
13. 修复分页问题:添加 MyBatisPlusConfig
创建:
cloud-user
└── src/main/java
└── com.example.cloud.user.config
└── MyBatisPlusConfig.java
代码如下:
package com.example.cloud.user.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MyBatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
PaginationInnerInterceptor paginationInnerInterceptor =
new PaginationInnerInterceptor(DbType.MYSQL);
interceptor.addInnerInterceptor(paginationInnerInterceptor);
return interceptor;
}
}
重新启动 CloudUserApplication。
再次请求:
GET http://localhost:9200/users/page?current=1&size=2
修复后的响应接近:
{
"code": 0,
"message": "success",
"data": {
"records": [
{
"id": 1,
"username": "zhangsan",
"nickname": "张三"
},
{
"id": 2,
"username": "lisi",
"nickname": "李四"
}
],
"total": 5,
"size": 2,
"current": 1,
"pages": 3
}
}
此时:
records:只返回 2 条数据
total:总记录数为 5
size:每页数量为 2
current:当前页为 1
pages:总页数为 3

控制台中可以观察到类似 SQL:
SELECT COUNT(*) AS total
FROM t_user;
SELECT id, username, nickname, phone, status, create_time
FROM t_user
LIMIT 2;
14. 使用 IDEA HTTP Client 测试接口
在下面位置创建:
cloud-user
└── src
└── test
└── http
└── user.http
内容如下:
### 查询全部用户
GET http://localhost:9200/users
### 根据 ID 查询用户
GET http://localhost:9200/users/1
### 查询不存在的用户
GET http://localhost:9200/users/999
### 分页查询:第一页
GET http://localhost:9200/users/page?current=1&size=2
### 分页查询:第二页
GET http://localhost:9200/users/page?current=2&size=2
### 新增用户
POST http://localhost:9200/users
Content-Type: application/json
{
"username": "test_user",
"nickname": "测试用户",
"phone": "13800000006",
"status": 1
}
### 修改用户
PUT http://localhost:9200/users
Content-Type: application/json
{
"id": 6,
"nickname": "修改后的昵称",
"phone": "13800000007",
"status": 1
}
### 删除用户
DELETE http://localhost:9200/users/6


IDEA HTTP Client 的优点:
测试文件可以直接保存在项目中
不需要频繁手动拼接 URL
可以和代码一起提交到版本库
适合当前阶段快速验证接口
本课程后续会在专门章节接入 Knife4j。
现阶段先使用 IDEA HTTP Client 测试接口,可以避免过早引入额外依赖和配置。
15. 本章常见问题
15.1 为什么要保留 Controller、Service、Mapper 三层结构?
即使当前示例业务简单,也不建议在 Controller 中直接调用 Mapper。
三层职责不同:
Controller:接收 HTTP 请求,返回 HTTP 响应
Service:处理业务逻辑
Mapper:访问数据库
后续加入缓存、事务、远程调用和消息队列后,Service 层会承载更多业务编排逻辑。
15.2 BaseMapper 和 IService 有什么区别?
BaseMapper<T> 属于数据访问层,提供基础 SQL 操作:
selectById
selectList
insert
updateById
deleteById
IService<T> 属于业务层,提供更适合业务代码调用的方法:
getById
list
save
updateById
removeById
page
本质关系:
Controller
↓
Service
↓
Mapper
↓
MySQL
15.3 为什么创建了 Page 对象,分页仍然不生效?
因为:
Page 只保存分页参数
PaginationInnerInterceptor 才负责修改 SQL
没有分页拦截器时,SQL 不会自动增加:
LIMIT
因此会返回全部数据。
15.4 为什么分页失效时 total 和 pages 都是 0?
因为分页拦截器没有生效时,MyBatis-Plus 不会自动执行总数统计 SQL:
SELECT COUNT(*)
所以:
total = 0
pages = 0
15.5 为什么 MyBatisPlusConfig 放在 cloud-user 中?
因为当前分页能力只在 cloud-user 中使用。
配置类放在:
com.example.cloud.user.config
可以被 CloudUserApplication 自动扫描。
后续如果多个服务都需要相同配置,可以再考虑:
抽取公共配置
建立 starter
复制最小配置
学习阶段先保持简单清晰。
15.6 为什么现在不接入 Knife4j?
Knife4j 在本课程中安排在后续专门章节统一接入。
当前阶段使用:
IDEA HTTP Client
已经足够验证接口。
等用户、商品、订单和网关服务基本完成后,再统一处理:
接口文档
接口分组
请求参数说明
统一返回展示
网关转发
多服务文档聚合
这样不会在当前章节引入过多额外配置,打断学习主线。
15.7 为什么启动类要扩大扫描范围?
启动类默认只扫描自身包和子包。
CloudUserApplication 位于:
com.example.cloud.user
而公共模块中的异常处理器位于:
com.example.cloud.common
如果不扩大扫描范围,两个包不是父子关系,公共模块中的 Spring Bean 可能不会被扫描。
因此使用:
@SpringBootApplication(scanBasePackages = "com.example.cloud")
统一扫描公共根包。
17. 本章结论
本章完成了第一个可以独立运行的 Spring Boot 业务服务:
cloud-user
当前项目已经具备:
MySQL 用户表
Spring Boot Web 接口
MyBatis-Plus 数据访问
Controller / Service / Mapper 分层
统一返回 Result<T>
用户 CRUD
分页查询
IDEA HTTP Client 接口测试
MyBatis-Plus 分页故障演练
本章不仅完成了用户服务功能,还通过分页失效问题理解了:
Page 对象与分页拦截器的职责区别
分页插件如何改写 SQL
为什么排查问题时要关注 SQL 日志
下一章将进入:
第 3 章:商品服务 cloud-product 开发
下一章会完成:
商品表设计
商品服务独立启动
商品详情查询
商品列表查询
库存扣减接口
库存不足业务校验
并发扣减库存风险分析
更多推荐

所有评论(0)