ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Spring Cloud整合实战:从零搭建微服务CRUD骨架

Spring Cloud整合实战:从零搭建微服务CRUD骨架 最近两个星期我一直在忙一件事把一个原本单体结构的后台服务重构成基于 Spring Cloud 的可扩展微服务骨架。目前第一步已经跑通了核心目标很直接——用一套相对标准、接地气的技术栈把用户管理和文章管理这类最经典的 CRUD 场景从零搭起来。整个项目已经运行在我的本地环境上网关、注册中心、配置中心、认证过滤、服务间调用这些东西都真实地跑通了不是只写了 Demo 没运行过。既然系列名字叫“Spring Cloud 整合(一) CRUD”这一篇的重点就很明确如何把 Spring Boot、Spring Cloud Gateway、Nacos、OpenFeign、Redis、MyBatis-Plus 这些组件整合到同一个项目里并且让一条完整的增删改查链路顺畅跑起来。如果你正准备把微服务落地到实际业务中或者已经启动了项目但被各种整合问题卡住这篇内容应该能帮你少踩很多坑。我自己就是从两眼一抹黑的状态走过来的所以很清楚大家看官方文档时最大的痛点每个组件单独看文档都挺正常一组合起来全是版本冲突、依赖报错、服务注册不上。这篇博客相当于我把整个实践过程重新走了一遍把值得说的地方都记下来了。1. 内容整体设计与思路拆解1.1 这不是一个空壳 Demo而是一条真实业务链路项目标题里的“CRUD”很容易让人误解成“随便写个 Controller 就算完事”但实际拆解下来一个能撑住业务的 CRUD 系统要考虑的东西远不止这些。我在设计这个项目时把完整链路拆成了五个环节外部请求先打进来然后网关做统一的鉴权、路由和跨域处理再通过负载均衡转发到具体的业务服务业务服务内部完成数据访问和缓存处理最后把统一格式的结果返回给调用方。这条链路看起来很基础但每一环都有对应的技术选型和理由。具体到本项目的技术栈我用的是 Spring Boot 2.7.x 作为基础框架Spring Cloud 2021.0.x 负责微服务治理配合 Spring Cloud Alibaba 2021.x 的 Nacos 做服务发现和配置管理网关层选用 Spring Cloud Gateway业务服务内部用 MyBatis-Plus 做数据访问Redis 承担缓存职责服务间调用通过 OpenFeign 完成。这套组合是目前国内中小团队用得最广的一组搭配原因很简单Nacos 既是注册中心又是配置中心省掉了一套 Eureka Config 的维护成本Gateway 基于 WebFlux性能和响应式模型比 Zuul 1.x 舒服很多MyBatis-Plus 在单表 CRUD 场景下几乎不用写 SQL。我最终确认的版本组合会在后面的实操部分详细给出这里先明确一点这套组合可以直接照着用我已经在真实环境里反复验证过了。1.2 为什么我选择了“模块化多工程”的结构项目在物理结构上采用了 Maven 多模块方式而不是单个 Spring Boot 应用。这种做法不是为了显得“有架构感”而是由微服务本身的部署和运维需求决定的。整个工程拆成了五个 Maven 模块micro-common 公共模块、micro-gateway 网关模块、micro-system 系统服务模块、micro-auth 认证服务模块以及一个 micro-api 模块专门存放 Feign 接口定义。每个模块都是独立的 Spring Boot 可启动应用除了 common 和 api 两个纯代码模块。为什么要单独拆出 common 和 api 呢这是我在实践中最有体会的一点。如果各微服务之间需要共享返回体、异常类或者 Feign 客户端接口把它们放在每个服务里复制粘贴后续维护就是一场灾难。单独拆出 common 模块所有服务统一引用返回格式天然一致单独拆出 api 模块Feign 接口的调用方和实现方引用同一份契约不会出现两边参数不一致的诡异问题。服务拆分的原则也要提前确定。我按“领域职责”切分了 system用户、文章等基础实体和 auth登录认证而不是按“功能操作”切分。如果你现在只有一个几十张表的单体应用从“用户、订单、商品”这种业务领域维度拆是最稳的起步方式不要为了拆而拆。1.3 从单体思维到微服务思维的转变点有一个思维转变特别重要单体应用里一个请求进来后Controller 可以直接调用 Service但在微服务环境下请求要先经过网关网关做路由和鉴权然后服务内部可能还要通过 Feign 调用别的服务拿数据。这就带来三个必须提前接受的约束。第一服务间调用会产生网络开销所以不要像写本地方法一样频繁地跨服务调用必要的时候做数据冗余和缓存是合理的。第二网关不能像普通 Spring MVC 工程那样依赖 spring-boot-starter-web因为 Gateway 基于 WebFlux强行引入 web 会导致启动直接失败。第三分布式环境下的配置管理不能再靠本地 application.yml 一把梭必须借助 Nacos 把各环境差异抽出来。后面所有实操步骤本质上都是在解决这三个约束带来的新问题。你能理解这三条后面的内容就顺了理解不了看代码会觉得云里雾里。2. 核心基础环境搭建Nacos 与项目骨架2.1 安装并配置 Nacos避免重启丢配置Nacos 在整个架构里承担的是注册中心和配置中心的双重角色。安装方式很简单去 GitHub 官方 release 页面下载 nacos-server 2.x 压缩包解压后进入 bin 目录Linux 下执行startup.sh -m standaloneWindows 下执行startup.cmd -m standalone单机模式启动。但这里有一个每篇教程几乎都不提的坑Nacos 默认内置的是 Derby 数据库不支持配置的多环境管理和持久化一旦重启 Nacos你在控制台创建的配置全是空的。我自己第一次使用时就踩了这个坑辛辛苦苦配置了一堆内容重启后全没了。正确做法是提前把 Nacos 的配置存储切到 MySQL。步骤也不复杂先在 MySQL 里创建nacos_config数据库然后执行 Nacos 安装包conf目录下的nacos-mysql.sql脚本最后修改conf/application.properties文件加这几行spring.datasource.platformmysql db.num1 db.url.0jdbc:mysql://localhost:3306/nacos_config?characterEncodingutf8connectTimeout1000socketTimeout3000autoReconnecttrueserverTimezoneAsia/Shanghai db.user.0root db.password.0你的数据库密码改完后重启 Nacos配置才能持久化。这一步值得一开始就做不然后面项目搭到一半Nacos 一重启所有配置丢失排查起来非常痛苦。2.2 Maven 依赖版本管理用 BOM 消灭版本冲突微服务项目最容易爆的问题就是版本冲突而最有效的预防手段是借助 BOM 清单统一管理依赖版本。所谓 BOMBill of Materials本质就是一个只包含 dependencyManagement 的 POM 文件只管理版本号不实际引入依赖。我整篇项目的依赖版本组合如下所有子模块继承micro-cloud-parent这个父 POM组件版本说明Spring Boot2.7.18稳定版本大量实战验证Spring Cloud2021.0.8对应 Boot 2.7.xSpring Cloud Alibaba2021.0.5.0对应 Nacos 2.xMyBatis-Plus3.5.3.2单表 CRUD 利器PostgreSQL42.6.0数据库驱动Hutool5.8.20工具类顺手用父 POM 里引入这三个 BOM 是关键操作dependencyManagement dependencies dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-alibaba-dependencies/artifactId version2021.0.5.0/version typepom/type scopeimport/scope /dependency dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-dependencies/artifactId version2021.0.8/version typepom/type scopeimport/scope /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-dependencies/artifactId version2.7.18/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement千万不要手动硬编码各组件版本号。我在早期尝试时试过在子模块里单独指定 Nacos 客户端版本、Feign 版本结果经常遇到 Spring 容器初始化时找不到类定义之类的诡异错误。BOM 用起来之后这些版本兼容性问题基本绝迹了。2.3 公共模块统一返回体和异常处理的必要性micro-common 模块是所有服务都要引用的基础模块它主要负责三件事统一返回体、统一异常状态码、通用工具类。统一返回体的设计非常关键。如果你不统一A 服务的成功返回是{code: 200, data: ...}B 服务的成功返回是{success: true, result: ...}那么网关和前端对接时就要写一堆适配逻辑。更现实的问题是微服务之间的 Feign 调用如果返回体结构不统一调用方解析时会很痛苦。我定义的返回体很朴素没有过度封装public class ResultT implements Serializable { private Integer code; private String message; private T data; // 省略构造函数和 getter/setter public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(success); result.setData(data); return result; } public static T ResultT error(String message) { ResultT result new Result(); result.setCode(500); result.setMessage(message); return result; } }同时定义全局异常处理器用 RestControllerAdvice 把业务异常、参数校验异常、未知异常统一捕获转成上面这个结构返回。这样客户端不管面对哪个服务解析逻辑完全一致。3. 实操核心环节CRUD 业务系统落地3.1 数据库设计与基础配置CRUD 的第一步永远是表结构设计。我准备了两个最贴近业务的示例表用户表sys_user和文章表sys_article。表设计没有搞花活但把微服务场景下常见的审计字段都加上了。文章表的建表语句如下CREATE TABLE sys_article ( id BIGSERIAL PRIMARY KEY, title VARCHAR(200) NOT NULL, summary VARCHAR(500), content TEXT, author_id BIGINT NOT NULL, status SMALLINT DEFAULT 1, create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, deleted SMALLINT DEFAULT 0 );注意几个细节。BIGSERIAL是 PostgreSQL 的自增主键类型和 MySQL 的 AUTO_INCREMENT 一个意思deleted字段是为 MyBatis-Plus 逻辑删除预留的author_id是为了后面演示服务间调用准备的——文章列表需要根据作者ID去 user 服务拿作者名字这就自然引出了 Feign 的使用场景。在application.yml里MyBatis-Plus 的核心配置如下mybatis-plus: mapper-locations: classpath*:/mapper/**/*.xml global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0 id-type: auto configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true逻辑删除和驼峰映射是必然要配的。不做驼峰映射Java 里的createTime永远映射不上数据库里的create_time不开逻辑删除删数据就真删了在这种演示项目里很容易误操作搞坏数据。3.2 实体、Mapper、Service、Controller 的四层实现在微服务工程里写 CRUDMyBatis-Plus 能帮我们把样板代码压到极简。实体类用注解声明表名和主键策略比如文章实体只需要继承一个Model类或者加TableName注解就能和表建立映射。我的文章实体核心代码如下Data TableName(sys_article) public class Article { TableId(type IdType.AUTO) private Long id; private String title; private String summary; private String content; private Long authorId; private Integer status; TableField(fill FieldFill.INSERT) private LocalDateTime createTime; TableField(fill FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; TableLogic private Integer deleted; }Mapper 接口更简单直接继承BaseMapperArticle就得到了单表的所有基础 CRUD 方法连 SQL 都不用写public interface ArticleMapper extends BaseMapperArticle { }Service 接口继承IServiceArticle实现类继承ServiceImplArticleMapper, Article这是 MyBatis-Plus 的常规三板斧。最终 Controller 里面代码也能压到很短PostMapping(/article) public ResultBoolean create(RequestBody Validated Article article) { return Result.success(articleService.save(article)); } DeleteMapping(/article/{id}) public ResultBoolean delete(PathVariable Long id) { return Result.success(articleService.removeById(id)); } PutMapping(/article) public ResultBoolean update(RequestBody Article article) { return Result.success(articleService.updateById(article)); } GetMapping(/article/{id}) public ResultArticle getById(PathVariable Long id) { return Result.success(articleService.getById(id)); }我以为这已经很简洁了但 MyBatis-Plus 还能再往前走一步如果你的接口签名固定可以直接继承它提供的MybatisPlusController模板类连 Controller 代码都能省。不过对于真实业务我一般宁可手写 Controller因为业务逻辑一旦复杂模板类的扩展空间反而受限。3.3 分页查询的完整实现MyBatis-Plus 分页插件分页可以说是 CRUD 里最必不可少又最容易出错的环节。MyBatis-Plus 的分页依赖一个拦截器插件必须在配置类里显式声明否则Page参数传进去会被当成普通参数处理最后查出来的数据不带任何分页效果。分页拦截器配置代码如下Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.POSTGRE_SQL)); return interceptor; } }分页查询接口我刻意设计成一个条件加分的组合场景GetMapping(/article/page) public ResultPageResultArticle page(RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize, RequestParam(required false) String keyword) { LambdaQueryWrapperArticle wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.hasText(keyword), Article::getTitle, keyword) .eq(Article::getStatus, 1) .orderByDesc(Article::getCreateTime); PageArticle page articleService.page(new Page(pageNum, pageSize), wrapper); PageResultArticle result new PageResult(); result.setTotal(page.getTotal()); result.setRecords(page.getRecords()); return Result.success(result); }注意到wrapper.like的第一个参数是boolean condition只有 keyword 有值时才会拼接这个条件。这个技巧非常实用否则你就得写一堆 if 判断去决定是否调用wrapper.like代码能丑好几倍。3.4 字段自动填充创建时间、更新时间不用手写每个表都有create_time和update_time如果每次插入和更新都要在代码里 set 一遍不仅冗余还容易漏。MyBatis-Plus 提供了 MetaObjectHandler 接口可以在插入和更新操作时自动填充指定字段。实现一个自定义的填充处理器Component public class MyMetaObjectHandler implements MetaObjectHandler { Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, createTime, LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } }这时对应实体字段上的TableField(fill FieldFill.INSERT)和TableField(fill FieldFill.INSERT_UPDATE)注解就生效了。之后所有的新增和修改操作这两个时间字段都自动维护不需要在业务代码里出现一行 set。这个机制不仅适用于时间字段还适合填充创建人、创建者所属组织等审计信息。只要当前用户信息能从上下文中拿到在 insertFill 里统一取出来填充就行每个 Service 里都不用再写重复代码。4. 网关层的整合路由、鉴权与统一入口4.1 Spring Cloud Gateway 动态路由配置网关在整个微服务架构中扮演统一入口的角色。所有请求先到网关网关根据路由规则把请求转发到具体服务。我选择 Spring Cloud Gateway除了性能好之外还因为它原生集成了 Nacos 做动态路由不需要重启网关就能调整路由规则。先说依赖问题。Gateway 是响应式框架基于 Spring WebFlux所以这个模块的 pom.xml 里绝不能引入spring-boot-starter-web。一旦引入启动直接报错错误信息还很绕我第一次遇到时排查了半天。网关模块的核心依赖是dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-starter-gateway/artifactId /dependency dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-starter-alibaba-nacos-discovery/artifactId /dependency路由配置我采用“配置文件 负载均衡”的方式。在application.yml中定义了两个服务的路由规则spring: cloud: gateway: routes: - id: micro-system uri: lb://micro-system predicates: - Path/api/system/** filters: - StripPrefix1 - id: micro-auth uri: lb://micro-auth predicates: - Path/api/auth/** filters: - StripPrefix1这里的lb://micro-system是关键。它告诉网关使用 LoadBalancer 客户端从 Nacos 注册中心找到名为 micro-system 的服务实例并按照负载均衡策略分配请求。这是微服务区别于普通反向代理的核心点——目标地址不是写死的 IP而是服务名。4.2 JWT 接口鉴权网关统一校验服务不重复写微服务场景下鉴权逻辑放在网关层做是最合理的。每个业务服务不重复写登录校验网关统一拦截请求、解析 JWT Token、校验身份然后把解析出的用户信息通过请求头传给下游服务。我在 micro-auth 服务里实现了一个标准的登录接口流程是接收用户名密码校验通过后用 JWT 工具类生成一个包含 userId、username 的 Token返回给前端。生成 Token 的 Hutool 写法非常省事String token JWTUtil.createToken(payload, secretKey.getBytes());业务服务并不直接处理 Token。微服务架构里网关层用全局过滤器拦截所有请求核心逻辑包括三件事判断请求路径是否在白名单内不在白名单的请求解析请求头中的 Authorization解析失败则直接返回 401。代码片段如下Component public class AuthGlobalFilter implements GlobalFilter, Ordered { private static final ListString WHITE_LIST Arrays.asList(/api/auth/login); Override public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { String path exchange.getRequest().getURI().getPath(); if (WHITE_LIST.contains(path)) { return chain.filter(exchange); } String token exchange.getRequest().getHeaders().getFirst(Authorization); if (StringUtils.hasText(token) token.startsWith(Bearer )) { token token.substring(7); } try { JWTValidator.of(token).validateDate(); return chain.filter(exchange); } catch (Exception e) { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } } Override public int getOrder() { return -100; } }白名单的设计非常重要。登录接口本身不能要求带 Token否则死循环后续如果加了短信验证码、图片验证码之类的接口也都要在白名单里处理。我在实际操作中维护了一个白名单列表每加一个免鉴权接口就往里加路径简单明了。4.3 跨域问题在网关层统一解决单体应用时代跨域配置就够烦的了微服务化之后如果每个服务都配一遍跨域既重复又容易漏。更合理的方式是在网关层统一配置跨域规则下游服务完全不管跨域问题。Gateway 的跨域配置和 Spring MVC 有点不同它用的是 WebFlux 的配置方式spring: cloud: gateway: globalcors: cors-configurations: [/**]: allowedOriginPatterns: * allowedMethods: - GET - POST - PUT - DELETE - OPTIONS allowedHeaders: * allowCredentials: true注意属性名是globalcors而不是其他拼法。还有一件事如果你在前面定义了全局过滤器要确保对 OPTIONS 预检请求直接放行否则前端复杂请求会因为预检被拦截而报跨域错误。我就是在过滤器里加了一个判断如果是 OPTIONS 请求就直接进入下一个过滤器链。5. 服务间调用与缓存整合OpenFeign 和 Redis5.1 OpenFeign 实现服务间调用接口契约单独管理现在到了最能体现微服务特色的环节文章服务需要根据 authorId 去用户服务查询作者名称。在单体应用里这是最普通不过的一次联表查询但在微服务架构下数据分散在各自服务的数据库里必须通过网络调用才能拿到。我使用 OpenFeign 来完成这个远程调用。由于接口定义会被多个服务引用我把 Feign 接口放在独立的 micro-api 模块中这样调用方和实现方引用的是同一份代码不会出现两边参数不一致的问题。在 micro-api 模块定义一个远程调用接口FeignClient(name micro-system, path /system) public interface RemoteUserService { GetMapping(/user/{id}) ResultUser getUserById(PathVariable(id) Long id); }调用方在使用时先引入 micro-api 依赖然后在启动类上加EnableFeignClients(basePackages com.example.api)最后在业务代码里注入RemoteUserService就能像调本地方法一样发远程请求。但这里有个大坑必须强调Feign 接口的参数不能随便用一个复杂对象特别是 GET 请求。如果你传一个对象Feign 默认会把它序列化成 JSON 放进请求体里而 GET 请求通常是没有请求体概念的最终可能 400 或者 405。所以 GET 请求尽量用PathVariable和RequestParam这种基础类型参数组合。5.2 配置 Feign 超时和日志远程调用更可控服务间调用最容易出现的问题是超时。默认的 Feign 超时时间是 1 秒在本地环境可能感受不到但一旦部署到服务器上网络波动、GC 停顿、数据库慢查询任何一个原因都能让远程调用超过 1 秒。实际生产环境里 1 秒超时太苛刻了我配置了全局的 Feign 超时feign: client: config: default: connectTimeout: 5000 readTimeout: 5000 compression: request: enabled: true mime-types: text/xml,application/json,application/xml min-request-size: 2048 response: enabled: true超时设置的原则是连接超时可以短一些连接不上快速失败读取超时要给得足够宽裕因为下游可能在做真实业务处理。我还额外开启了请求和响应的 GZIP 压缩这对大数据量的接口效果很明显响应体积能缩小 70% 以上。5.3 Redis 缓存热点数据注意序列化配置微服务化之后每次查询作者信息都通过 Feign 去查一次用户服务在高频场景下对用户服务的压力会非常大。这里用到 Redis 做缓存简单直接的方案就是 Spring Cache 注解。我的做法是在 Feign 实现类所在的服务里对查询用户信息的方法加缓存注解Cacheable(cacheNames user, key #id) public User getUserById(Long id) { return userMapper.selectById(id); }同时在业务服务里配置 Redis 作为缓存存储。需要注意序列化器的设置Spring Boot 默认的 RedisTemplate 使用 JDK 序列化存进 Redis 里的数据全是二进制乱码排查问题极其痛苦。我在配置类里统一改成了 JSON 序列化Bean public RedisTemplateString, Object redisTemplate(RedisConnectionFactory factory) { RedisTemplateString, Object template new RedisTemplate(); template.setConnectionFactory(factory); GenericJackson2JsonRedisSerializer serializer new GenericJackson2JsonRedisSerializer(); template.setDefaultSerializer(serializer); template.setKeySerializer(RedisSerializer.string()); template.setHashKeySerializer(RedisSerializer.string()); template.setValueSerializer(serializer); template.setHashValueSerializer(serializer); return template; }然后改造分页查询先从 Redis 缓存读取命中则直接返回不命中则查询数据库并回填缓存。这里要注意设置合理的过期时间比如 5 分钟并且更新、删除文章时要把对应缓存清掉否则用户会看到旧数据。我在更新接口里加了一行手动删除缓存的操作redisTemplate.delete(article:: article.getId());这是最简单的缓存一致性处理方式足够应付大多数 CRUD 场景。如果你要追求强一致那就得引入更复杂的 Caffeine Redis 多级缓存方案了属于后面系列的扩展内容。6. 常见问题与排查技巧实录6.1 典型问题一网关启动报错 “Spring MVC found on classpath”这个错误是整合 Gateway 时最容易踩的坑。Gateway 基于 WebFlux它本质上和 Spring MVC 是两套 Web 框架不能共存。错误的起因往往是你在网关模块里直接复制了其他业务模块的 pom.xml把spring-boot-starter-web也带了进来。解决办法很简单确认网关模块的依赖列表里没有spring-boot-starter-web。如果你需要发送 HTTP 请求不要用 RestTemplate 或者 WebClient 之外的方案用 webflux 的 WebClient 或者把 HTTP 客户端相关的逻辑放在下游服务。我自己的排查经验是不要只检查当前 pom.xml还要看是否通过其他依赖间接引入了 web 模块。最直接的方式是执行mvn dependency:tree查看依赖树看到spring-boot-starter-web就能定位到来源。6.2 典型问题二Nacos 注册成功但服务间调用失败明明服务都启动成功了在 Nacos 控制台也能看到服务列表但通过网关访问时却报 503 或者连接拒绝。这类问题定位下来大多数原因是路由 ID 与注册的服务名不一致。我踩过一次的坑网关路由配置里uri: lb://micro-system但 Nacos 注册的服务名是micro_system下划线和中横线不匹配导致 LoadBalancer 找不到可用实例。这种问题最隐蔽因为它只在运行时报错日志里通常是一大串No instances available for。建议在项目立项第一天就统一服务命名规范全部用中横线风格。另外在启动服务时观察控制台的注册日志确认服务名、IP、端口都正确后再往下走。6.3 典型问题三MyBatis-Plus 分页查询返回总数为 0分页查询能查出数据但 total 字段是 0这个问题几乎每个用 MyBatis-Plus 的人都会遇到。原因就是前面说过的没有配置分页插件拦截器。没有拦截器时Page 对象只是被当成一个普通查询参数MyBatis-Plus 不会生成 count 语句自然拿不到总数。当你确认配置类已经加了PaginationInnerInterceptor还是有问题就检查一下数据库类型是否写对了。我项目用的是 PostgreSQL所以DbType.POSTGRE_SQL如果你用的是 MySQL就要改成DbType.MYSQL。分页插件会针对不同数据库生成不同的分页 SQL写错类型会导致语法错误或者分页结果不对。6.4 典型问题四Feign 调用传入复杂对象报 405/400GET 请求想传对象这类写法在单体应用里很常见但 Feign 会直接教你做人。因为 Feign 的 GET 请求没有请求体概念复杂对象会被序列化放 URL 里URL 太长直接就 400 了或者对方接口压根没设计接收方式405 也是家常便饭。符合规范的写法是 GET 请求把参数拆成基础类型放在RequestParam或者PathVariable里或者改造成 POST 请求传 JSON 对象。我在项目里遵循一个原则查询用 GET 基础参数复杂条件查询用 POST Query 对象这样既避免 URL 长度问题也让 Feign 传参更稳定。6.5 几个提高开发效率的小技巧最后分享几个我这次整合过程中实际用到的小技巧。日志配置要早点做。微服务排障最大的困难就是日志分散我给每个服务都配置了 Logback统一日志格式控制台和文件都保留。后续如果要接 ELK 或者 SkyWalking第一位的就是日志规范。本地联调的时候可以用spring-cloud-starter-bootstrap配合 Nacos 多环境配置。我建了application-dev.yml和application-prod.yml放在 Nacos 配置中心本地启动时通过-Dspring.profiles.activedev指定环境前端团队和后端团队用同一套配置联调问题变少了很多。如果服务多本地启动很不方便我推荐用 Docker Compose 把 Nacos、Redis、PostgreSQL 全部容器化。一个docker-compose up -d全部起来比手动一个个装高效太多。这些基础设施经过容器化后团队新成员加入时只需要一条命令就能把依赖环境跑起来省下来的时间相当可观。6.6 关于过度设计的提醒学到这一步你已经拥有了一个能跑通完整 CRUD 链路的微服务骨架这时候最需要警惕的就是过度设计。我见过不少团队刚搭好骨架就急着上 Sentinel 限流、Seata 分布式事务、Kafka 消息队列结果业务还没跑起来基础设施先崩了。微服务化的核心目标是“业务可扩展、团队可协作”不是“技术栈越全越牛”。就拿分布式事务来说如果你的业务确实有跨服务写操作优先考虑是否能通过业务设计规避比如把相关操作放在同一服务里或者能接受最终一致性再考虑引入 MQ。Seata 这类框架引入之后复杂度是呈指数级上升的没有实际业务需求千万别提前上。从个人体验来说先把基础的 CRUD 链路吃透让团队能围绕这套骨架顺畅地写业务比什么都重要。等到业务量确实增长到需要限流降级、链路追踪、分布式事务的规模时再按需引入每个组件都能落到位。这套 Spring Cloud 整合系列还有很长的路可以走下一步我准备把 Sentinel 限流、分布式事务、以及基于 Docker 的整套环境编排陆续加进来让骨架从“能跑”真正进化到“能扛”。
返回列表