后端开发规范:tio-boot、java-db 与 Kv
本文后续新增接口、重构和代码审查的基准。示例省略 import 及与说明无关的业务实现;业务对象 Kv 指 com.jfinal.kit.Kv。文档不固定框架版本,实际能力以项目依赖和对应源码为准。
1. 总体分层
请求链路:路由匹配 → 鉴权拦截器 → Handler → Service → Db。应用需要复用数据访问业务规则时,可在 Service 与 Db 之间使用薄 Store/DAO。
| 层次 | 应承担的职责 | 不应承担的职责 |
|---|---|---|
| 配置类 | 初始化框架能力、注册路由及拦截器 | 重复实现框架配置、启动时初始化业务表 |
| 路由配置 | 注册 HTTP 方法、路径、Handler 方法引用与权限元数据 | 解析参数、执行业务、维护第二份路由权限清单 |
| 鉴权拦截器 | 根据匹配路由认证身份,将身份放入请求上下文 | 判断订单状态、余额或资源归属 |
| Handler | 读取请求、校验格式和范围、取得当前身份、调用 Service、输出响应 | 执行数据库事务、堆积业务逻辑 |
| Service | 业务状态、权限归属、幂等、事务、数据库操作 | 读取 HttpRequest、设置 HttpResponse |
| Store/DAO | 复用应用的数据访问规则 | 自建连接池和事务管理器、重复包装所有 Db API |
| Db/DbPro | 通用 SQL、结果转换、参数绑定和事务 | 硬编码米旺表名、租户、用户权限或 HTTP 状态 |
Service 的接口业务方法推荐返回 RespBodyVo;供其他服务复用的查询方法可以返回 Kv、List<Kv> 或明确的业务类型。
2. 对象创建与 Aop 边界
| 对象 | 创建与持有方式 |
|---|---|
| 通常只执行一次的配置类 | new XxxConfiguration().config() |
| Handler | 在路由配置中 new,注册方法引用,由路由器持有 |
| 拦截器、异常处理器 | 直接创建并注册,由对应注册位置持有 |
| Service、DAO、Store 等共享对象 | 使用 Aop.get(...) |
| 需要环境参数的共享适配器、加密配置 | 显式构造后 Aop.put(...),再创建依赖它们的服务 |
Handler 本身不放进 Aop,但它依赖的 Service 仍通过 Aop 获取:
public class AuthHandler extends BaseHandler {
private final AuthService auth = Aop.get(AuthService.class);
}
不要为以上依赖额外编写无参委托构造器和依赖参数构造器。不要为了统一形式把所有对象都放入 Aop。不要在不同 Handler 中分别 new AuthService(),以免登录会话等共享状态分散。
Aop 创建对象时可能使用子类代理;交由 Aop 创建的类保留可用的无参构造方式,不声明为 final。配置完成后再替换 Aop 依赖不会更新已创建对象内部的引用。
测试应先注册共享依赖,再创建被测对象;需要独立 Service 实例时可使用 Aop.getPrototype(...)。测试结束清理测试容器和临时配置,避免相互污染。
3. 路由与鉴权
路由注册时明确指定 HTTP 方法,并将访问策略附在同一条路由元数据中:
AuthHandler authHandler = new AuthHandler();
router.add(
HttpMethod.POST,
"/api/mi/auth/sms/login",
authHandler::authSmsLogin,
Map.of(MiAuthInterceptor.ACCESS, AccessPolicy.PUBLIC));
框架负责按方法匹配路由及生成 405/Allow。应用不再自行比较请求方法,不再使用 routes.add(...) 维护另一份权限列表。
身份验证放在匹配路由后的拦截器中,依据该路由的策略区分公开、用户及管理员接口。Service 继续校验当前用户能否操作具体资源;登录成功不等于拥有全部资源权限。
已有 HttpInteceptorConfigure 容器不代表后台鉴权模型已经注册。应复用容器、保留已有拦截器,并按应用路由边界配置后台和业务鉴权,不能仅凭容器非空就省略必要配置。
4. Handler:先解析,再调用
每个阶段使用明确类型与具名变量。不要把校验、分页解析、条件表达式嵌进 Service 方法的实参。
public HttpResponse authSmsLogin(HttpRequest request) {
Kv parameters = Kv.create().set(request.getRequestMap());
String phone = MiValidators.validatePhone(parameters.get("phone"));
String code = ParameterValidator.text(parameters.get("code"), "code", 12);
Long inviterUserId = parameters.containsKey("inviterUserId")
? ParameterValidator.id(parameters.get("inviterUserId"), "inviterUserId")
: null;
RespBodyVo serviceResult = auth.smsLogin(phone, code, inviterUserId);
return TioRequestContext.getResponse().respond(serviceResult);
}
- 使用 HttpRequest 已有方法读取参数、请求头、原始正文;不要再次建立 RequestParams 包装层。
getRequestMap()合并查询/表单参数和 JSON 对象正文,JSON 同名字段优先。只有协议要求原始正文的场景才使用getBodyString(),例如支付签名验证。- 用 tio-utils 的
ParameterValidator校验格式、必填、枚举、长度、整数范围和对象结构。 - 手机号、地区编码等复用字段规则放进应用校验类,如
MiValidators;仍由 Handler 调用。 - 组合请求参数在 Handler 校验后形成 Kv,传入 Service。业务状态、余额、归属和数据库依赖的动态表单规则保留在 Service。
- 不重复引入 Input、JsonResponses 等已被框架能力覆盖的辅助层。
- 业务字段的长度限制属于业务校验;整个 HTTP 请求体的大小限制属于框架配置,不在业务代码写死 65536 等阈值。
5. Kv 与类型读取
业务参数、数据库记录、事务结果使用 Kv;记录列表使用 List<Kv>。
long productId = input.getLong("productId");
int quantity = input.getInt("quantity");
String requestNo = input.getStr("requestNo");
Kv fields = Kv.create()
.set("product_id", productId)
.set("quantity", quantity)
.set("client_request_no", requestNo);
使用 getLong、getInt、getStr 等替代 ((Number) value).longValue() 和反复的 String 强制转换。getter 的转换不能替代 Handler 对非法输入的严格校验。
getter 可能返回 null:必填值先校验,可选值使用 Long/Integer 等包装类型。比较两个包装类型的数值时使用 Objects.equals 或明确拆箱,不用 == 比较两个对象引用。
数据库 JSON 内部对象可能仍是 Map/List;必要时显式转换:
Kv payload = Kv.create().set((Map<?, ?>) record.get("payload"));
Long resourceId = payload.getLong("resourceId");
转换前应保证字段已校验且非空;不能直接假定所有反序列化对象都是 Kv。Kv 不保证字段遍历顺序。
Kv 规范针对动态业务记录。Map<String, String> 请求头、Map<String, byte[]> 密钥表、路由元数据等已有明确契约的结构无需机械改写为 Kv。
6. Db 与薄 Store/DAO
使用 java-db 时直接调用静态 nexus.io.db.activerecord.Db;不要在业务类中保存默认数据源的 DbPro 字段。命名数据源使用 Db.use(name),同一事务内保持同一数据源。
| API | 类型与用途 |
|---|---|
findMaps | 返回 List<Kv> |
findFirstMap | 返回 Kv,没有记录时返回 null |
paginateMap | 返回 Kv,包含 list、total、page、pageSize;list 为 List<Kv> |
insertMapReturning | 接收 Kv 字段,返回 Kv 记录 |
updateMapByColumns | 接收 Kv 字段及 Kv 条件,返回受影响行数 int |
toJsonbParameters | 返回 JDBC 参数数组 Object[],将其中的 Kv/Map/List 显式转为 JSONB |
txResult | 在事务中执行回调并返回明确类型的结果 |
上述分页、插入返回及按列更新方法有 PostgreSQL 方言要求;JSON 解析列必须明确提供。更新条件不能为空,租户与资源条件必须显式传入。不要将字符串 List 的 JSONB 语义与 SQL 数组语义混用。
静态 Kv 查询入口使用主数据源;只有纯读查询才按需显式使用读副本。不能把写入返回语句或加锁 SQL 发往读副本。
Store 可以保留米旺的 JSON 列清单、租户默认值、逻辑删除、用户状态检查、锁定和数据库错误到业务错误的转换。通用查询、分页和 SQL 拼接委托 Db。雪花 ID 与具体业务表规则不成为 Db 的隐含默认值。
米旺租户约束、行锁和幂等规则不能在“简化封装”时被省略。SQL 值通过参数绑定,不拼接用户输入。
7. 事务与响应分开
public RespBodyVo createOrder(long userId, Kv input) {
Kv result = db.tx(() -> {
db.lockUser(userId);
// 校验商品、幂等请求和金额,执行写入。
Kv order = createOrderRecord(userId, input);
return order;
});
return RespBodyVo.ok(result);
}
示例中的 createOrderRecord 代表实际业务写入。使用静态 Db 时同样先保存 Db.txResult(...) 的返回值,再构造响应。不要写 RespBodyVo.ok(db.tx(...))。
带结果事务通过抛异常回滚;txResult 正常返回 null/false 也是成功返回值,不表示回滚。需要 boolean 提交控制时才使用对应的 Db.tx API。嵌套事务遵守框架连接及回滚语义,不自建 ThreadLocal Connection。
8. 响应与异常
成功响应使用已完成的服务结果:
RespBodyVo serviceResult = service.execute(input);
return TioRequestContext.getResponse().respond(serviceResult);
respond(RespBodyVo) 负责序列化,不执行 Service,不自动补充 msg("ok"),也不覆盖已设置的 HTTP 状态。业务设置了消息就保留,没有设置就保持原样。
不使用 Supplier 包装每次 Service 调用来统一捕获异常。Java 会先执行 Service,再进入 respond;参数解析和 Service 抛出的异常由框架请求调度器捕获。
业务错误复用 nexus.io.tio.boot.exception.BusinessException:
BusinessException.require(allowed, 403, "Permission denied");
BusinessException.require(valid, "Invalid operation"); // 默认 400
throw new BusinessException(409, "Conflicting record");
业务异常状态范围为 400~599,使用 getStatus() 读取。应用可以注册 TioBootExceptionHandler:业务异常映射对应状态,参数异常映射 400,未知异常返回通用 500 消息;内部详情写日志,不返回给客户端。
自定义异常处理器、ThrowableHandler 或错误页面优先;没有由其完成响应时,框架可以为直接抛出的 BusinessException 生成默认业务错误响应。不要在每个 Handler 重复 try/catch,也不要在 java-db 中绑定 HTTP 异常类型。
9. 框架初始化与外部依赖
- 优先使用 tio-boot-admin 内置 Db、Redis、MongoDB、Interceptor、Handler、Controller 配置,配置类直接创建;先核对已有配置职责和实际生效条件,避免重复初始化。
TioAdminControllerConfiguration包含 ApiTable 控制器注册,不因业务自定义路由而省略后台所需的通用表能力。- Redis/MongoDB 配置类存在不代表应用必须依赖这些服务;按实际连接配置启用。米旺首版尽量不新增 Redis/ES,需要时先说明具体需求。
- 非必要不集成 Sa-Token,不把历史可选方案作为默认依赖。
- 初始化 SQL 由用户手动执行;应用启动不自动建表、初始化管理员或重置密码。隔离测试可在随机 schema 中创建测试结构,并在结束时清理。
- 第三方未接入时保留接口、编写伪代码并明确返回未实现,不能伪造支付、实名等成功结果。米旺固定短信验证码仅按开发环境开关启用,不用于生产。
- 密码和密钥放在本地配置中,不写入源码、示例、日志或规范文档。
10. 可读性与维护
- 使用明确类型,不使用 var。
- if/else 和循环体都写大括号,不写单行分支;复杂条件拆成能表达含义的变量。
- 参数解析、校验、Service 调用、响应输出分段;中间变量使用业务含义明确的名称。
- 不将 SQL、事务、参数解析等多个阶段嵌套进一行调用。
- 共享服务依赖在字段处
Aop.get(...)初始化;配置和 Handler 不进入 Aop。 - 优先查阅框架源码和已有文档;确认没有对应能力后再扩展通用方法。
- 尊重已有人工及其他 AI 修改,修改前读取当前代码,不覆盖与任务无关的内容。
- 新增框架 API 时同步调整调用方和文档;不在说明性标题或规范正文固定容易过期的版本号。
11. 修改后的验证清单
- [ ] 路由方法、路径和访问策略来自同一条注册信息。
- [ ] Handler 已独立解析和校验参数,Service 不依赖 HTTP 请求对象。
- [ ] Service、DAO 复用 Aop;配置、Handler 直接创建。
- [ ] 业务记录使用 Kv;类型 getter 未绕过参数校验或引入 null 拆箱、包装类型引用比较。
- [ ] 事务结果与响应包装分开,异常能正确回滚。
- [ ] 租户、用户归属、软删除、行锁和幂等规则保持完整。
- [ ] 成功消息不自动补充,异常通过统一入口处理。
- [ ] 第三方和数据库初始化符合上述边界。
- [ ] 受影响模块编译通过;运行与变更有关的数据库及 HTTP 回归测试。
- [ ] 框架能力变更完成本地安装,再构建依赖它的上层项目;代码、API 文档与示例保持一致。
米旺完整回归及打包命令:
mvn clean package '-Dmi.integration=true'
该集成测试使用独立随机 PostgreSQL schema;确认本地测试连接配置正确后执行。文档修改运行 pnpm docs:check 检查链接与导航;普通文档整理不要求重复执行业务测试。
