SpringBoot接口分层规范外网网关、内部服务与参数边界Spring Boot 接口“能接收参数并返回 JSON”并不等于契约设计完整。前后端流量与服务间调用面对的信任来源、协议包装、校验和错误语义并不相同。如果外网 Controller 直接承担业务内部服务无法复用如果内外接口共用同一请求对象签名、用户上下文和业务字段会互相污染。标准写法的核心是流量边界而不是注解数量。MetaLite 将外网 Gateway 与内部 Controller 分开声明共用业务参数但不共用请求协议。本文先建立南北流量和东西流量模型再结合 backend-gateway 与 backend-admin 的接口代码给出完整检查清单。一、先看完整链路一个接口声明两次并不是重复以“创建用户”为例请求链路如下浏览器或外部调用方 ↓ backend-gateway外部接口 校验外部系统参数与登录凭证 解密、解析业务参数 External...Req → Internal...Req ↓ InternalServiceClient 根据 provider 和 endpoint 调用内部服务 ↓ backend-admin内部接口 校验 InternalReq 和嵌套业务参数 调用 SysUserService ↓ 统一 RespT 沿原链路返回Gateway 和 Admin 中都有/api/admin/sys/user/create看起来像重复声明实际职责不同层次面向对象请求模型核心职责backend-gateway浏览器、App、第三方调用方External*Req公网协议、安全处理、请求转换、RPC 转发backend-admin可信的内部服务调用Internal*Req内网协议、业务参数校验、Service 编排同一个 endpoint 在两个边界上保持一致使网关不必维护额外的“外部路径到内部路径”映射表。但相同 URI 不等于相同信任级别真正的边界由请求类型、处理器链和部署网络共同建立。二、对外接口的类应该怎样声明backend-gateway中的SysUserApi使用了统一的类级声明Tag(name系统管理模块 - 用户相关接口)RequestMapping(path/api/admin/sys/user,consumesMediaType.APPLICATION_JSON_VALUE,producesMediaType.APPLICATION_JSON_VALUE)RestControllerpublicclassSysUserApi{}这里一次明确四件事RestController表示返回值直接序列化为响应体RequestMapping统一模块路径方法只维护动作路径consumes明确只接收 JSON避免接口对输入媒体类型含糊produces明确输出 JSON文档、调用方和测试工具得到一致预期。Tag与方法上的Operation则负责接口文档分组与业务说明Operation(summary创建用户,description)PostMapping(path/create)MetaLite 的后台接口采用“资源路径动作”的 POST JSON 风格。这是一种适合内部 RPC 和复杂业务命令的工程选择并不是 REST 的唯一标准。如果团队公开的是资源型开放 API也可以采用 GET、POST、PUT、DELETE重要的是风格与错误语义保持一致而不是混用两套规则。三、对外请求为什么不能只写一个业务 DTO公网请求除了业务字段还需要携带协议和安全上下文。MetaLite 把外部请求拆成四种类型请求类型是否登录是否带业务参数ExternalReq否否ExternalBizParamReqT否是ExternalLoginReq是否ExternalLoginBizParamReqT是是最基础的ExternalReq包含publicclassExternalReqimplementsPojo{NotBlankprivateStringapiVersion;NotBlankprivateStringappId;privateStringencryptData;privateStringplaintext;Min(1677654864000L)privatelongtimestamp;privateStringsign;}需要登录的请求增加userTokenpublicclassExternalLoginReqextendsExternalReq{NotBlankprivateStringuserToken;}带业务参数的登录请求则通过泛型声明业务类型publicclassExternalLoginBizParamReqTextendsParamextendsExternalLoginReq{privateTbizParam;}这里有一个容易误读的细节对外调用方真正提交的是plaintext或encryptDatabizParam主要用于接口文档展示源码注释也明确要求调用方不要直接上送该字段。因此Gateway 的第一阶段校验重点是公网协议字段加密业务数据经过安全处理器解密为plaintext后再由WebUtil.genInternalReq解析成具体参数类型并构造内部请求。四、一个标准的 Gateway 方法长什么样创建用户的外部入口如下Operation(summary创建用户,description)PostMapping(path/create)publicRespVoidcreateUser(RequestBodyValidExternalLoginBizParamReqSaveSysUserParamreq,Parameter(hiddentrue)BindingResultbindingResult){returninternalServiceClient.callOneInstanceRtnData(RpcRequest.builder().provider(InternalServiceEnum.ADMIN.getCode()).endpoint(WebUtil.getRequestUri()).rpcMode(RpcModeEnum.HTTP_POST_JSON).param(WebUtil.genInternalReq(req,SaveSysUserParam.class)).build(),Void.class);}它只做协议层工作用请求泛型声明业务参数类型让 Bean Validation 检查外部请求指定内部服务提供者沿用当前请求 URI 作为内部 endpoint将外部请求转换为内部请求告诉 RPC 客户端响应数据的目标类型。这里没有创建用户的业务规则。Gateway 不应该知道用户名怎样判重、默认密码怎样生成、操作日志怎样记录。这些逻辑属于 Admin 服务。五、BindingResult 为什么必须跟在 Valid 参数后面MetaLite 的标准方法签名同时保留Valid和BindingResultpublicRespVoidcreateUser(RequestBodyValidExternalLoginBizParamReqSaveSysUserParamreq,Parameter(hiddentrue)BindingResultbindingResult){// ...}BindingResult应紧跟在被校验的参数后面Spring 才能把该对象的校验结果与它正确关联。Parameter(hidden true)则避免把框架内部参数展示到 OpenAPI 文档中。业务方法里为什么没有手工写if(bindingResult.hasErrors()){// 拼装错误返回}因为backend-application的ApiReceiveParamHandler已经统一处理BindingResultbindingResultaspectInfo.findParam(BindingResult.class);if(bindingResultnull||!bindingResult.hasErrors()){returnResp.ok();}for(FieldErrorfieldError:bindingResult.getFieldErrors()){if(Strings.CS.equalsAny(fieldError.getCode(),PARAM_REQUIRED_CODES)){returnResp.error(ErrorCode.PARAM_REQUIRED,fieldError.getField());}returnResp.error(ErrorCode.PARAM_INVALID,fieldError.getField());}它把NotBlank、NotEmpty、NotNull归为“参数必填”其他约束归为“参数不合法”还支持以开头的自定义提示。这样所有接口使用同一种错误协议Controller 不再复制校验分支。六、为什么业务参数要到内部接口再完整校验Gateway 收到的业务内容可能是密文也可能是 JSON 字符串。WebUtil.genInternalReq负责验证 JSON 形态、反序列化并转换请求publicstaticInternalBizParamReqgenInternalReq(ExternalReqexternalReq,Class?extendsParambizParamClass){InternalBizParamReqinternalReqnewInternalBizParamReq();StringplaintextexternalReq.getPlaintext();if(StringUtils.isBlank(plaintext)){returninternalReq;}ParamparamFastJson.json2Obj(plaintext,bizParamClass);internalReq.setBizParam(param);returninternalReq;}内部请求明确要求业务参数存在并递归校验publicclassInternalBizParamReqTextendsParamextendsInternalReq{NotNullValidprivateTbizParam;}NotNull保证业务对象存在Valid继续检查SaveSysUserParam内部的字段约束。于是校验形成两个层次Gateway 校验调用方可以控制的外部协议参数Admin 校验解密并反序列化后的真实业务参数。这比在 Gateway 中同时处理签名、解密和所有业务字段更清晰也避免内部服务重复接受公网协议字段。七、业务参数类应该怎样写业务参数应该使用明确类型实现Param而不是MapString, ObjectDataSchemapublicclassSaveSysUserParamimplementsParam{privateStringuserId;NotBlankSchema(title登录用户名,requiredModeREQUIRED)privateStringuserName;NotBlankSchema(title手机号,requiredModeREQUIRED)privateStringphone;privateStringnickName;privateStringemail;privateintstatus;}建议遵守几条规则1. 一个参数类表达一个稳定用例查询、保存、ID 定位不要混成一个万能对象。MetaLite 分别使用SaveSysUserParam、QueryUserParam、UserIdParam。2. 必填约束写在最接近数据的位置字段用NotBlank、NotNull、Min等约束嵌套对象用Valid触发递归校验。3. 查询条件优先使用包装类型Integer status能区分“不传”和“查询状态 0”int status不能。如果字段不传时就应自然等于 0基本类型才是合适选择。4. 创建和更新约束不同就不要勉强共用源码中的保存参数复用了创建和更新userId只在更新场景需要。如果两种场景的必填字段差异继续扩大应拆成两个参数类或明确采用校验分组不能把所有规则都推迟到 Service 中。5. Schema 是文档不是校验requiredMode REQUIRED让接口文档更清楚但真正阻止空值的是 Bean Validation 注解。两者应该保持一致不能只写其中一个。八、内部接口应该怎样声明backend-admin保持与 Gateway 相同的资源路径和动作路径但参数换成内部协议Tag(name系统管理模块 - 用户相关接口)RequestMapping(path/api/admin/sys/user,consumesMediaType.APPLICATION_JSON_VALUE,producesMediaType.APPLICATION_JSON_VALUE)RestControllerpublicclassSysUserApi{ResourceprivateSysUserServicesysUserService;Operation(summary创建用户,description)PostMapping(path/create)publicRespVoidcreateUser(RequestBodyValidInternalBizParamReqSaveSysUserParamreq,Parameter(hiddentrue)BindingResultbindingResult){returnsysUserService.createUser(req.getBizParam());}}内部接口不再验证appId、签名和公网时间戳而是接收由框架构造的InternalReqpublicclassInternalReqimplementsPojo{NotBlankprivateStringprovider;NotBlankprivateStringconsumer;}这两个字段描述服务调用关系并由框架自动填充。它们不是让外部调用方伪造的“内部凭证”。生产环境仍需配合网络隔离、服务身份校验和入口限制不能因为请求类名叫InternalReq就默认调用可信。九、没有业务参数时不要制造空 DTO如果接口只依赖登录上下文不需要业务参数对外使用ExternalLoginReqpublicRespSysUserEntitygetMyself(RequestBodyValidExternalLoginReqreq,BindingResultbindingResult){returninternalServiceClient.callOneInstanceRtnData(RpcRequest.builder().provider(InternalServiceEnum.ADMIN.getCode()).endpoint(WebUtil.getRequestUri()).rpcMode(RpcModeEnum.HTTP_POST_JSON).param(WebUtil.genInternalReq(req)).build(),SysUserEntity.class);}内部对应使用InternalReq业务身份从统一线程上下文读取publicRespSysUserEntitygetMyself(RequestBodyValidInternalReqreq,BindingResultbindingResult){StringuserIdThreadContext.getLoginUserId();returnResp.ok(sysUserService.getUserDtoByUserId(userId));}不要为了形式统一创建EmptyParam也不要让客户端上传一个本应由认证链确定的用户 ID。系统上下文与业务参数属于两种不同来源。十、返回值怎样写才稳定MetaLite 所有接口统一返回RespT数据形态通过泛型表达无业务数据RespVoid用于创建、更新、删除等只需表达成功或失败的操作。单个对象RespSysUserEntity内部接口可以使用returnResp.ok(sysUserService.getUserDtoByUserId(userId));如果对外契约对字段稳定性或安全性要求较高应返回专用 DTO而不是直接暴露持久化实体。源码中的实体返回适合当前工程边界不应被理解为所有开放 API 的通用结论。列表RespListUserOrgInfoDto分页结果RespPageResultDtoSysUserEntityGateway 对普通数据调用internalServiceClient.callOneInstanceRtnData(rpcRequest,SysUserEntity.class);分页数据则调用internalServiceClient.callOneInstanceRtnPageData(rpcRequest,SysUserEntity.class);RPC 客户端拿到了元素类型才能把内部响应稳定地反序列化为目标泛型结构。不要统一返回裸Object更不要让每个 Controller 自己发明code/message/data。十一、Gateway 与 Admin 的职责红线一个对外接口可以做外部请求协议声明调用方、用户和安全上下文接入请求解密与外转内路由到目标内部服务对响应做网关级加密和状态处理。它不应该做数据库查询业务唯一性判断业务事务领域状态流转为了少一次 RPC 复制 Service 逻辑。内部接口负责校验已经解析的业务参数从可信上下文读取内部身份调用 Service 完成业务编排将结果包装为统一响应。内部 Controller 同样不应塞入大段业务代码。它是传输协议到应用服务之间的适配层不是另一个 Service。十二、一份可复用的接口检查清单新增 MetaLite 风格接口时可以逐项检查接口声明是否使用RestController类级路径是否稳定且模块化consumes/produces是否明确Tag、Operation是否描述真实用途对外和对内 endpoint 是否保持可追踪的一致关系。参数写法是否根据登录与业务参数选择正确的External*Req内部是否使用InternalReq或InternalBizParamReqT业务参数是否使用明确的Param类型可选数值是否使用包装类型系统上下文是否避免由客户端重复提交。参数校验RequestBody Valid是否完整BindingResult是否紧跟被校验参数嵌套业务对象是否有ValidBean Validation 与 Schema 必填说明是否一致创建和更新约束差异是否已经显式处理。返回值是否统一返回RespT无数据是否使用Void分页是否使用PageResultDtoTRPC 是否提供正确的反序列化类型对外契约是否需要专用 DTO 隔离实体变化。十三、什么才叫“标准写法”Spring Boot 并不存在一份适合所有项目的官方 Controller 模板。所谓标准不是注解排列得整齐而是团队对每一个边界都有一致答案什么请求来自公网什么请求只在服务内部传播系统参数与业务参数怎样分开校验在哪一层发生身份上下文由谁建立业务逻辑放在哪里返回值怎样稳定传递。MetaLite 用外部四类请求、内部两类请求、统一参数校验处理器、WebUtil请求转换、InternalServiceClient和RespT把这些答案固化成可以复制的工程结构。这才是“标准接口”真正带来的价值不是让某一个 Controller 少写几行而是让下一百个接口仍然能被快速理解、统一治理和稳定演进。框架简介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 系列内容将持续更新围绕核心设计、源码链路、技术取舍与生产实践展开。欢迎关注作者及时获取后续内容。在线演示演示地址: https://admin.metalite.top/演示账号: guess演示密码: admin2026
