资讯动态

Spring Cloud微服务整合CRUD:从Nacos注册到OpenFeign调用的完整实践

发布时间:2026/10/4 7:13:41 来源:尧图企业网站定制
1. 为什么这个系列的第一篇要写CRUD微服务落地的第一块敲门砖先说一个很多人容易误解的点Spring Cloud 是个宏观的微服务治理方案名字里带个云字但实际上它解决的不是业务代码怎么写而是多个服务进程之间怎么组织、怎么发现、怎么通信、怎么容错。CRUD 则是任何业务系统里最基础、也最绕不开的数据操作能力。把 Spring Cloud 和 CRUD 放到一起整合本质上是做这样一件事让一个业务模块以微服务的方式跑起来并且通过注册中心、网关、服务调用这些 Spring Cloud 核心组件完成完整的请求链路。我自己的体会是网上一搜 Spring Cloud铺天盖地都是注册中心原理网关过滤器分布式事务这些偏治理侧的内容反而很少有人把一个最简单的用户模块从数据库到前端接口完整走通微服务调用链这件事讲清楚。但实际项目落地的时候你第一步需要的就是一个能跑通的 CRUD 服务因为它的链路最短、问题最直观、也最适合用来验证整套基础设施是否正常。这篇文章定位为Spring Cloud 整合一适合两类人一类是刚接触微服务、想搭一套本地可运行 Demo 的开发者另一类是已经用单体写过很多 CRUD、想看看同一套业务代码放到微服务架构里会发生什么变化的同学。文章里我会把选型理由、配置细节、启动顺序、踩坑记录一次讲透保证你照着做能跑起来而不是看了一堆抽象概念。整体架构我会控制在三个服务内一个用于演示 CRUD 的user-service一个负责转发请求的网关服务gateway-service再加上注册中心 Nacos。后文还会引入 OpenFeign 演示服务间调用这也是微服务场景下最典型的用法。2. 版本选型与父工程搭建这一步决定后面三个月是否顺利2.1 Spring Cloud 与 Spring Boot 的版本对应关系Spring Cloud 的版本号经历了一次重要变化。早期用Dalston、Edgware、Hoxton这类伦敦地铁站名来命名从2020.0开始改成了年份命名方式。很多新手在这里第一个坑就是Spring Cloud 和 Spring Boot 必须严格匹配否则启动时会报各种莫名其妙的 NoClassDefFoundError 或者 Bean 创建异常。我这里直接给出两套经过验证的稳定组合你可以按自己的 JDK 情况选组合JDKSpring BootSpring CloudSpring Cloud Alibaba传统稳定组合8/112.7.182021.0.82021.0.5.0新版组合173.2.x2023.0.x2023.0.1.0我个人建议如果是为了学习和本地验证优先选第一套JDK 8 Spring Boot 2.7.18 Spring Cloud 2021.0.8。原因很现实这套组合的网上资料最多遇到问题一搜就有答案而且 Nacos、Gateway、OpenFeign 这些组件的兼容性都已经被大量生产项目验证过了。Spring Boot 3.x 虽然新但 Jakarta EE 的包名迁移、Spring Security 6 的配置变化对初学者来说排查成本偏高。2.2 父 POM 的依赖管理我会创建一个 Maven 父工程只放依赖管理和公共属性不写业务代码。这样做的好处是子模块之间版本统一后续引入新的微服务模块时不需要再重复指定版本号。父 POM 的核心内容如下parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent properties spring-cloud.version2021.0.8/spring-cloud.version spring-cloud-alibaba.version2021.0.5.0/spring-cloud-alibaba.version mybatis-plus.version3.5.3.2/mybatis-plus.version /properties dependencyManagement dependencies dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-dependencies/artifactId version${spring-cloud.version}/version typepom/type scopeimport/scope /dependency dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-alibaba-dependencies/artifactId version${spring-cloud-alibaba.version}/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement注意dependencyManagement里的import方式。它不像普通依赖那样把 jar 直接引入而是把对应 BOM 里的版本管理信息导入当前 POM。这样你在子模块里写依赖的时候可以不加versionMaven 会自动找父工程里锁定的版本。2.3 子模块的拆分方式父工程下我会建两个子模块分别对应两个可启动的服务spring-cloud-crud-demo ├── pom.xml # 父工程 ├── gateway-service/ # 网关服务 └── user-service/ # 用户服务承载 CRUD 业务严格来说网关也是微服务的一个成员所以它也要注册到 Nacos只是它的职责不是处理业务而是做路由转发。两个子模块的pom.xml里只需声明自身需要的依赖即可。user-service的依赖如下dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-starter-alibaba-nacos-discovery/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version${mybatis-plus.version}/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-starter-openfeign/artifactId /dependency /dependenciesgateway-service的依赖则要注意不要加spring-boot-starter-web。这一点我在后面会专门讲Gateway 基于 WebFlux 响应式模型和传统的 Servlet Web 容器冲突加了就启动报错。3. 注册中心接入用 Nacos 让服务先彼此看见3.1 为什么选择 Nacos 而不是 EurekaEureka 2.x 已经停止维护这已经是共识。Nacos 是阿里巴巴开源的服务发现与配置管理组件目前在国内微服务项目中占绝对主流。选它的理由很简单既能做服务注册发现又能做配置中心一套组件解决两个问题而且和 Spring Cloud Alibaba 生态结合得非常顺滑。后文如果继续写这个系列配置中心的整合就会直接基于 Nacos不需要额外引入新组件。3.2 本地启动 Nacos ServerNacos Server 的启动方式有源码编译、Docker、下载发行包三种。对于本地验证我推荐直接下载最新稳定版发行包# 解压后进入 bin 目录Linux/macOS 执行 sh startup.sh -m standalone # Windows 执行 startup.cmd -m standalonestandalone参数表示单机模式Nacos 默认使用内置的 Derby 数据库存储服务实例信息不需要额外安装数据库。启动成功后访问http://127.0.0.1:8848/nacos默认用户名密码都是nacos能看到控制台界面就说明注册中心已经就绪。3.3 user-service 接入 Nacosuser-service的application.yml中配置如下server: port: 8081 spring: application: name: user-service cloud: nacos: discovery: server-addr: 127.0.0.1:8848 namespace: public group: DEFAULT_GROUP datasource: url: jdbc:mysql://127.0.0.1:3306/crud_demo?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: auto几个关键点解释一下spring.application.name是服务在注册中心里的唯一标识也是后面服务间调用和网关路由的关键依据。名字别乱起建议用中划线分隔比如user-service而不是userService。namespace和group可以在没有显式配置时省略默认就是public和DEFAULT_GROUP。但我在实际项目里建议一开始就显式写出来因为后续做环境隔离dev/test/prod 各占一个 namespace时你会理解这两个参数的意义。3.4 启动类上加注解在UserServiceApplication上加上EnableDiscoveryClientSpringBootApplication EnableDiscoveryClient public class UserServiceApplication { public static void main(String[] args) { SpringApplication.run(UserServiceApplication.class, args); } }在 Spring Cloud 2021.x 版本里注册发现能力已经默认开启不加这个注解也能注册但我建议保留。原因有两个一是显式表明这个服务要参与服务发现二是如果你做一些自定义的注册逻辑或者需要注入 DiscoveryClient 对象时这个注解会让代码意图更清晰。启动user-service后回到 Nacos 控制台服务列表里应该能看到user-service已经注册上来状态为健康。4. CRUD 三件套的实现实体、持久层、接口层微服务架构下的业务代码写法其实和单体项目没有本质区别。这也是我特别想强调的一点Spring Cloud 的整合重点在基础设施和通信链路而不是让你把熟悉的 CRUD 写法推翻重来。4.1 数据库准备先准备一张简单的用户表CREATE DATABASE IF NOT EXISTS crud_demo DEFAULT CHARACTER SET utf8mb4; USE crud_demo; CREATE TABLE user ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键ID, username VARCHAR(64) NOT NULL COMMENT 用户名, email VARCHAR(128) DEFAULT NULL COMMENT 邮箱, phone VARCHAR(20) DEFAULT NULL COMMENT 手机号, created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id) ) ENGINE InnoDB DEFAULT CHARSET utf8mb4 COMMENT 用户表;这里有个字段命名的小细节数据库字段用下划线风格created_atJava 实体用驼峰风格createdAt。MyBatis-Plus 的map-underscore-to-camel-case配置会自动做映射省去大量手写 ResultMap 的工作。我在 3.3 里的配置已经打开了这个开关。4.2 实体类与 Mapper实体类对应表结构Data TableName(user) public class User { TableId(type IdType.AUTO) private Long id; private String username; private String email; private String phone; private LocalDateTime createdAt; private LocalDateTime updatedAt; }TableName(user)注解是因为user这个表名不是 Java 命名规范的驼峰形式如果不注解MyBatis-Plus 会默认映射到user实体类名正好也能对上但为了保险起见还是显式标注。Mapper 接口更简单Mapper public interface UserMapper extends BaseMapperUser { }BaseMapperT是 MyBatis-Plus 提供的通用 CRUD Mapper内置了selectById、insert、updateById、deleteById、selectList等方法。正常情况下你不需要写任何 XML 映射文件也不需要写 SQL 语句。这套写法的好处是一个实体对应一个 MapperCRUD 的基础方法全部开箱即用。4.3 Service 层的接口与实现Service 层建议按接口 实现类的模式拆开虽然代码量多一些但在业务复杂后你会受益于这种隔离public interface UserService { User getUserById(Long id); Long createUser(User user); Boolean updateUser(User user); Boolean deleteUser(Long id); ListUser listUsers(); } Service RequiredArgsConstructor public class UserServiceImpl implements UserService { private final UserMapper userMapper; Override public User getUserById(Long id) { return userMapper.selectById(id); } Override public Long createUser(User user) { user.setCreatedAt(LocalDateTime.now()); user.setUpdatedAt(LocalDateTime.now()); userMapper.insert(user); return user.getId(); } Override public Boolean updateUser(User user) { user.setUpdatedAt(LocalDateTime.now()); return userMapper.updateById(user) 0; } Override public Boolean deleteUser(Long id) { return userMapper.deleteById(id) 0; } Override public ListUser listUsers() { return userMapper.selectList(null); } }注意构造器注入的写法。在 Spring 官方文档里构造器注入是推荐的依赖注入方式它让依赖关系不可变、不容易产生循环依赖也方便单元测试。我早期写 Spring 项目时习惯用Autowired字段注入直到一次排查空指针问题发现是依赖没有初始化完成从那以后就转向了构造器注入。4.4 Controller 层与统一返回结构Controller 层直接暴露 HTTP 接口但我不建议直接把实体返回给前端更规范的做法是引入一个统一返回体。这里用一个极简的RT类Data public class RT { private Integer code; private String message; private T data; public static T RT ok(T data) { RT r new R(); r.setCode(200); r.setMessage(success); r.setData(data); return r; } public static T RT error(String message) { RT r new R(); r.setCode(500); r.setMessage(message); return r; } }Controller 如下RestController RequestMapping(/user) RequiredArgsConstructor public class UserController { private final UserService userService; GetMapping(/{id}) public RUser getUserById(PathVariable Long id) { return R.ok(userService.getUserById(id)); } PostMapping public RLong createUser(RequestBody User user) { return R.ok(userService.createUser(user)); } PutMapping public RBoolean updateUser(RequestBody User user) { return R.ok(userService.updateUser(user)); } DeleteMapping(/{id}) public RBoolean deleteUser(PathVariable Long id) { return R.ok(userService.deleteUser(id)); } GetMapping public RListUser listUsers() { return R.ok(userService.listUsers()); } }到这一步user-service已经是一个具备完整 CRUD 能力的 HTTP 服务了。你可以直接用浏览器或者 Postman 访问http://127.0.0.1:8081/user/1测试一下能返回 JSON 就说明业务侧已经跑通。但这不是微服务的完整形态接下来要做的是把服务纳入网关并演示服务间如何调用。5. 网关与服务间调用请求链路的最后一公里5.1 Gateway 的角色定位与配置网关是微服务架构里所有外部请求的统一入口。你可以把它理解成一个前台收发室外面的人不知道每个办公室服务具体在哪只要把快递请求交给收发室收发室根据地址路由规则分发给对应的办公室。创建gateway-service子模块pom.xml引入dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-starter-gateway/artifactId /dependency dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-starter-alibaba-nacos-discovery/artifactId /dependency配置文件server: port: 8080 spring: application: name: gateway-service cloud: nacos: discovery: server-addr: 127.0.0.1:8848 gateway: routes: - id: user-service-route uri: lb://user-service predicates: - Path/api/user/** filters: - StripPrefix1 logging: level: org.springframework.cloud.gateway: debug这里的路由配置拆开看id路由的唯一标识自定义即可。uri: lb://user-servicelb://前缀表示启用负载均衡后面跟的是服务名Gateway 会从 Nacos 拉取user-service的实例列表并按负载均衡策略进行分发。predicates路由断言Path/api/user/**表示以/api/user/开头的请求都会命中这条路由。filters: StripPrefix1转发到下游服务前去掉路径中的第一级前缀。举个例子外部请求GET http://127.0.0.1:8080/api/user/1经过 Gateway 后StripPrefix1会去掉api变成/user/1然后转发到user-service的/user/1接口。这样前端调用可以统一带/api前缀而每个微服务内部接口不需要关心外部前缀是什么。5.2 为什么 Gateway 不能和 spring-boot-starter-web 共存这是一个高频踩坑点。Gateway 底层基于 Spring WebFlux使用的是 Reactor Netty 响应式模型spring-boot-starter-web则是传统的 Servlet 模型基于 Tomcat。两者同时出现在类路径下时Spring Boot 的自动配置会发生冲突典型报错是Spring MVC found on classpath, which is incompatible with Spring Cloud Gateway解决方案只有一个gateway-service 的 pom.xml 里不要引入spring-boot-starter-web。如果你是从别的项目拷贝 pom 过来很容易带出这个依赖启动时报错后需要仔细核对依赖树mvn dependency:tree -Dincludesorg.springframework.boot:spring-boot-starter-web5.3 用 OpenFeign 实现服务间调用CRUD 场景里网关把请求路由到user-service就够了但很多时候业务会有跨服务的数据需求。比如订单服务需要查询用户信息这就涉及服务间调用。OpenFeign 是 Spring Cloud 生态里最主流的声明式 HTTP 客户端。先在一个独立的模块或者任意服务里定义 Feign 接口。这里为了演示我在user-service里加一个UserClient模拟被网关服务调用FeignClient(name user-service, path /user) public interface UserClient { GetMapping(/{id}) RUser getById(PathVariable(id) Long id); }FeignClient注解里的name是目标服务在注册中心的服务名path是公共路径前缀。方法定义和 Spring MVC Controller 的映射写法一致OpenFeign 会在运行时动态生成实现类发 HTTP 请求到目标服务的对应接口。调用方启动类加上EnableFeignClientsEnableFeignClients SpringBootApplication public class SomeServiceApplication { public static void main(String[] args) { SpringApplication.run(SomeServiceApplication.class, args); } }然后就可以像注入普通 Service 一样使用UserClientService public class OrderService { private final UserClient userClient; public User getUserInfo(Long userId) { return userClient.getById(userId); } }这里 OpenFeign 会配合负载均衡组件以服务名user-service从注册中心拿到真实地址然后完成调用。整个过程对业务代码完全透明你不需要关心目标服务的 IP 和端口。5.4 负载均衡策略的小知识在 Spring Cloud 2021.x 里原来的 Ribbon 客户端已经被 Spring Cloud LoadBalancer 取代。默认的负载均衡策略是轮询Round Robin如果你需要改成随机策略可以通过配置实现。不过在刚开始整合阶段轮询够用了先跑通链路再说不要过早优化策略。6. 本地完整启动验证与高频踩坑记录6.1 启动顺序很重要我把启动流程固定下来按这个顺序操作可以最大程度减少排查成本启动 MySQL确认crud_demo库和user表存在。启动 Nacos Server浏览器访问控制台确认可用。启动user-service确认 Nacos 服务列表中出现该服务。启动gateway-service同样确认注册成功。通过网关发起请求验证完整链路。验证请求建议用这几个# 新增用户注意走网关端口 8080 curl -X POST http://127.0.0.1:8080/api/user \ -H Content-Type: application/json \ -d {username: test_user, email: testexample.com, phone: 13800000000} # 查询用户 curl http://127.0.0.1:8080/api/user/1 # 查询所有用户 curl http://127.0.0.1:8080/api/user # 更新用户 curl -X PUT http://127.0.0.1:8080/api/user \ -H Content-Type: application/json \ -d {id: 1, username: updated_user, email: updatedexample.com} # 删除用户 curl -X DELETE http://127.0.0.1:8080/api/user/1如果删掉了用户记得再插一条数据做后续测试或者把删除请求放到最后执行。6.2 我实际遇到过的几个问题问题一Nacos 注册成功但 Gateway 转发报 503这个问题的典型场景是user-service在 Nacos 里显示健康但通过网关访问时返回 503 Service Unavailable。排查步骤我也是踩了几次坑才总结出来的先直接访问http://127.0.0.1:8081/user/1确认服务本身健康。再检查 Gateway 的配置重点看uri是不是lb://user-service拼写是否正确。lb://不能少少了下游地址无法解析。看 Gateway 日志如果出现Unable to load instance之类的关键字多半是 Nacos 上服务的实例状态异常可能是注册了多个环境比如 dev 和 test 用了同一个 Nacos可以通过namespace隔离。问题二MyBatis-Plus 的 LocalDateTime 反序列化报错前端传 JSON 给后端后端返回 JSON 给前端LocalDateTime 字段会面临序列化格式问题。如果不做配置返回的可能是数组形式的[2025, 3, 15, 10, 30, 0]前端解析会很痛苦。我在user-service里加了统一的 Jackson 配置spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8注意这个配置对LocalDateTime类型的字段不一定生效因为 LocalDateTime 默认是由 JSR310 模块序列化的。为了稳妥我在字段上添加了注解JsonFormat(pattern yyyy-MM-dd HH:mm:ss) private LocalDateTime createdAt;问题三启动时端口被占本地调试多个微服务时端口冲突是常客。Linux/macOS 用lsof -i :8081查看占用端口的进程Windows 用netstat -ano | findstr 8081。也可以直接把微服务的端口号都改成随机端口server: port: 0但这样服务名就成了唯一标识如果你后面调试时要通过固定端口访问反而更麻烦。本地学习阶段还是建议固定端口。问题四OpenFeign 调用时复杂对象参数丢失这是我曾经疏忽的地方Feign 接口方法里如果有多个参数必须用PathVariable或者RequestParam显式标注参数名。如果漏了注解编译时参数名会被擦除导致请求发送出去后目标服务收到null。构建时可以用-parameters参数保留元数据但最稳妥的做法还是每个参数都加上对应注解。6.3 这篇整合对后续系列的意义到这里一个带注册发现、网关路由、服务间调用、全链路 CRUD 的微服务骨架已经完成。你拥有了一个随时可以扩展的基础设施再加业务模块时只需要新建一个子模块写上业务代码注册到 Nacos然后在网关里加一条路由即可。实际上这也是我写这一篇整合的初衷。很多人觉得微服务难不是难在概念而是难在第一次把整套东西串起来。一旦串起来之后就会发现后端的治理组件配置中心、熔断、链路追踪都是在这个骨架上不断做加法。下一篇整合我打算写配置中心的接入也就是把user-service里的数据源配置挪到 Nacos 配置中心做到配置动态刷新。那件事做完之后你会对配置和代码分离有更直观的感受。7. 一组可以直接抄作业的工程配置清单为了照顾不同阅读习惯的同学我把整个工程的关键配置以清单形式再汇总一遍。按照下面的清单核对基本可以避免遗漏。模块配置文件关键配置项父工程 pom.xmlspring-cloud-dependencies2021.0.8统一管理 Spring Cloud 组件版本父工程 pom.xmlspring-cloud-alibaba-dependencies2021.0.5.0统一管理 Nacos 等 Alibaba 组件版本user-serviceapplication.ymlnacos discovery、datasource、mybatis-plus 配置user-servicepom.xmlstarter-web、nacos-discovery、mybatis-plus、openfeign、mysqlgateway-serviceapplication.ymlnacos discovery、gateway routes含 StripPrefix1gateway-servicepom.xmlstarter-gateway、nacos-discovery不引入 starter-web依赖与注解方面的核对点如下启动类上要有SpringBootApplication需要注册发现就加EnableDiscoveryClient需要 Feign 就加EnableFeignClients。Mapper注解让 MyBatis 扫描到 Mapper 接口或者在启动类上使用MapperScan(com.example.mapper)批量扫描。Controller 层统一返回RT后续加全局异常处理器时才能保证异常和正常返回的结构一致。Gateway 路由配置中lb://前缀表示负载均衡服务名要和 Nacos 上注册的一致。8. 关于这个骨架我还想强调的几件事第一微服务整合不是越复杂越好。现在 Spring Cloud 生态里的组件非常多熔断、限流、链路跟踪、分布式事务各有各的场景。但如果你刚起步就把这些全部引入任何一个环节出问题排查起来都会非常痛苦。先维护一个最小可用的链路是我验证过最稳妥的推进方式。第二本地调试时日志就是最好的老师。我在本地调试时会把 MyBatis-Plus 的 SQL 日志打开3.3 配置里的log-impl也把 Gateway 的日志级别调成 debug。这样做的好处是请求到达哪个环节、SQL 执行了什么、路由转发到了哪里全部一目了然。很多同学遇到 500 错误就发懵其实把日志翻一翻很多问题都能找到答案。第三关于 OpenFeign 和 Gateway 的整合顺序我见过有些人先做网关后做 Feign导致调试时链路太多分不清问题在哪。我建议的顺序是先直连确认服务本身没问题再通过 Feign 做服务间调用最后接入网关统一入口。一步一步验证每一步的结论都是确定的这样最终链路出问题时可以迅速定位是哪个环节引入的。这篇文章到这里Spring Cloud 整合 CRUD 就已经全部跑通了。整个过程中我刻意避开了那些炫技式的高级配置只保留必需的部分。理由很简单你先把这条最基础的链路走通下一篇文章在配置中心里改数据源配置时才能意识到什么叫改动一处服务不重启。那才是微服务治理真正有意思的开始。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑