请求校验与响应转换
通过框架统一控制请求大小、复用业务异常和转换响应值,可以让 Handler 专注于参数模型和服务调用。通用校验位于 tio-utils,HTTP 数据提取与响应转换位于 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。JSON 接口直接使用 request.getBodyString() 获取原始正文,再通过 JsonUtils.parse 解析为实体,无需先把参数序列化回 JSON。需要统一读取表单、JSON 或查询参数时,可使用框架的 request.getRequestMap(),不必自行实现 Content-Type 分支。
下面的公共工具先检查正文是否为空,再直接解析为实体。解析后检查结果非空,可同时拒绝 JSON null。字段必填、长度和业务规则在实体解析后按接口需要校验。
import nexus.io.tio.http.common.HttpRequest;
import nexus.io.tio.utils.json.JsonUtils;
import nexus.io.tio.utils.validator.ParameterValidator;
public final class RequestParameters {
private RequestParameters() {
}
public static <T> T body(HttpRequest request, Class<T> type) {
String body = request.getBodyString();
ParameterValidator.require(body != null && !body.trim().isEmpty(), "Request body is required");
T input = JsonUtils.parse(body, type);
ParameterValidator.require(input != null, "Request body is required");
return input;
}
}
正文非空等参数校验抛出 ParameterValidationException,由全局异常处理器转换为 400;JSON 解析异常也由全局异常处理器统一转换为 400。业务条件使用 BusinessException.require,避免静态导入后丢失调用者名称。
一个接口对应一个 Handler
请求模型放在 model 包,服务放在 service 包,Handler 放在 handler 包。下列文件分别保存到对应包中:
package example.model;
public record MaterialRequest(String 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 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;
import nexus.io.tio.utils.json.JsonUtils;
import nexus.io.tio.utils.validator.ParameterValidator;
public class MaterialHandler {
public HttpResponse handle(HttpRequest request) {
MaterialRequest input = JsonUtils.parse(request.getBodyString(), MaterialRequest.class);
ParameterValidator.require(input != null, "Request body is required");
long caseId = ParameterValidator.id(input.caseId(), "caseId");
MaterialService service = Aop.get(MaterialService.class);
Kv data = service.detail(caseId);
return TioRequestContext.getResponse().respond(
RespBodyVo.ok(ResponseValueNormalizer.normalize(data)));
}
}
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");
全局异常处理器捕获同一个 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。业务专属密钥配置、手机号用途前缀、权限规则保留在应用服务中。
