Skip to content

请求校验与响应转换 ​

通过框架统一控制请求大小、复用业务异常和转换响应值,可以让 Handler 专注于参数模型和服务调用。通用校验位于 tio-utils,HTTP 参数绑定位于 tio-http-common,响应转换位于 tio-boot,参数异常与业务异常由 java-model 提供。

统一配置正文大小 ​

properties
# 普通 JSON、表单和文本正文:32 KiB
http.max-request-body-size=32768
# 所有请求的正文总上限:20 MiB
http.multipart.max-request-size=20971520
# multipart 文件相关配置:10 MiB
http.multipart.max-file-size=10485760

所有值均为字节整数。http.max-request-body-size 默认 0,沿用总上限;正数表示普通正文上限,负数会阻止启动。普通正文同时受到总上限约束。multipart/form-data 上传继续使用原来的总请求及文件配置。

框架解码器在接收完整正文之前检查 Content-Length,超限时拒绝解码并关闭连接;这是传输层限制,不经过业务异常处理器。字段必填、长度和枚举仍由应用使用 ParameterValidator 校验。

实体与动态字段 ​

确定字段使用请求和响应实体,动态字段使用 Kv。通过 HttpRequest.getBodyObject(type) 直接绑定 JSON 正文,减少 Handler 中的重复解析代码。

getBodyObject(type) 使用配置的 JSON provider 解析当前正文。正文为 Java null 或解析结果为 null 时抛出 ParameterValidationException;其他解析与类型转换异常保留 JSON provider 的异常类型。支持的 JSON 结构取决于目标类型,例如实体对应对象、数组类型对应数组。字段必填规则由应用校验工具使用 ParameterValidator 检查。查询参数和已解码表单字段通过 getRequestMap() 单独读取,文件内容通过上传文件 API 获取。

该方法不判断 Content-Type,也不合并查询或表单字段。字符串等字段的类型转换遵循 JSON provider 的规则;业务校验针对绑定后的实体执行。字段约束、密码策略、手机号规则和业务凭证检查可以封装在应用自己的校验工具中。全局异常处理器捕获 ParameterValidationException 及所用 JSON provider 的解析异常并返回 HTTP 400,业务条件使用 BusinessException.require。

java
import com.jfinal.kit.Kv;
import nexus.io.tio.http.common.HttpRequest;

public class DynamicParameters {
  public Kv read(HttpRequest request) {
    return request.getBodyObject(Kv.class);
  }
}

动态正文绑定为 Kv,保留参数中的显式 null;确定字段使用下面的实体绑定方式。只读取查询或表单参数时,可使用 Kv.create().set(request.getRequestMap())。

在同一次请求中传递认证上下文 ​

拦截器完成 Token 校验后,可以使用 setLocalAttribute 保存当前请求的身份实体,Handler 再用 getLocalAttribute 读取。这样不需要把请求身份放进共享 Service 字段,也无需让仅用于本次请求的实体实现序列化接口。

java
package example.security;

import nexus.io.tio.http.common.HttpRequest;

public final class RequestIdentity {
  private static final String KEY = "example.identity";

  private RequestIdentity() {
  }

  public record Identity(String userId, String deviceId) {
  }

  public static void attach(HttpRequest request, Identity identity) {
    request.setLocalAttribute(KEY, identity);
  }

  public static Identity current(HttpRequest request) {
    return (Identity) request.getLocalAttribute(KEY);
  }
}

setAttribute 对应可序列化属性,值需要实现 Serializable;setLocalAttribute 接受普通对象,适合请求处理期间的上下文。两组方法各自存取,不混用。身份实体只能由可信的认证逻辑写入,业务 Service 仍需检查资源归属。

一个接口对应一个 Handler ​

请求模型放在 model 包,服务放在 service 包,Handler 放在 handler 包。下列文件分别保存到对应包中:

java
package example.model;

public record MaterialRequest(String caseId) { }

字段校验放在独立工具类中:

java
package example.validation;

import example.model.MaterialRequest;
import nexus.io.tio.utils.validator.ParameterValidator;

public final class MaterialParameterValidator {
  private MaterialParameterValidator() {
  }

  public static long caseId(MaterialRequest input) {
    return ParameterValidator.id(input.caseId(), "caseId");
  }
}
java
package example.service;

import com.jfinal.kit.Kv;
import nexus.io.db.activerecord.Db;
import nexus.io.model.exception.BusinessException;

public class MaterialService {
  public Kv detail(long caseId) {
    Kv row = Db.findFirstMap("select id, title from case_material where id=?", caseId);
    BusinessException.require(row != null, 404, "NOT_FOUND", "Material not found");
    return row;
  }
}
java
package example.handler;

import com.jfinal.kit.Kv;
import example.model.MaterialRequest;
import example.service.MaterialService;
import example.validation.MaterialParameterValidator;
import nexus.io.jfinal.aop.Aop;
import nexus.io.model.body.RespBodyVo;
import nexus.io.tio.boot.http.TioRequestContext;
import nexus.io.tio.boot.utils.ResponseValueNormalizer;
import nexus.io.tio.http.common.HttpRequest;
import nexus.io.tio.http.common.HttpResponse;

public class MaterialHandler {
  public HttpResponse handle(HttpRequest request) {
    MaterialRequest input = request.getBodyObject(MaterialRequest.class);
    long caseId = MaterialParameterValidator.caseId(input);
    MaterialService service = Aop.get(MaterialService.class);
    Kv data = service.detail(caseId);
    RespBodyVo body = RespBodyVo.ok(ResponseValueNormalizer.normalize(data));
    return TioRequestContext.getResponse().respond(body);
  }
}
java
import example.handler.MaterialHandler;
import nexus.io.tio.boot.server.TioBootServer;
import nexus.io.tio.http.server.router.HttpRequestRouter;

HttpRequestRouter router = TioBootServer.me().getRequestRouter();
router.post("/api/material/detail", new MaterialHandler()::handle);

Handler 和拦截器直接创建;Service 使用 Aop.get。认证由已注册的拦截器完成,服务继续检查资源归属与业务权限。

显式响应转换 ​

ResponseValueNormalizer.normalize(data) 不改变全局 JSON 设置,调用方按接口需要手动调用:

  • Long 转十进制字符串,保留 JavaScript 客户端读取大整数的精度。
  • Timestamp 转 UTC Instant 字符串,保留小数秒。
  • Map/Kv、List 和 record 递归转换,保留 null。
  • record 支持 accessor 上的 FastJson2 JSONField(name=...) 别名;空别名保留字段原名。
  • PostgreSQL PGobject 仅对 json/jsonb 解析为结构化数据,再递归转换;其他数据库类型保留原值。

输入实体和数据库对象不会被修改。普通 JavaBean、数组和其他类型保留原值,由所选 JSON provider 负责序列化;需要递归转换时使用上面列出的容器和 record。

统一业务异常 ​

java
import nexus.io.model.exception.BusinessException;

BusinessException.require(allowed, 403, "FORBIDDEN", "Permission denied");
BusinessException.require(valid, "Invalid operation");
throw new BusinessException(409, "CONFLICT", "Conflicting operation");

status 是 HTTP 错误状态,取值范围为 400~599;客户端约定的业务错误码(例如 "-10"、"USER_LOCKED")放在 code 参数中,两者互不影响。全局异常处理器捕获同一个 BusinessException,用 getStatus() 设置 HTTP 状态,用 getMessage() 生成 RespBodyVo.fail,按接口契约选择将 getCode() 放入业务错误信息。无业务码的构造方式仍然可用。参数异常则捕获 ParameterValidationException,不与业务异常混用。

通用工具复用 ​

java
import java.sql.Timestamp;
import java.time.Instant;
import nexus.io.tio.utils.crypto.Pbkdf2PasswordUtils;
import nexus.io.tio.utils.date.JdbcTimeUtils;

String encoded = Pbkdf2PasswordUtils.hash("example-password-123");
boolean matches = Pbkdf2PasswordUtils.matches("example-password-123", encoded);
Instant timestamp = JdbcTimeUtils.toInstant(Timestamp.from(Instant.now()));

密码工具使用随机盐和 PBKDF2WithHmacSHA256,校验沿用保存的参数格式。JdbcTimeUtils 接受 Timestamp、Instant、OffsetDateTime 和 null。业务专属密钥配置、手机号用途前缀、权限规则保留在应用服务中。

函数路由与 Service 方法 ​

函数路由可以直接注册 Service 方法并省略只负责转发的 Handler。完整示例、参数绑定、异常处理及 HttpRequestRouter.post 适配方式见 HttpRequestFunction:直接注册 Service 方法。

统一参数异常类型 ​

ParameterValidationException 与 BusinessException 统一由 java-model 提供。校验工具 ParameterValidator 仍位于 tio-utils,HTTP 参数绑定位于 tio-http-common;它们抛出同一个参数异常,便于应用集中处理。

java
import nexus.io.model.exception.ParameterValidationException;

throw new ParameterValidationException("name is required");

全局异常处理器使用上面的导入,捕获后设置 HTTP 400 并返回 RespBodyVo.fail(400, error.getMessage())。完整示例见函数路由的异常处理。迁移已有应用时,将旧包导入改为上述路径,并重新编译依赖方,使抛出与捕获使用同一类型。

MIT Licensed