从单体到微服务:Controller、Service、RPC、Feign、Mapper 到底该怎么写
刚开始从单体项目转到微服务项目时,很容易被这些东西绕晕:
Controller
Service
Mapper
RPC
Feign
Nacos
Gateway
api 模块
biz 模块
其实微服务并没有推翻我们以前熟悉的 Spring Boot 开发方式。
最核心的变化只有一个:
原来一个项目内部的方法调用,现在可能变成两个服务之间的网络调用。
只要把这一点想明白,整个微服务代码结构就会清晰很多。
一、先回忆单体项目怎么写
以前做单体 Spring Boot 项目,一个业务通常就是:
请求
↓
Controller
↓
Service
↓
Mapper
↓
MySQL
例如修改用户昵称:
UserController
↓
UserService
↓
UserMapper
↓
user 表
所有代码都运行在:
同一个 Spring Boot
同一个 JVM
同一个进程
所以 Service 之间可以直接调用 Java 方法。
二、微服务其实就是把大项目拆开
原来:
xiaohashu
├── 登录
├── 用户
├── 文件
├── 笔记
└── 评论
现在拆成:
xiaohashu-auth
xiaohashu-user
xiaohashu-oss
xiaohashu-note
xiaohashu-comment
可以把每个微服务理解成:
一个独立运行的小型 Spring Boot 项目。
例如 User 服务内部依然是:
Controller
↓
Service
↓
Mapper
↓
Database
这一部分和以前写单体基本没有区别。
真正不同的是:
如果 User 需要 OSS 帮忙,不能再直接调用 OSS 的 Service。
因为它们已经是两个独立进程。
这时候就需要远程调用。
三、以后拿到需求,第一件事不是写代码
以后拿到一个需求,先问:
这个功能到底属于哪个服务?
比如:
修改昵称
明显属于:
User 服务
再比如:
上传图片
属于:
OSS 服务
再比如:
登录、Token、认证
属于:
Auth 服务
所以以后第一步一定是:
需求
↓
判断服务归属
而不是直接开始写 Controller。
四、第二步:当前服务自己能完成吗?
这是微服务开发中特别重要的判断。
假设现在要:
修改昵称
User 服务自己就拥有用户数据。
那么:
UserController
↓
UserService
↓
UserMapper
↓
MySQL
直接完成。
不需要 Feign。
但是如果需求是:
修改头像
User 服务负责:
修改用户资料
但是图片上传属于:
OSS 服务
那么 User 自己做不完。
于是:
User
↓
调用 OSS
这时候才需要:
RPC
Feign
所以判断方式非常简单:
当前服务自己能完成?
↓
能
↓
Controller → Service → Mapper
如果:
当前服务自己做不了?
↓
需要另外一个服务
↓
Service → RPC → Feign → 远程服务
五、Controller 到底负责什么?
Controller 可以简单理解成:
负责接请求。
主要做:
接收参数
参数校验
调用 Service
返回结果
例如:
public Response<?> updateAvatar(MultipartFile file) {
return userService.updateAvatar(file);
}
Controller 不应该塞大量复杂业务逻辑。
所以以后看到 Controller,脑子里就想:
“请求到了,我把它交给 Service。”
六、Service 到底负责什么?
Service 是整个业务里面最重要的一层。
可以把它理解成:
业务流程的总指挥。
例如修改头像:
上传图片
↓
拿到图片 URL
↓
获取当前 userId
↓
更新用户头像
这整个流程应该由:
UserService
负责组织。
但是 Service 不一定亲自完成所有工作。
比如:
上传图片
交给 OSS。
更新数据库
交给 Mapper。
所以:
Controller
负责接请求
Service
负责业务编排
Mapper
负责数据库
Feign
负责调用远程服务
这是以后写微服务最重要的一组职责划分。
七、Mapper 什么时候使用?
以后准备写:
xxxMapper.update(...)
之前,先问一句:
这个数据是不是属于当前微服务?
例如:
UserService
↓
UserMapper
合理。
因为用户数据属于 User。
NoteService
↓
NoteMapper
也合理。
因为笔记属于 Note。
但是:
AuthService
↓
UserMapper
就需要警觉。
因为用户数据应该属于:
User 服务
所以更合理的是:
AuthService
↓
UserRpcService
↓
UserFeignApi
↓
User 服务
↓
UserService
↓
UserMapper
教程中修改密码的重构就是这种思路:认证服务不再直接更新用户数据库,而是调用用户服务完成密码持久化。
后来认证服务中原来的 Mapper、MyBatis、数据库相关代码也被清理掉了。
这背后的核心思想就是:
谁的数据,尽量由谁自己管理。
八、Feign 到底负责什么?
如果两个 Service 在同一个项目:
Service A
↓
Service B
直接:
serviceB.xxx();
就可以。
但是如果:
Auth
和:
User
已经是两个独立微服务,就不能直接调用 Java 对象。
于是:
Auth
↓
HTTP
↓
User
OpenFeign 就是把这个 HTTP 请求包装成:
userFeignApi.updatePassword();
看起来像普通 Java 方法。
但是底层其实是:
POST /user/password/update
教程中的 UserFeignApi.updatePassword() 就对应着用户服务的密码更新 HTTP 接口。
所以可以记住:
Feign = 把远程 HTTP 调用写成 Java 方法调用。
九、RPC 层又是干嘛的?
项目里可能还有:
UserRpcService
结构变成:
AuthService
↓
UserRpcService
↓
UserFeignApi
其实不是必须有这一层。
它主要是为了:
把远程调用相关代码和真正的业务代码分开。
例如:
AuthService
只想表达:
更新用户密码
于是写:
userRpcService.updatePassword(password);
而 RPC 层内部负责:
创建 DTO
↓
调用 Feign
↓
处理返回值
↓
处理远程异常
教程中的 UserRpcService 就是先构造更新密码 DTO,然后调用 UserFeignApi。
所以可以理解成:
Service
负责业务
RPC
负责和其他微服务打交道
Feign
负责真正发送 HTTP 请求
十、api 模块又是什么?
例如:
xiaohashu-user-api
它不是一个需要启动的微服务。
可以把它理解成:
User 服务对外公布的接口说明书。
里面通常放:
FeignClient
DTO
服务名称常量
例如:
UserFeignApi
UpdateUserPasswordReqDTO
FindUserByPhoneReqDTO
相当于 User 服务告诉其他服务:
我提供哪些接口
你应该传什么数据
我会返回什么数据
而真正处理业务的:
xiaohashu-user-biz
才会启动。
所以:
user-api
= 对外协议
user-biz
= 真正业务实现
十一、完整走一遍:修改头像
需求:
用户修改头像。
首先判断:
用户资料
↓
属于 User 服务
所以入口:
UserController
流程:
客户端
↓
Gateway
↓
UserController
↓
UserService
UserService 发现:
我要修改头像
↓
必须先上传图片
但是上传文件属于:
OSS 服务
所以:
UserService
↓
OssRpcService
↓
FileFeignApi
↓
OSS 服务
OSS 收到请求以后,又回到了我们熟悉的模式:
FileController
↓
FileService
↓
FileStrategy
↓
Minio / Aliyun
上传成功得到:
avatarUrl
然后返回 User:
OSS
↓
Feign
↓
RpcService
↓
UserService
UserService 再:
获取 userId
↓
UserMapper
↓
保存 avatarUrl
完整链路:
客户端
↓
Gateway
↓
UserController
↓
UserService
↓
OssRpcService
↓
FileFeignApi
↓
OSS 服务
↓
FileController
↓
FileService
↓
Minio / Aliyun
↓
返回图片 URL
↓
UserService
↓
UserMapper
↓
MySQL
十二、再走一遍:修改密码
修改密码可能先进入:
Auth 服务
因为 Auth 负责密码处理和认证。
流程:
AuthController
↓
AuthService
↓
BCrypt 加密密码
但是保存用户密码属于:
User 服务
所以:
AuthService
↓
UserRpcService
↓
UserFeignApi
↓
User 服务
到了 User 服务之后:
UserController
↓
UserService
↓
UserMapper
↓
MySQL
最终:
客户端
↓
Gateway
↓
Auth
↓
密码加密
↓
Feign
↓
User
↓
Mapper
↓
数据库
所以修改头像和修改密码看起来业务不同,其实代码结构完全是同一个套路。
十三、以后开发新功能,可以固定使用这套流程
以后拿到需求,先在脑子里跑:
第一步:
这个业务属于哪个服务?
第二步:
当前服务自己能不能完成?
第三步:
如果自己能完成:
Controller
↓
Service
↓
Mapper
如果需要其他服务:
Controller
↓
Service
↓
RPC
↓
Feign
↓
远程微服务
↓
Controller
↓
Service
↓
Mapper
最后再考虑:
userId 怎么传?
Token 怎么校验?
事务怎么处理?
Redis 要不要用?
异常怎么返回?
不要一上来就想着这些组件。
先把:
业务属于谁、数据属于谁、谁调用谁
弄明白。
十四、微服务开发的核心心法
以后看到这些类,可以这样理解:
Controller
有人来找我了
Service
这个业务具体怎么完成
Mapper
我要操作自己的数据库
RPC
我要找另一个微服务帮忙
Feign
我要真正发送 HTTP 请求
api 模块
这是我的对外调用协议
Nacos
我要调用的服务在哪里
Gateway
外部请求从哪里进入系统
十五、最终整体结构
以后写微服务,脑子里可以一直保留这张图:
客户端
↓
Gateway
↓
当前服务 Controller
↓
Service
↙ ↘
自己能完成 需要别的服务
↓ ↓
Mapper RPC
↓ ↓
自己的数据库 Feign
↓
Nacos
↓
目标微服务
↓
Controller
↓
Service
↓
Mapper
↓
数据库
所以微服务并没有把以前的开发方式推翻。
每一个微服务内部依然是:
Controller
↓
Service
↓
Mapper
只是当业务跨越服务边界的时候,中间多出了:
RPC
↓
Feign
↓
HTTP
最终可以用一句话总结:
先判断业务属于哪个服务;本服务能做就 Service → Mapper,本服务做不了就 Service → RPC → Feign → 目标服务;谁的数据,尽量由谁自己操作。
只要这套思维形成,以后再写微服务功能,就不会先被 Nacos、Feign、RPC 这些组件绕进去,而是先从业务流程出发,再自然决定代码应该放在哪一层。