Spring Framework

第08篇:Data Binding类型绑定

2026-01-295 min readJava 专题系列
Spring Framework

公众号: 西魏陶渊明
CSDN: https://springlearn.blog.csdn.net

天下代码一大抄, 抄来抄去有提高, 看你会抄不会抄!

一、前言

1.1 什么是数据绑定?

本文我们所描述的数据绑定可以理解成。

  1. 将结构化的数据文本, 转换成Java对象。
  2. 通过Spring提供的API方式,而不通过反射的方式将属性信息,绑定到Java对象。

1.2 为什么需要数据绑定API呢?

为什么需要数据绑定呢? 因为在Spring中很多地方都使用到了数据绑定,通过提供统一的API,有以下这些好处。

  1. 不使用反射,给开发者提供了一种更优雅的方式。
  2. 逻辑业务收口,将Spring中自带的很多功能进行组合,通过一个API的方式进行业务收口,而不是要开发者自己去组装Spring已有的功能。
  3. 通过提供标准的API的方式,在标准API中暴露扩展点,方便开发者二次开发,定制转换逻辑。

1.3 Spring中常见的数据绑定有那些?

在Spring中随处可见参数的绑定如下。

  1. 比如 @RequestBody 如何将参数绑定到实体对象上。
  2. 应用配置信息如何通过 @ConfigurationProperties将参数绑定到配置实体类中
  3. 参数绑定如何实现,弱类型绑定绑定。即: 下划线自动转驼峰,忽略字母大小写。 ConfigurationPropertiesBinder

如果你有下面这些疑问,那么本篇文章将会告诉你这些问题的答案,希望对你有用。

二、数据绑定API

在回答 1.3 提出的问题前,我们先了解下在Spring中有那些能进行数据绑定的工具类把。

为了方面演示,首先我们这里先定义一个实体。

java
1@Data 2@ToString 3public class User{ 4 5 @NotBlank(message = "name 不能为空") 6 @Size(min = 2, max = 120, message = "不能小于2字符,大于120字符") 7 private String name; 8 9 @Max(value = 120, message = "年龄不能大于120") 10 @Min(value = 0, message = "年龄不能小于0") 11 private Integer age; 12 13 @NotEmpty(message = "家庭成员不能为空") 14 @Size(max = 4, message = "数量不能超过4个") 15 private List<String> membersFamily; 16 17 @NotNull(message = "地址不能为空") 18 private Address address; 19}

2.1 BeanWrapper

首先分享一个简单的用法,使用这种方式我们可以给一个实体对象,进行属性的绑定。(PS: 如果你看过Spring自动注入的原理,会发现也会用到BeanWrapper:AbstractAutowireCapableBeanFactory#autowireBean

下面我们给User对象进行属性的绑定。

java
1 @Test 2 public void testBeanWrapper() { 3 User emptyUser = new User(); 4 BeanWrapper beanWrapper = new BeanWrapperImpl(emptyUser); 5 beanWrapper.setPropertyValue("name", "Jay"); 6 beanWrapper.setPropertyValue("address", new Address("中国")); 7 beanWrapper.setPropertyValue(new PropertyValue("age", "32")); 8 beanWrapper.setPropertyValue(new PropertyValue("membersFamily", Arrays.asList("谢霆锋", "孙悟空"))); 9 System.out.println(emptyUser); 10 // User(name=Jay, age=32, membersFamily=[谢霆锋, 孙悟空], address=Address(name=中国, oneAddress=null, twoAddress=null)) 11 }

BeanWrapper还可以针对字段进行定制转换逻辑,我们看下面这几个API.

  • void registerCustomEditor(Class<?> requiredType, PropertyEditor propertyEditor) 如果不指定字段名称,可以只针对类型进行定制。

  • void registerCustomEditor(@Nullable Class<?> requiredType, @Nullable String propertyPath, PropertyEditor propertyEditor) 根据类型 + 字段名注册。

  • PropertyEditor findCustomEditor(@Nullable Class<?> requiredType, @Nullable String propertyPath); 根据类型获取属性编辑器

  1. 给name字段绑定时候,将value转换成小写。
java
1 @Test 2 public void testBeanWrapper2() { 3 BeanWrapper beanWrapper = new BeanWrapperImpl(User.class); 4 // String类型,属性是name的,将属性自动转换成小写。 5 beanWrapper.registerCustomEditor(String.class, "name", new PropertyEditorSupport() { 6 @Override 7 public void setAsText(String text) throws IllegalArgumentException { 8 super.setValue(text.toLowerCase(Locale.ROOT)); 9 } 10 }); 11 beanWrapper.setPropertyValue("name", "Jay"); 12 beanWrapper.setPropertyValue("address", new Address("中国")); 13 beanWrapper.setPropertyValue(new PropertyValue("age", "32")); 14 beanWrapper.setPropertyValue(new PropertyValue("membersFamily", Arrays.asList("谢霆锋", "孙悟空"))); 15 // User(name=jay, age=32, membersFamily=[谢霆锋, 孙悟空], address=Address(name=中国, oneAddress=null, twoAddress=null)) 16 System.out.println(beanWrapper.getWrappedInstance()); 17 }

除此之外,Spring中还有很多内置的属性编辑器,如下表格。

Class作用
ByteArrayPropertyEditor将字符串转换为相应的字节表示形式。BeanWrapperImpl默认注册。
ClassEditor将表示类的字符串解析为实际类,反之亦然。当找不到类时,将引发IllegalArgumentException。默认情况下,由BeanWrapperImpl注册
CustomBooleanEditor布尔属性的可自定义属性编辑器。默认情况下,由BeanWrapperImpl注册,但可以通过将其自定义实例注册为自定义编辑器来覆盖。
CustomCollectionEditor集合的属性编辑器,将任何源集合转换为给定的目标集合类型。
CustomDateEditorjava.util的可定制属性编辑器。日期,支持自定义DateFormat。默认情况下未注册。用户必须根据需要以适当的格式注册。
CustomNumberEditor任何Number子类的可自定义属性编辑器,如Integer、Long、Float或Double。默认情况下,由BeanWrapperImpl注册,但可以通过将其自定义实例注册为自定义编辑器来覆盖。
FileEditor将字符串解析为java.io。文件对象。默认情况下,由BeanWrapperImpl注册。
InputStreamEditor单向属性编辑器,可以获取字符串并(通过中间的ResourceEditor和Resource)生成InputStream,以便可以将InputStriam属性直接设置为字符串。请注意,默认用法不会为您关闭InputStream。默认情况下,由BeanWrapperImpl注册。
LocaleEditor可以将字符串解析为Locale对象,反之亦然(字符串格式为[language][country][variant],与Locale的toString()方法相同)。也接受空格作为分隔符,作为下划线的替代。默认情况下,由BeanWrapperImpl注册。
PatternEditor将字符串解析为java.util.regex。
PropertiesEditor可以将字符串(使用java.util.Properties类的javadoc中定义的格式格式化)转换为Properties对象。默认情况下,由BeanWrapperImpl注册。
StringTrimmerEditor修剪字符串的属性编辑器。可选地,允许将空字符串转换为空值。默认情况下未注册 — 必须是用户注册的。
URLEditor可以将URL的字符串表示解析为实际的URL对象。默认情况下,由BeanWrapperImpl注册。

这些属性编辑器都比较简单,我们只分析其中的一个。CustomBooleanEditor

java
1public class CustomBooleanEditor extends PropertyEditorSupport { 2 // 当text等于on/yes/1的时候都会自动转换成布尔类型。true 3 @Override 4 public void setAsText(@Nullable String text) throws IllegalArgumentException { 5 6 } 7 8}

2.2 Binder

Binder 可以给绑定的对象添加指定的前缀。

java
1 @Test 2 public void test2() { 3 MockEnvironment environment = new MockEnvironment(); 4 // 下划线,中划线自动转驼峰,大小写不敏感 5 environment.setProperty("customer.Name", "Jay"); 6 // 大小写不敏感 7 environment.setProperty("customer.Age", "0"); 8 environment.setProperty("customer.members_family", "周杰伦,谢霆锋,诸葛亮"); 9 environment.setProperty("customer.address.name", "杭州"); 10 environment.setProperty("customer.address.oneAddress", "上城区"); 11 environment.setProperty("customer.address.twoAddress", "学区房"); 12 13 Binder binder = Binder.get(environment); 14 User user = new User(); 15 // 这里我们声明所有的前缀都是customer开头 16 binder.bind(ConfigurationPropertyName.of("customer"), Bindable.ofInstance(user), null); 17 // User(name=Jay, age=0, membersFamily=[周杰伦, 谢霆锋, 诸葛亮], address=Address(name=杭州, oneAddress=上城区, twoAddress=学区房)) 18 System.out.println(user); 19 }

2.3 DataBinder

  • setDisallowedFields() 设置需要忽略的字段,如果给忽略的字段赋值,就会报错。
  • setAllowedFields() 允许绑定的,如果没有指定默认都允许,如果指定了就仅限指定的字段,可以绑定。
  • setRequiredFields() 指定的字段必须要有值,不能为null。
  • registerCustomEditor() 注册转换逻辑。
  • addValidators(...) 添加绑定的验证器。
  • bind() 执行绑定动作。
  • validate() 执行规则校验。
  • getBindingResult() 获取绑定结果信息
  • getTarget() 获取绑定后的数据对象
java
1 @Test 2 public void testDataBinder() { 3 // 绑定那个字段,以及绑定的逻辑 4 DataBinder dataBinder = new DataBinder(new User()); 5 // 忽略不绑定的,如果有绑定就报错 6// dataBinder.setDisallowedFields("age"); 7 // 允许绑定的,如果没有指定默认都允许,如果指定了就仅限指定的字段,可以绑定。 8// dataBinder.setAllowedFields("name","address","age"); 9 // 必须要有值,不能为null 10 dataBinder.setRequiredFields("age"); 11 // 字段转换器 12 dataBinder.registerCustomEditor(String.class, "name", new PropertyEditorSupport() { 13 @Override 14 public void setAsText(String text) throws IllegalArgumentException { 15 // name转成大写 16 setValue(text.toUpperCase()); 17 } 18 }); 19 dataBinder.registerCustomEditor(Address.class, "address", new PropertyEditorSupport() { 20 @Override 21 public void setAsText(String text) throws IllegalArgumentException { 22 // name转成大写 23 setValue(text.toUpperCase()); 24 } 25 26 @Override 27 public void setValue(Object value) { 28 if (value instanceof Address) { 29 ((Address) value).setName("Beijing"); 30 } 31 super.setValue(value); 32 } 33 }); 34 // 添加验证规则 35 dataBinder.addValidators(new Validator() { 36 @Override 37 public boolean supports(Class<?> clazz) { 38 return User.class.equals(clazz); 39 } 40 41 @Override 42 public void validate(Object target, Errors errors) { 43 if (((User) target).getAge() <= 0) { 44 errors.rejectValue("age", "年龄不合法"); 45 } 46 } 47 }); 48 PropertyValues pvs = new MutablePropertyValues( 49 Arrays.asList(new PropertyValue("name", "jar"), 50 new PropertyValue("age", 0), 51 new PropertyValue("address", new Address()))); 52 dataBinder.bind(pvs); 53 // 验证字段 54 dataBinder.validate(); 55 BindingResult results = dataBinder.getBindingResult(); 56 // org.springframework.validation.BeanPropertyBindingResult: 1 errors 57 // Field error in object 'target' on field 'age': rejected value [0]; codes [年龄不合法.target.age,年龄不合法.age,年龄不合法.java.lang.Integer,年龄不合法]; arguments []; default message [null] 58 System.out.println(results); 59 // User(name=JAR, age=0, membersFamily=null, address=Address(name=Beijing, oneAddress=null, twoAddress=null)) 60 System.out.println(dataBinder.getTarget()); 61 } 62

2.4 WebDataBinder 自定义Web请求参数转换器

WebDataBinder 继承自DataBinder Spring允许通过其进行自定义的类型转换,从而将请求参数转换成方法参数。

  • 如下我们定义一个get请求,将请求参数转换成 Address对象。
java
1 @GetMapping("get") 2 public String test(@RequestParam("ads") Address address) { 3 return address.toString(); 4 }
  • 首先我们先实现PropertyEditorRegistrar,设置解析的规则
java
1@Component("customPropertyEditorRegistrar") 2public class AddressPropertyEditorRegistrar implements PropertyEditorRegistrar { 3 4 @Override 5 public void registerCustomEditors(PropertyEditorRegistry registry) { 6 registry.registerCustomEditor(Address.class, new PropertyEditorSupport() { 7 @Override 8 public void setAsText(String text) throws IllegalArgumentException { 9 String[] split = text.split("|"); 10 Address address = new Address(); 11 address.setOneAddress(split[0]); 12 address.setTwoAddress(split[1]); 13 setValue(address); 14 } 15 }); 16 } 17}
  • 给WebDataBinder添加转换器
java
1@RestController 2public class RegisterUserController 3 4 @Autowired 5 private PropertyEditorRegistrar customPropertyEditorRegistrar; 6 7 /** 8 * 只对当前接口有效 9 * 10 * @param binder 11 */ 12 @InitBinder 13 void initBinder(WebDataBinder binder) { 14 this.customPropertyEditorRegistrar.registerCustomEditors(binder); 15 } 16 // GET http://localhost:8080/get?ads=杭州市|上城区 17 @GetMapping("get") 18 public String test(@RequestParam("ads") Address address) { 19 // Address(name=null, oneAddress=杭州市, twoAddress=上城区) 20 return address.toString(); 21 } 22}

三、问题解答

3.1 @RequestBody 如何将参数绑定到实体对象上

主要思路就是从HttpServletRequest中获取参数,通过读取请求方法的 MediaType 类型,选择对应的 HttpMessageConverter 转换器。

  1. HandlerMethodArgumentResolver 处理web方法的参数
  2. HandlerMethodReturnValueHandler 处理web方法的参数

3.1.1 执行器请求方法

同时如果请求类的使用到了 @InitBinder 自定义请求参数转换,在这里也会被执行。

java
1 // 从Request对象中获取方法参数 getMethodArgumentValues 2 @Nullable 3 public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, 4 Object... providedArgs) throws Exception { 5 6 Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs); 7 if (logger.isTraceEnabled()) { 8 logger.trace("Arguments: " + Arrays.toString(args)); 9 } 10 return doInvoke(args); 11 }

3.1.2 转换请求参数

  • RequestResponseBodyMethodProcessor

3.1.3 @RequestBody 处理

这里主要使用到的接口是 HttpMessageConverter,同时这里我们能看到一段逻辑就是 什么时候会进行参数检查。就在 validateIfApplicable,如果参数失败就会抛出 MethodArgumentNotValidException 异常。

3.2 ConfigurationProperties 参数绑定原理

  • ConfigurationPropertiesBindingPostProcessor

在配置类Bean, 初始化前,执行绑定。

java
1public class ConfigurationPropertiesBindingPostProcessor 2 implements BeanPostProcessor, PriorityOrdered, ApplicationContextAware, InitializingBean { 3 4 private ConfigurationPropertiesBinder binder; 5 6 // 从容器中获取绑定工具类,ConfigurationPropertiesBinder 7 @Override 8 public void afterPropertiesSet() throws Exception { 9 this.registry = (BeanDefinitionRegistry) this.applicationContext.getAutowireCapableBeanFactory(); 10 this.binder = ConfigurationPropertiesBinder.get(this.applicationContext); 11 } 12 13 // bean初始化前,进行绑定 14 @Override 15 public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { 16 bind(ConfigurationPropertiesBean.get(this.applicationContext, bean, beanName)); 17 return bean; 18 } 19 20 private void bind(ConfigurationPropertiesBean bean) { 21 if (bean == null || hasBoundValueObject(bean.getName())) { 22 return; 23 } 24 Assert.state(bean.getBindMethod() == BindMethod.JAVA_BEAN, "Cannot bind @ConfigurationProperties for bean '" 25 + bean.getName() + "'. Ensure that @ConstructorBinding has not been applied to regular bean"); 26 try { 27 this.binder.bind(bean); 28 } 29 catch (Exception ex) { 30 throw new ConfigurationPropertiesBindException(bean, ex); 31 } 32 } 33 34}

3.2.1 ConfigurationPropertiesBinder

因为在构造中就获取到了Spring的上下文对象,所有就能从Spring中获取到所有他想要的工具。 最终调用前面我们学过的 Binder 去进行绑定。

java
1class ConfigurationPropertiesBinder { 2 3 ConfigurationPropertiesBinder(ApplicationContext applicationContext) { 4 this.applicationContext = applicationContext; 5 this.propertySources = new PropertySourcesDeducer(applicationContext).getPropertySources(); 6 this.configurationPropertiesValidator = getConfigurationPropertiesValidator(applicationContext); 7 this.jsr303Present = ConfigurationPropertiesJsr303Validator.isJsr303Present(applicationContext); 8 } 9 10 BindResult<?> bind(ConfigurationPropertiesBean propertiesBean) { 11 Bindable<?> target = propertiesBean.asBindTarget(); 12 ConfigurationProperties annotation = propertiesBean.getAnnotation(); 13 BindHandler bindHandler = getBindHandler(target, annotation); 14 // 获取到前缀,进行绑定 15 return getBinder().bind(annotation.prefix(), target, bindHandler); 16 } 17}

3.2.2 ConstructorBinding 的用法

最后我们介绍一个很少用的注解,这个注解往往用于配合 @ConfigurationProperties使用,但是使用的场景不多。

@ConstructorBinding 可用于指示应该,使用构造函数参数而不是调用setter来绑定配置属性的注释。可以在类型级别(如果有明确的构造函数)或在要使用的实际构造函数上添加。

最后,都看到这里了,最后如果这篇文章,对你有所帮助,请点个关注,交个朋友。