天天看點

聊聊 Spring 核心特性中的資料綁定 (DataBinder)前面的話了解資料綁定PropertyValuesDataBinderSpring XML 配置中屬性設定分析WEB 請求參數資料綁定分析總結

前面的話

Spring 的核心特性包括 IOC 容器、事件、資源管理、國際化、校驗、資料綁定、類型轉換、EL 表達式、AOP。其他特性可以輕易的在網絡上找到很多資料,而資料綁定這個特性即便在 Spring 官網描述卻也不太多。這是因為資料綁定主要應用于 Spring 内部,對于使用者而言直接使用的場景并不多。如果想要深入了解 Spring 内部的運作機制,資料綁定是必須了解的一塊内容。

了解資料綁定

資料綁定允許将使用者輸入的内容動态綁定到應用程式中的領域模型中。對于 Spring 而言,用于輸入具體的場景主要包括 xml 中 bean 的定義、web 環境下的請求參數。

資料綁定在 Spring 中的核心類是

org.springframework.validation.DataBinder

,可以看到這個類位于 validation 包中,資料綁定時也往往伴随着參數校驗。先看看如何手動使用這個類。

@Data
public class LoginDTO {

    private String username;

    private String password;
}

public class App {

    public static void main(String[] args) {
        LoginDTO dto = new LoginDTO();

        Map<String, Object> input = new HashMap<>();
        input.put("username", "hkp");
        input.put("password", "123");
        PropertyValues propertyValues = new MutablePropertyValues(input);

        DataBinder dataBinder = new DataBinder(dto, "loginDTO");
        dataBinder.bind(propertyValues);
		
		// LoginDTO(username=hkp, password=123)
        System.out.println(dto);
    }
}
           

DataBinder#bind

是 DataBinder 類的核心方法,它接收 PropertyValues 類型的參數,并将參數中的資料設定到對象的屬性中。

PropertyValues

PropertyValues 可以簡單的了解為 Map,它包含更多的屬性資訊。接口核心方法定義如下。

public interface PropertyValues extends Iterable<PropertyValue>

	PropertyValue[] getPropertyValues();

	PropertyValue getPropertyValue(String propertyName);

	boolean contains(String propertyName);

	boolean isEmpty();
}
           

PropertyValues 繼承接口 Iterable,可以了解為 PropertyValue 的容器,PropertyValue 表示對象中的某一個屬性值,包含的主要字段如下。

public class PropertyValue extends BeanMetadataAttributeAccessor implements Serializable {

	private final String name;

	private final Object value;

	private boolean optional = false;
	// 是否進行過類型轉換
	private boolean converted = false;
	// 類型轉換後的值
	private Object convertedValue;
}
           

PropertyValue 包括原始值和轉換後的值,也就意味着 Spring 可能會對用于輸入的值進行類型轉換。

PropertyValues 接口常用實作如下。

  • MutablePropertyValues:标準的 PropertyValues 實作,除了屬性擷取,還支援對屬性的設定。
  • ServletConfigPropertyValues:支援 web 環境擷取 servlet 上下文配置的 PropertyValues。
  • ServletRequestParameterPropertyValues:支援 web 環境擷取請求參數的 PropertyValues。

DataBinder

DataBinder 是資料綁定的核心類,它内部的屬性可以分為兩類,一類是資料綁定的配置,另一類用于資料綁定,了解這些屬性後我們就能大概知道它内部的機制。

資料綁定配置有關的字段如下。

public class DataBinder implements PropertyEditorRegistry, TypeConverter {

	// 是否忽略未知字段
	private boolean ignoreUnknownFields = true;

	// 是否忽略無效字段,如這些字段的值在目标對象中不可通路(如嵌套路徑中的字段對應值為空)
	private boolean ignoreInvalidFields = false;

	// 是否自動增長包含空值的嵌套路徑
	private boolean autoGrowNestedPaths = true;

	// 自動增長的數組或集合的大小的限制
	private int autoGrowCollectionLimit = DEFAULT_AUTO_GROW_COLLECTION_LIMIT;

	// 綁定字段白名單
	private String[] allowedFields;

	// 綁定字段黑名單
	private String[] disallowedFields;

	// 必須綁定的字段
	private String[] requiredFields;
}
           

資料綁定功能實作的字段如下。

public class DataBinder implements PropertyEditorRegistry, TypeConverter {

	// 需要進行資料綁定的目标對象
	private final Object target;
	// 需要進行資料綁定的目标對象名稱
	private final String objectName;

	// 資料綁定結果
	private AbstractPropertyBindingResult bindingResult;

	// 類型轉換
	private SimpleTypeConverter typeConverter;
	// 類型轉換服務
	private ConversionService conversionService;

	// 将錯誤碼轉換為消息編碼的解析器
	private MessageCodesResolver messageCodesResolver;
	// 錯誤處理器,用于處理字段缺失和進行異常轉換
	private BindingErrorProcessor bindingErrorProcessor = new DefaultBindingErrorProcessor();
	// 參數校驗器
	private final List<Validator> validators = new ArrayList<>();
}
           

資料綁定功能實作的字段又分為幾大類:資料綁定的目标對象、負責類型轉換的轉換器、負責參數校驗的校驗器、儲存參數校驗結果的 BindingResult。

DataBinder 進行資料綁定時,需要将屬性值轉換為目标對象屬性的類型,還會把校驗結果存至 BindingResult。由于 DataBinder 實作了接口 PropertyEditorRegistry、TypeConverter,還會将實作委托到内部的 typeConverter。

至于校驗器則用于額外的校驗方法使用,在上篇《Spring 參數校驗最佳實踐及原了解析》有進行分析 web 請求參數的校驗會調用 DataBinder 的校驗方法,感興趣可以自行閱讀。

也就是說 DataBinder 兼具對象屬性設定、類型轉換、校驗的能力。

#bind

方法實作代碼如下。

public void bind(PropertyValues pvs) {
		MutablePropertyValues mpvs = (pvs instanceof MutablePropertyValues ?
				(MutablePropertyValues) pvs : new MutablePropertyValues(pvs));
		doBind(mpvs);
	}
           

這裡對屬性進行了簡單處理後就進入了真正資料綁定的方法。

protected void doBind(MutablePropertyValues mpvs) {
		checkAllowedFields(mpvs);
		checkRequiredFields(mpvs);
		applyPropertyValues(mpvs);
	}
           

資料綁定時會對屬性必要的校驗和處理。讓我們把重點放到屬性的設定。

protected void applyPropertyValues(MutablePropertyValues mpvs) {
		try {
			// Bind request parameters onto target object.
			getPropertyAccessor().setPropertyValues(mpvs, isIgnoreUnknownFields(), isIgnoreInvalidFields());
		} catch (PropertyBatchUpdateException ex) {
			// Use bind error processor to create FieldErrors.
			for (PropertyAccessException pae : ex.getPropertyAccessExceptions()) {
				getBindingErrorProcessor().processPropertyAccessException(pae, getInternalBindingResult());
			}
		}
	}
           

屬性設定直接調用 PropertyAccessor 設定屬性的方法,遇到異常後則使用錯誤處理器處理。那麼 PropertyAccessor 是從哪裡來的呢?繼續跟蹤代碼。

protected ConfigurablePropertyAccessor getPropertyAccessor() {
		return getInternalBindingResult().getPropertyAccessor();
	}
           

這裡可以看出,PropertyAccessor 是從内部的 BindingResult 擷取到的,繼續跟蹤代碼則會發現這裡 BindingResult 的實作是 BeanPropertyBindingResult,至于 PropertyAccessor 的實作使用的則是 BeanWrapper。也是說真正進行資料綁定的實作由 BeanWrapper 完成,這裡不再具體分析。

Spring XML 配置中屬性設定分析

我們已經對 Spring 資料綁定的能力有了一定的了解,那 Spring 是怎麼運用這項能力的呢?這裡進行一些簡單分析,先看 Spring 如何将 XML 中的屬性配置設定到 bean 的屬性上的。

資料綁定中,屬性使用 PropertyValues 表示,Spring 會将 xml 中的屬性配置轉換為 PropertyValues 并存儲至表示 bean 中繼資料的 BeanDefinition。核心代碼如下。

public class BeanDefinitionParserDelegate {
	public void parsePropertyElement(Element ele, BeanDefinition bd) {
		String propertyName = ele.getAttribute(NAME_ATTRIBUTE);
		... 省略部分代碼
		try {
			Object val = parsePropertyValue(ele, bd, propertyName);
			PropertyValue pv = new PropertyValue(propertyName, val);
			bd.getPropertyValues().addPropertyValue(pv);
		} finally {
			this.parseState.pop();
		}
	}
}
           

有了 BeanDefinition,Spring 就可以根據其内部的中繼資料執行個體化 bean,并設定 bean 的屬性。核心代碼如下。

public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFactory
		implements AutowireCapableBeanFactory {

	protected void applyPropertyValues(String beanName, BeanDefinition mbd, BeanWrapper bw, PropertyValues pvs) {
		... 省略部分代碼
		try {
			bw.setPropertyValues(new MutablePropertyValues(deepCopy));
		} catch (BeansException ex) {
			throw new BeanCreationException(
					mbd.getResourceDescription(), beanName, "Error setting property values", ex);
		}
	}
}
           

可以看到,bean 屬性的設定和 DataBinder 資料綁定的底層實作一樣,也是委托給 BeanWrapper 設定屬性值,至于這裡的 BeanWrapper,則是 Spring 執行個體化 bean 之後建立的。

WEB 請求參數資料綁定分析

Web 環境下,Spring 需要把請求中的參數或其他參數綁定到 Controller 方法參數值中,這是由 HandlerMethodArgumentResolver 接口來處理的,它可以根據 Controller 方法參數定義解析出參數值。

對于請求到 Model 參數的綁定,實作類為 ModelAttributeMethodProcessor 有關參數綁定的核心代碼如下。

public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler {

	@Override
	@Nullable
	public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
										NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
			... 省略部分代碼
			WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
			if (binder.getTarget() != null) {
				if (!mavContainer.isBindingDisabled(name)) {
					// 資料綁定
					bindRequestParameters(binder, webRequest);
				}
				// 參數校驗
				validateIfApplicable(binder, parameter);
				if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
					throw new BindException(binder.getBindingResult());
				}
			}
			if (!parameter.getParameterType().isInstance(attribute)) {
				// 類型轉換
				attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
			}
			bindingResult = binder.getBindingResult();
			... 省略部分代碼		
	}

	protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) {
		((WebRequestDataBinder) binder).bind(request);
	}
}
           

可以看到,這裡直接調用了

WebRequestDataBinder#bind

方法将 request 轉換為方法參數中的屬性,此外資料綁定後還進行了參數校驗與類型轉換。

總結

Spring 資料綁定的核心類是 DataBinder,其底層使用 BeanWrapper 實作對象屬性值的設定,對于資料綁定,往往又伴随着資料校驗、類型轉換。Spring 中的類型轉換同樣具有多種實作,下篇将進行介紹。