Skip to content

后端开发规范: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 获取:

java
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 方法,并将访问策略附在同一条路由元数据中:

java
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 方法的实参。

java
import com.jfinal.kit.Kv;
import nexus.io.model.body.RespBodyVo;
import nexus.io.tio.boot.http.TioRequestContext;
import nexus.io.tio.http.common.HttpRequest;
import nexus.io.tio.http.common.HttpResponse;
import nexus.io.tio.utils.validator.ParameterValidator;

public HttpResponse authSmsLogin(HttpRequest request) {
  Kv parameters = request.getBodyObject(Kv.class);
  String phone = ParameterValidator.text(parameters.get("phone"), "phone", 11);
  ParameterValidator.require(phone.matches("1[3-9][0-9]{9}"), "请输入大陆手机号");
  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 正文通过 getBodyObject() 绑定实体,动态字段使用 Kv。需要手工解析 JSON 或保留原始正文进行支付验签时,使用 getBodyString()。
  • 用 tio-utils 的 ParameterValidator 校验格式、必填、枚举、长度、整数范围和对象结构。
  • 手机号、地区编码等复用字段规则放进应用校验类,如 MiValidators;仍由 Handler 调用。
  • 组合请求参数在 Handler 校验后形成 Kv,传入 Service。业务状态、余额、归属和数据库依赖的动态表单规则保留在 Service。
  • 不重复引入 Input、JsonResponses 等已被框架能力覆盖的辅助层。
  • 业务字段的长度限制属于业务校验;整个 HTTP 请求体的大小限制属于框架配置,不在业务代码写死 65536 等阈值。

5. Kv 与类型读取 ​

业务参数、数据库记录、事务结果使用 Kv;记录列表使用 List<Kv>。

java
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;必要时显式转换:

java
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 值通过参数绑定,不拼接用户输入。

无需解析 JSON 列时,可以使用 Db.findFirstMap(sql, paras) 或 DbPro.findFirstMap(sql, paras),不必传入空的 JSON 列数组。没有匹配记录仍返回 null,调用 getter 前必须判空;静态 Db 入口继续使用主数据源。

需要解析 JSON 列时使用 findFirstMap(sql, jsonFields, parameters),其中 parameters 为 Object[];无 SQL 参数时显式传 new Object[0]。避免仅传 findFirstMap(sql, jsonFields):新增重载后,String[] 可能匹配为 SQL 参数数组。单个 SQL 参数本身为 String[] 时,用 new Object[] { values } 包装;SQL NULL 使用 (Object) null 或 new Object[] { null }。

7. 事务与响应分开 ​

java
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. 响应与异常 ​

成功响应使用已完成的服务结果:

java
RespBodyVo serviceResult = service.execute(input);
return TioRequestContext.getResponse().respond(serviceResult);

respond(RespBodyVo) 负责序列化,不执行 Service,不自动补充 msg("ok"),也不覆盖已设置的 HTTP 状态。业务设置了消息就保留,没有设置就保持原样。

不使用 Supplier 包装每次 Service 调用来统一捕获异常。Java 会先执行 Service,再进入 respond;参数解析和 Service 抛出的异常由框架请求调度器捕获。

业务错误复用 nexus.io.model.exception.BusinessException,参数校验错误使用 nexus.io.model.exception.ParameterValidationException。全局异常处理器将参数异常转换为 HTTP 400,业务异常使用其对应的 HTTP 状态:

java
import nexus.io.model.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. 可读性与维护 ​

  1. 使用明确类型,不使用 var。
  2. if/else 和循环体都写大括号,不写单行分支;复杂条件拆成能表达含义的变量。
  3. 参数解析、校验、Service 调用、响应输出分段;中间变量使用业务含义明确的名称。
  4. 不将 SQL、事务、参数解析等多个阶段嵌套进一行调用。
  5. 共享服务依赖在字段处 Aop.get(...) 初始化;配置和 Handler 不进入 Aop。
  6. 优先查阅框架源码和已有文档;确认没有对应能力后再扩展通用方法。
  7. 尊重已有人工及其他 AI 修改,修改前读取当前代码,不覆盖与任务无关的内容。
  8. 新增框架 API 时同步调整调用方和文档;不在说明性标题或规范正文固定容易过期的版本号。

11. 修改后的验证清单 ​

  • [ ] 路由方法、路径和访问策略来自同一条注册信息。
  • [ ] Handler 已独立解析和校验参数,Service 不依赖 HTTP 请求对象。
  • [ ] Service、DAO 复用 Aop;配置、Handler 直接创建。
  • [ ] 业务记录使用 Kv;类型 getter 未绕过参数校验或引入 null 拆箱、包装类型引用比较。
  • [ ] 事务结果与响应包装分开,异常能正确回滚。
  • [ ] 租户、用户归属、软删除、行锁和幂等规则保持完整。
  • [ ] 成功消息不自动补充,异常通过统一入口处理。
  • [ ] 第三方和数据库初始化符合上述边界。
  • [ ] 受影响模块编译通过;运行与变更有关的数据库及 HTTP 回归测试。
  • [ ] 框架能力变更完成本地安装,再构建依赖它的上层项目;代码、API 文档与示例保持一致。

米旺完整回归及打包命令:

powershell
mvn clean package '-Dmi.integration=true'

该集成测试使用独立随机 PostgreSQL schema;确认本地测试连接配置正确后执行。文档修改运行 pnpm docs:check 检查链接与导航;普通文档整理不要求重复执行业务测试。

延伸阅读 ​

MIT Licensed