请求校验与响应转换
通过框架统一控制请求大小、复用业务异常和转换响应值,可以让 Handler 专注于参数模型和服务调用。通用校验位于 tio-utils,HTTP 参数绑定位于 tio-http-common,响应转换位于 tio-boot,参数异常与业务异常由 java-model 提供。
统一配置正文大小
# 普通 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。
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 字段,也无需让仅用于本次请求的实体实现序列化接口。
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 包。下列文件分别保存到对应包中:
package example.model;
public record MaterialRequest(String caseId) { }字段校验放在独立工具类中:
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");
}
}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;
}
}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);
}
}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。
统一业务异常
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,不与业务异常混用。
通用工具复用
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;它们抛出同一个参数异常,便于应用集中处理。
import nexus.io.model.exception.ParameterValidationException;
throw new ParameterValidationException("name is required");全局异常处理器使用上面的导入,捕获后设置 HTTP 400 并返回 RespBodyVo.fail(400, error.getMessage())。完整示例见函数路由的异常处理。迁移已有应用时,将旧包导入改为上述路径,并重新编译依赖方,使抛出与捕获使用同一类型。
