Day 04 / 共 20 天 · 第 1 周 后端架构

REST API 层:后端对外的门面

今天看后端"对外暴露了哪些接口":一张控制器总表,再精读最典型的路由 CRUD,最后看统一响应和状态码策略——这套模式所有控制器都一样,学一个就会全部。

📍 你在整门课的位置(第 1 周 后端架构)
D01 全景 D02 启动 D03 分层 D04 REST D05 横切 D06 SDK D07 存储 D08 客户端 D09 模型 D10 流转
💡 一句话兜住今天(REST 就像寄快递) 昨天说 Controller 是"服务员",今天看它菜单长什么样。REST 风格就像寄快递单:URL = 包裹地址/v1/routes/demo 指"路由 demo"这件东西),HTTP 动词 = 你要干嘛(GET 查、POST 寄新的、PUT 改、DELETE 撤单)。看一眼"动词 + 地址"就知道这次请求要对哪件东西做什么——这就是 REST 好用的地方。所有控制器都用同一张单据模板,学会一个就会全部 16 个
L01

统一 REST 风格

所有控制器长得几乎一样:@RestController + @RequestMapping("/v1/...") + @Validated + @Tag(Swagger 分组),返回 ResponseEntity<Response<T>>ResponseEntity<PaginatedResponse<T>>,通过 ControllerUtil.buildResponseEntity(...) 包装。

什么是 RESTful? 简单说就是"用 HTTP 动词表达操作,用 URL 表达资源":GET /v1/routes 列表、GET /v1/routes/{name} 查一个、POST /v1/routes 新建、PUT /v1/routes/{name} 改、DELETE /v1/routes/{name} 删。看到 URL 和方法就知道在干嘛,这就是 REST 的好处。
L02

控制器全表

控制器基路径主要端点
RoutesController/v1/routeslist/get/post/put/delete
ServicesController/v1/serviceslist(只读)
ServiceSourceController/v1/service-sourcesCRUD
DomainsController/v1/domainsCRUD
ConsumersController/v1/consumersCRUD
TlsCertificatesController/v1/tls-certificatesCRUD
WasmPluginsController/v1/wasm-pluginsCRUD + /config + /readme
WasmPluginInstancesController/v1(四作用域)global/domain/route/service
ai/AiRoutesController/v1/ai/routesCRUD
ai/LlmProvidersController/v1/ai/providersCRUD
mcp/McpServerController/v1/mcpServerCRUD + consumers
DashboardController/dashboardinit/info/configData
SystemController/systeminit/info/config
SessionController/sessionlogin/logout
UserController/userinfo/changePassword
HealthzController/healthzready(就绪探针)
读法:大部分是标准 CRUD;ServicesController 只读(服务从注册中心发现,不能增删);WasmPluginInstancesController 特殊——它按四个作用域(全局/域名/路由/服务)分别提供接口。
L03

精读 Routes CRUD

RoutesController.java 是最典型的 CRUD,逐段看 add:71-86):

@PostMapping
public ResponseEntity> add(@RequestBody Route route) {
    if (StringUtils.isEmpty(route.getName()))                       // :78 名字非空
        throw new ValidationException("name can't be empty.");
    if (route.getName().endsWith(HigressConstants.INTERNAL_RESOURCE_NAME_SUFFIX)) // :81 禁内部后缀
        throw new ValidationException(...);
    route.validate();                                                // :84 领域模型自校验
    return ControllerUtil.buildResponseEntity(routeService.add(route));
}
读法:三步校验(名字非空 → 禁止 .internal 后缀 → route.validate() 领域自校验)后交给 routeService.addupdate:88-107)还会校验"URL 里的名字"和"body 里的名字"一致。delete:109-120)成功返回 204。控制器只做"守门",不做业务。
📝 举个例子:四种输入,守门员分别怎么判 POST body {"name":""} → 名字空 → 抛 ValidationException400
POST body {"name":"demo.internal"} → 命中禁用后缀 → 400
PUT /v1/routes/demo body {"name":"other"} → URL 名与 body 名不一致 → 400
POST body {"name":"demo","services":[...]} 全部合法 → 交 routeService.add201
L04

最小示例:ServicesController

ServicesController.java:35-51 只有一个 list,是最干净的入门样本:

@GetMapping
public ResponseEntity> list(@ParameterObject ServicePageQuery query) {
    return ControllerUtil.buildResponseEntity(serviceService.list(query));
}
为什么服务只能看不能改? 服务(Service)不是你"创建"的,而是 Higress 从注册中心(K8s / Nacos / Consul…)发现的。控制台只是把发现到的服务列出来给你选,不提供增删改——这符合"服务发现"的语义。真正管理"从哪发现服务"的是服务来源(Service Source),Day 14 讲。
L05

统一响应 Response DTO

console/controller/dto/Response.java:32-57 是三段式包装:

class Response { boolean success; String message; T data; }
// success(data) / failure(msg|Throwable) 静态工厂
// getErrorMessage :63-75 会把 K8s ApiException 的 responseBody 也拼进错误信息
读法:前端拿到的永远是 {success, message, data} 这个统一形状,好处理。特别贴心的是 getErrorMessage 把 K8s 报错的原始 body 也带上——排查"为什么写 K8s 失败"时能直接看到根因。分页版是 PaginatedResponse
L06

ControllerUtil:按方法定状态码

console/controller/util/ControllerUtil.java:46-78 根据 HTTP 方法自动决定状态码:

  • GET 且结果为 null404
  • GET 且是分页 → PaginatedResponse.success
  • POST → 201 CREATED
  • DELETE → 有内容 200,否则 204
为什么要费劲区分状态码? HTTP 状态码是"机器可读的结果信号"。201 表示"创建成功"、204 表示"删了,没内容返回"、404 表示"没找到"。前端和监控系统靠这些码就能判断结果,不用去解析 body。把这套逻辑收进一个工具类,所有控制器统一遵守,省得每个接口自己写。
方法 + 返回值ControllerUtil 判定最终状态码
GET 查单个,结果 null没找到404
GET 分页列表PaginatedResponse.success200
POST 新建成功创建类201
DELETE 无返回内容删除且无 body204
同一个 buildResponseEntity,按"动词 + 结果"分岔出状态码 buildResponseEntity() GET null → 404 POST → 201 DELETE → 204 GET 列表 → 200
图注:状态码逻辑收进一个工具方法,所有控制器统一遵守,前端/监控靠码就能判结果。

👶 小白:直接全返回 200、把结果塞 body 里,前端自己看不就行了?

👨‍🏫 老师:能跑,但不专业。状态码是"机器可读的信号"——监控系统看到一片 4xx/5xx 就知道出问题了,浏览器缓存、重试策略也都认状态码。全返 200,等于把所有信息藏进 body,机器就瞎了。用对码,是让整个生态都能读懂你的结果。

L07

分页查询

列表接口普遍接收一个查询对象(如 RoutePageQuery),用 @ParameterObject(Springdoc)把 URL 上的 ?pageNum=&pageSize=&name= 自动绑定成对象字段,返回 PaginatedResult

读法:@ParameterObject 让 Swagger 把查询对象的每个字段都展开成独立的查询参数文档,前端调用方一目了然。分页在 SDK 层实现(第 2 周会看到 CommonPageQuery/PaginatedResult)。
L08

今日小结 + 动手

🧠 今天你应该能回答

  • 控制器的统一风格是什么?RESTful 的动词/URL 约定?
  • RoutesController.add 的三步校验?
  • 为什么服务(Services)只读?
  • Response 的三段式?ControllerUtil 怎么定状态码?

✋ 动手

cd /Users/bitmart/work/codes/github/higress-group/higress-console/backend/console/src/main/java/com/alibaba/higress/console
sed -n '71,120p' controller/RoutesController.java
sed -n '32,75p' controller/dto/Response.java
sed -n '46,78p' controller/util/ControllerUtil.java
ls controller/
明天预告 · Day 05横切与 Service 业务层——请求生命周期的枢纽 ApiStandardizationAspect(traceId/鉴权/统一异常),以及 console 自身业务 service(登录、系统配置存 ConfigMap)。
← Day 03 Day 05 · Service 层 →