183 lines
10 KiB
Markdown
183 lines
10 KiB
Markdown
# 已有 HTTP 项目逐步接入
|
||
|
||
从 `v0.9.0-preview` 开始,以下能力贯通运行时、`.api`、代码生成和 OpenAPI。CLI 和项目框架依赖一起升级;入口见 [README](README.md)。这些都是通用扩展点,框架不内置任何项目的 ID、支付回调、鉴权或数据库规则。
|
||
|
||
## 1. 保留标准 Handler 与中间件
|
||
|
||
```go
|
||
app := omni.New(omni.WithLogger(true))
|
||
app.Use(httpx.WrapMiddleware(existingMiddleware)) // func(http.Handler) http.Handler
|
||
// go-zero 风格的 func(http.HandlerFunc) http.HandlerFunc:
|
||
app.Use(httpx.WrapFuncMiddleware(existingFuncMiddleware))
|
||
app.POST("/legacy", httpx.WrapHandler(http.HandlerFunc(existingHandler)))
|
||
```
|
||
|
||
实际使用时先配置所有 `Use`,再注册路由。`WrapHandler` 也接受 `http.FileServer` 等标准 `http.Handler`。
|
||
|
||
- Handler 经过 App 的日志、requestId、恢复和应用中间件链,原来的 URL、状态码、Header、响应内容由它自己控制。
|
||
- `c.Raw()` 的写入也会更新 `c.Status()` / `c.Responded()`;404 会记为 404。提交响应后返回错误或 panic,不会再追加错误 JSON。103 等临时响应不算最终提交。
|
||
- 桥接器不缓存完整响应。下载继续流式复制;底层支持时保留 Flusher、Hijacker、Pusher、ReaderFrom、CloseNotifier。`http.ResponseController` 可沿 `Unwrap` 访问底层控制能力。能力仍取决于网络适配器和其他中间件。
|
||
- 请求 context、取消信号和已有值继续传递,Omni 的路径参数可用 Go 1.22 的 `r.PathValue("id")` 读取。其他框架私有的参数读取器仍需由项目适配。标准中间件应从原 `r.Context()` 派生上下文;不能换成无关的 Background,也不能在 ServeHTTP 返回后调用 next。异步 timeout middleware 不在此桥接器的契约内。
|
||
- 标准中间件在每条注册链构造一次,状态会跨请求保留。下游返回的错误在标准中间件退出前编码,因此压缩、审计等中间件能处理错误响应。它们自己的缓冲或改写行为仍由项目负责。
|
||
|
||
调用流使用 `App.Handle/GET/POST` 等入口即可,不要绕到原始 Router 注册。暂不使用 `.api` 的旧接口也能这样接入。
|
||
|
||
## 2. 动态 JSON:允许渐进补全 DTO
|
||
|
||
```go
|
||
type LegacyReply {
|
||
Data any `json:"data"`
|
||
}
|
||
```
|
||
|
||
`interface{}` 是 `any` 的等价写法,支持 `[]any`、`map[string]any` 和对应的 `interface{}` 写法。带方法的 interface 不支持,会提示明确的类型错误。
|
||
|
||
动态字段允许 object、array、string、number、bool、null;OpenAPI 字段 schema 为 `{}`,不假装知道内部结构。它放开的是**该字段的内容**,外围 DTO 未声明的字段仍被拒绝,JSON 重复键、深度、总大小限制仍生效。动态数字解析为 `json.Number`,避免大整数先转 float64 丢精度。原始 JSON 的空白、转义和字节布局不会保留;验签请走原始 body 入口。
|
||
|
||
这不会自动补齐缺少的业务路由,也不代表兼容全部 goctl 语法。能确定结构的新接口优先使用命名 DTO。
|
||
|
||
## 3. 按路由限制 body 与保留手写入口
|
||
|
||
普通 JSON 路由只声明需要的大小,不改变其他路由默认的 1 MiB:
|
||
|
||
```text
|
||
@body (mode: json
|
||
maxBytes: 2097152
|
||
)
|
||
@handler createItem
|
||
post /items (CreateItemReq) returns (ItemResp)
|
||
```
|
||
|
||
`maxBytes` 单位为字节,允许 1 至 1 TiB;不写时为 1 MiB。已知超长或读取未知长度/chunked body 超限都按 413 处理。内置 App/RouterGroup 在全局中间件读取前安装 reader 限额;同时保持接口响应策略。网络层若有更小限额,以更小值为准。手写 Handler 吞掉读取错误或已经提交响应后,框架不能重新改写 HTTP 状态,务必检查读取错误。
|
||
|
||
### 只替换绑定函数
|
||
|
||
```text
|
||
@binding custom
|
||
@body (mode: json
|
||
maxBytes: 65536
|
||
)
|
||
@handler createItem
|
||
post /items (CreateItemReq) returns (ItemResp)
|
||
```
|
||
|
||
首次生成 `internal/handler/<group>/createItemBind.go`:
|
||
|
||
```go
|
||
func (h *Handler) bindCreateItem(c *httpx.Context, req *types.CreateItemReq) error {
|
||
// 调用项目自己的解码器,填充 req;失败返回 error。
|
||
}
|
||
```
|
||
|
||
Handler 继续调用 Service 并使用统一响应;只把绑定过程交给这个函数。初始 hook 返回 501,不能忽略实现。自定义绑定需自己实现 Content-Type、未知字段、尾部数据等校验,并遵守 DTO 文档;框架负责路由总大小限制,不隐式套第二次 JSON 绑定。
|
||
|
||
### 上传、原文验签、下载
|
||
|
||
```text
|
||
@handlerMode manual
|
||
@body (mode: multipart
|
||
maxBytes: 10485760
|
||
)
|
||
@handler upload
|
||
post /files
|
||
|
||
@handlerMode manual
|
||
@body (mode: raw
|
||
maxBytes: 65536
|
||
)
|
||
@produces "text/plain"
|
||
@handler callback
|
||
post /callback
|
||
|
||
@handlerMode manual
|
||
@produces "application/octet-stream"
|
||
@handler download
|
||
get /download
|
||
```
|
||
|
||
首次生成 `<operation>Manual.go`,由项目实现整个 `func(h *Handler) Callback(c *httpx.Context) error`;依赖通过 `h.deps` 访问。生成器保留路由、中间件与文档管理,不会预读、JSON 解码或重新包装手写输出,也不生成这个路由的 Service 方法。需要调用 Service 时自行组合。
|
||
|
||
- manual 无返回类型默认成功状态 200;也可 `returns (UploadResp)` 描述 JSON DTO,手写实现按声明调用 `c.OK` 等。`@produces` 用于无 DTO 的手写输出,描述成功响应媒体类型,不会自动设置运行时 Header。
|
||
- raw 的文档为二进制请求体;multipart 为开放对象,具体文件字段与逐部分校验由项目实现。raw/multipart 请求 DTO 仅可含 path/form/header 字段,不能同时声明 JSON body 字段。`form` 始终指 URL query。
|
||
- 原文验签直接读取 `c.Request().Body`,在声明的小限额内 `io.ReadAll`;大文件用 `MultipartReader` 配合 `io.Copy` 流式处理,不能整个读入内存。
|
||
- 使用 `ParseMultipartForm` 时,其内存参数只是内存阈值,不是总上传限额。App/桥接器在请求结束(含错误和 panic)后删除临时文件;业务仍须关闭 `FormFile` 得到的文件。`MultipartReader` 不自动建临时文件,业务自行创建的文件由业务清理。
|
||
- 手写 Handler 的成功输出由它控制;返回的 error、鉴权拒绝仍走接口的错误策略。三方若要求错误也返回指定文本,使用 [自定义 Encoder](RESPONSE.md#手写路由配置文件与三方回调) 并维护对应错误文档;`@produces` 不会改变错误协议。
|
||
|
||
直接注册 Go 路由:
|
||
|
||
```go
|
||
app.HandleWithBodyLimit("POST", "/upload", 10<<20, nil, uploadHandler)
|
||
// nil encoder 沿用 App 策略,也可传项目自己的 response.Encoder。
|
||
```
|
||
|
||
通用注册器使用 `httpx.HandleWithBodyLimit(...)`;实现 `httpx.BodyLimitRegistrar` 可像 App 一样在全局中间件前限流量,未实现时只能在路由中间件入口限制。单独 `httpx.LimitBody` 同样受中间件顺序影响。
|
||
|
||
### 重新生成规则
|
||
|
||
`handlersGen.go`、`routesGen.go`、DTO、OpenAPI 继续由生成器管理;`*Bind.go`、`*Manual.go` 只首次创建,后续不会覆盖或删除。切换 manual 回自动模式时,先迁移实现并自行移除对应 Manual 文件,否则生成器明确拒绝,避免重复方法。修改 DTO 导致手写函数签名不匹配时,需要一起调整项目代码;`go test ./...` 是必要检查。
|
||
|
||
## 4. 自定义标量:项目定义行为,框架连接调用
|
||
|
||
例如输入 ID 兼容数字/字符串、输出统一为字符串:
|
||
|
||
```text
|
||
scalar BusinessID (
|
||
goImport: "example.com/billing/internal/ids"
|
||
goType: ID
|
||
jsonInput: string | int64
|
||
jsonOutput: string
|
||
text: true
|
||
)
|
||
type ItemReq {
|
||
ID BusinessID `path:"id"`
|
||
}
|
||
type ItemResp {
|
||
ID BusinessID `json:"id"`
|
||
}
|
||
```
|
||
|
||
`goImport`/`goType` 指向项目已有的具体 Go 类型,不是框架内置 ID。生成器创建类型别名和 `types.ContractScalars()`,然后把注册表显式传给每个 Binder。运行时使用标准接口:
|
||
|
||
| 项目类型的方法 | 用途 |
|
||
| --- | --- |
|
||
| 值接收者 `MarshalJSON() ([]byte, error)` | JSON 输出 |
|
||
| 指针接收者 `UnmarshalJSON([]byte) error` | JSON 输入 |
|
||
| 指针接收者 `UnmarshalText([]byte) error` | `text: true` 时的 path/query/header |
|
||
|
||
完整参考 [示例 ID 类型](examples/compatibility/ids/id.go)。CLI 校验声明并生成代码,不执行项目 Go 代码;启动时校验方法是否齐全,缺失会明确失败。
|
||
|
||
输入可选 `string`、`bool`、`int32`、`int64`、`float64`,用 `|` 声明多个允许的 JSON 类型;输出只声明一个。框架先校验 JSON 类型和数字范围,再调用项目转换方法。字符串是否是合法 ID、是否允许空串或负数由项目方法决定,不能把数字溢出静默截断。path/query/header 实际传输文本,OpenAPI 对这些位置描述 string;query 切片通过重复键传递。
|
||
|
||
普通字段不受注册影响,普通 `int64` 仍拒绝 JSON 字符串。没有全局注册表,也不会自动给所有 ID 放宽规则。标量可嵌套、放入切片/map,`*BusinessID` 允许 null;缺失默认必填,`optional`/`omitempty` 可选。标量的额外约束放在转换器中,目前不叠加 `binding`、default/options/range 等标签。
|
||
|
||
请求 OpenAPI 按 `string | int64` 生成 `anyOf`,响应按 `string` 生成,避免把输入兼容误描述为输出也有两种类型。输出方法必须遵守声明,框架不会每次重新验证响应;项目需为自定义类型测试边界行为。
|
||
|
||
不使用 `.api` 也可显式注册:
|
||
|
||
```go
|
||
binder := contract.MustNew[CreateReq](
|
||
contract.ScalarOf[ids.ID]("string|int64", "string", true),
|
||
)
|
||
// binder.Bind(c.Request(), httpx.RouteParams(c.Context()), &req)
|
||
```
|
||
|
||
## 可运行示例与验收
|
||
|
||
```sh
|
||
go run ./examples/compatibility
|
||
curl -i http://127.0.0.1:8080/legacy
|
||
curl -i -X POST http://127.0.0.1:8080/echo \
|
||
-H 'Content-Type: application/json' -d '{"id":9223372036854775807,"data":{"hello":true}}'
|
||
curl -i -F file=@README.md http://127.0.0.1:8080/upload
|
||
curl -OJ http://127.0.0.1:8080/download
|
||
```
|
||
|
||
完整契约:[compatibility.api](examples/contracts/compatibility.api)。生成项目起初的手写 hook 仍为 501;真实实现与原文 HMAC 验签回归见 [消费者 fixture](cmd/go-omni/apigen/testdata/compatibility)。
|
||
|
||
```sh
|
||
GO_OMNI_SCAFFOLD_INTEGRATION=1 go test -race ./cmd/go-omni/apigen -run Compatibility
|
||
go test -race ./httpx ./contract
|
||
```
|
||
|
||
这些是框架验收,不代表任何业务项目已经迁移完毕;业务的路由覆盖、响应兼容、权限、资金事务和数据库行为需要业务自己的回归。
|