Spring 将转换、表示格式化、对象绑定和验证视为相互独立的操作。因此,请求参数可以转换为类型化属性,同时仍能生成结构化错误,而无需将领域对象与 Web 控制器耦合。
转换与格式化 #
ConversionService 是类型转换的门面。它可以先判断是否存在可用的转换,然后按原始类或按 TypeDescriptor 执行转换;后者会保留注解和嵌套泛型信息。
扩展接口分别覆盖不同的需求:
Converter<S, T>实现一次强类型转换。ConverterFactory<S, R>为一组目标子类型创建转换器,例如将String转换为任意枚举类型。GenericConverter声明可转换的源/目标类型对集合,并接收TypeDescriptor值,以执行依赖上下文的转换。ConditionalGenericConverter还可以判断自身是否匹配特定的源类型和目标类型。
final class StringToOrderId implements Converter<String, OrderId> {
@Override
public OrderId convert(String source) {
return new OrderId(UUID.fromString(source.strip()));
}
}Formatter<T> 属于表示层。其 Printer 和 Parser 操作使用 Locale,因此适用于数字、日期和面向用户的文本。对于与上下文无关的类型变更,应使用转换器;当文本表示取决于区域设置或 UI 策略时,应使用格式化器。通过 FormatterRegistry 注册二者;DefaultFormattingConversionService 同时实现注册表和转换服务契约。
遗留 JavaBeans 属性编辑器 #
Spring 仍出于兼容性目的支持 java.beans.PropertyEditor。PropertyEditor 有状态,主要通过文本和内部值工作,并且不是线程安全的。因此,容器不会共享实例,而是为每次绑定操作创建编辑器。
JavaBeans 约定会查找与 Type 位于同一位置、名为 TypeEditor 的编辑器;PropertyEditorManager 还支持显式注册和搜索路径。Spring 增加了用于按类型或属性路径注册的 PropertyEditorRegistry,以及用于封装可复用注册逻辑的 PropertyEditorRegistrar。BeanWrapper 和 DataBinder 会公开这些注册表。
新的应用程序转换应使用 ConversionService;当集成要求使用属性编辑器的 JavaBeans API 时,属性编辑器仍然适用。HTTP 消息体通常不会由属性编辑器解码:Spring MVC 使用 HttpMessageConverter 处理 JSON 等消息体格式。
使用 DataBinder 进行绑定
#
DataBinder 将输入的属性值应用到目标对象。它解析嵌套属性路径,调用转换器或属性编辑器,将失败记录在 BindingResult 中,并可以调用验证器。
var binder = new DataBinder(command, "order");
binder.setAllowedFields("customerId", "quantity");
binder.setConversionService(conversionService);
binder.addValidators(new OrderValidator());
binder.bind(propertyValues);
binder.validate();
BindingResult result = binder.getBindingResult();绑定不可信输入属于安全边界。应定义允许的字段,或使用专用命令对象,而不要暴露范围过大的领域对象。转换失败会成为字段错误,而不是要求异常逃逸出绑定操作。嵌套路径和集合的自动增长可以配置;对于由攻击者控制的输入,应设置上限。
Spring 的 Validator 契约
#
Spring Validator 声明其支持的类,并通过 Errors 报告失败:
final class OrderValidator implements Validator {
public boolean supports(Class<?> type) {
return OrderCommand.class.isAssignableFrom(type);
}
public void validate(Object target, Errors errors) {
var order = (OrderCommand) target;
ValidationUtils.rejectIfEmpty(errors, "customerId", "required");
if (order.quantity() <= 0) {
errors.rejectValue("quantity", "positive");
}
}
}Errors.pushNestedPath() 和 popNestedPath() 允许组合验证器使用正确的路径报告子对象错误。错误代码是逻辑标识符;消息解析稍后可以选择本地化文本。
Jakarta Bean Validation #
Spring 6 集成了 jakarta.validation Bean Validation 3.x。LocalValidatorFactoryBean 会引导 Hibernate Validator 等提供程序,并同时实现 Jakarta 的 ValidatorFactory/Validator 访问能力以及 Spring 的 Validator 接口。它参与 Spring 的生命周期管理,并在关闭时关闭底层工厂。
MVC 绑定可以通过 @Valid 或 @Validated 应用约束注解。方法验证通过 Spring 的方法验证基础设施,将约束应用于方法参数和返回值。验证不能替代转换:值必须先以约束所作用的类型表示。
评论
在 GitHub 上阅读或评论(发表评论需要登录)