Spring-MVC微服务接口怎么分层-网关Controller与Service边界

一次绕过业务网关的排障:Spring MVC 微服务接口为何必须分层

Spring MVC 项目最容易出现的结构退化,是 Controller 从协议入口逐渐变成业务编排中心:校验、转换、权限、事务和数据库调用全部堆在一个方法里。代码仍然能运行,但任何复用都会绕回 HTTP 层。

简单的"三层架构"名称并不能解决问题,关键是外部协议何时结束、内部业务契约从哪里开始,以及横切规则由谁执行。边界不清时,网关与服务会同时校验又同时遗漏。

MetaLite 把外网 Gateway、内部 Controller 与 Service 分别用于协议治理、内部契约和业务实现,并用处理器链承接通用安全逻辑。本文从一次绕过网关的排障路径进入,再以实际调用链说明三层各自不能承担什么。

一、网关 Controller 只负责外部协议

backend-gatewaySysUserApi.createUser 为例,方法接收:

java 复制代码
ExternalLoginBizParamReq<SaveSysUserParam>

它不直接写用户表,而是构造 RpcRequest

java 复制代码
return internalServiceClient.callOneInstanceRtnData(
    RpcRequest.builder()
        .provider("admin")
        .endpoint(WebUtil.getRequestUri())
        .param(WebUtil.genInternalReq(req, SaveSysUserParam.class))
        .build(),
    Void.class);

网关层的职责是维护公网契约、完成认证与协议转换,然后选择内部服务实例。业务规则不应在这里复制一份,否则网关和业务服务会逐渐出现两套判断。

二、内部 Controller 为什么仍然有价值

backend-admin 中存在相同路径的 SysUserApi.createUser,但参数换成:

java 复制代码
InternalBizParamReq<SaveSysUserParam>

它只提取业务参数并调用:

java 复制代码
return sysUserService.createUser(req.getBizParam());

内部 Controller 看起来很薄,却建立了清晰边界:HTTP 反序列化、Bean Validation、内部调用者信息属于接口层;创建用户、唯一性校验、事务和缓存刷新属于 Service。

薄 Controller 不是没有设计,而是把变化频率不同的职责隔离开。

三、为什么外部和内部可以使用相同 URI

MetaLite 的网关和业务服务都声明 /api/admin/sys/user/create。网关通过 WebUtil.getRequestUri() 把当前路径作为 RPC endpoint 转发。

这样做的优点是:

  • 外部与内部接口不用维护路径映射表;
  • 日志按同一 URI 检索;
  • 新增接口时转发代码更直观。

代价是部署边界必须严格:业务服务端口不能直接暴露给不可信网络。否则调用方可能绕过网关的认证、限流、解密和权限处理器,直接访问内部 Controller。

相同 URI 是降低映射成本的约定,不是安全隔离措施。

四、Service 为什么不能返回任意异常

Service 可以返回 Resp 表达可预期业务失败,也可以抛 ServiceException 交给统一错误体系转换。入口层负责把异常变成协议响应,RPC 调用层则要保留失败语义,不能把所有异常都吞成"调用失败"。

这种边界让 Service 不需要知道当前请求来自浏览器、开放 API 还是其他微服务。

但团队必须统一一种主要风格。若同类校验有时返回 Resp.error、有时抛异常,事务回滚和调用方处理会变得难以预测。

五、BindingResult 为什么可以出现在签名里却不手工判断

MetaLite Controller 保留隐藏的 BindingResult,参数校验由 API 切面中的 ApiReceiveParamHandler 统一读取和转换。

这样每个方法不再重复:

java 复制代码
if (bindingResult.hasErrors()) { ... }

前提是接口必须经过对应切面。脱离该入口链单独调用 Controller,不能假设校验错误仍会以相同方式处理。

六、网关编排业务副作用要克制

登录接口在 RPC 成功后调用 userAuthService.afterLogin(loginResp),由网关写入登录 Token。这属于协议与会话边界,放在网关有合理性。

但订单创建、库存扣减等领域副作用不应采用同样方式堆在网关。网关一旦承担跨服务业务事务,就会变成新的大单体。

判断标准是:这项逻辑是在维护外部协议与安全会话,还是在实现领域业务规则。

七、这套分层不等于 Spring Cloud Gateway

MetaLite 使用的是显式 Spring MVC 业务网关,不是根据路由表动态转发所有流量的 Spring Cloud Gateway。每个外部接口都有明确 Controller,并显式构造内部请求。

它适合需要强协议治理、请求模型转换和业务化安全处理的管理系统;如果目标是高吞吐通用反向代理、WebSocket 或动态路由,仍应评估专业网关。

八、一套可执行的接口分层规则

可以把职责约束为:

应负责 不应负责
网关入口 公网协议、认证、签名、加解密、限流、转发 数据库事务、领域规则
内部 Controller 内网协议、参数校验、Service 调用 重复网关安全逻辑、复杂业务
Service 业务规则、事务、缓存与跨 DAO 协调 HTTP 请求对象、网关密钥
DAO 查询、更新与数据路由 登录状态、接口响应拼装

MetaLite 的核心不是多写一层 Controller,而是让每一层的失败都能定位到协议、安全、业务或数据访问中的一个明确位置。

九、用一次"绕过网关"实验验证分层边界

这篇文章与接口规范总纲的区别,不在于再画一遍分层图,而在于验证:一旦绕过业务网关,哪些安全前提会立即失效。

可以准备同一个创建用户请求,分别走两条路径:

请求路径 请求内容 预期结果
正常公网入口 调用 backend-gateway,携带外部请求模型 网关完成调用方认证、用户认证和协议转换,再调用内部服务
直接访问 Admin 直接请求内部 URI,不带可信内部上下文 生产网络应拒绝访问,而不是依赖 Controller 自己猜测来源
伪造内部 Header 公网请求自行添加用户 Header 入口代理必须删除或覆盖,后端端口不得直接暴露
网关与内部 URI 不一致 修改任意一侧映射 WebUtil.getRequestUri() 转发后无法命中,集成测试应立即失败

这组实验揭示了三个必须同时成立的条件:公网只能到达网关;内部 Header 只能由可信调用链写入;网关和内部 Controller 的契约必须由测试锁定。只讨论 Controller 是否"薄",无法证明这套分层真的安全。

建议把上述四种请求加入部署验收,而不只运行单元测试。这样 045 关注的是边界被绕过时怎样失败,095 则继续承担完整接口规范总纲,两篇文章不再重复。


框架简介 MetaLite 是面向企业生产环境的新一代 Java 微服务技术底座。系列文章重点分享代码背后的设计思路、技术取舍与工程实践。

源码基线 JDK 21、Spring Boot 3.2.9、Spring Cloud 2023.0.1、Spring Cloud Alibaba 2023.0.1.3,具体组件版本以项目 backend-bom 为准。

作者简介 15 年 Spring 体系企业级开发经验,专注于 Java 微服务架构、工程治理与生产实践。

持续更新 MetaLite 系列内容将持续更新,围绕核心设计、源码链路、技术取舍与生产实践展开。欢迎关注作者,及时获取后续内容。

在线演示 演示地址: admin.metalite.top/ 演示账号: guess 演示密码: admin@2026

相关推荐
明月_清风1 小时前
看完 DSH 文档后,我总结了这 7 个关键点
前端·后端·deepseek
省长1 小时前
不想写代码,但想要集成一个登录页面?Sa-Token-Quick-Login 帮你实现!
java·后端·开源
铁皮饭盒1 小时前
浏览器AI肯定离不开onnxruntime, 微软开源
前端·javascript·后端
得物技术1 小时前
企业级 MultiAgent 的记忆系统:短期上下文与四层记忆架构实现|得物技术
java·人工智能·后端
MacroZheng2 小时前
又一个神级画图Skill开源,再见draw.io!
java·人工智能·后端
唐青枫2 小时前
把代码搬到编译期:Zig comptime、泛型与类型反射实战
后端
风曳丷2 小时前
06|Direct 与 Indirect Prompt Injection
后端
风曳丷2 小时前
05|Prompt Injection 不是一句神奇咒语
后端
喜欢睡觉2 小时前
单词管理系统
前端·后端