【原創】遨遊springmvc之WebDataBinder

1.前言

先上原理圖java

在咱們學習servlet的時候咱們知道有一個方法叫作:request.getParameter("paramName"),它返回的是一個String類型,可是若是一切都是這樣子咱們開發程序的時候就會顯得特別麻煩,由於java引入了對象的概念,咱們每每把一個表單的數據封裝在一個業務中的一個javaBean對象裏面,javaBean對象裏面的屬性會有不一樣類型,如:int,double,byte等等。因此須要幾個東西來把String轉化成服務端真正的類型,爲了解決這個問題,springmvc引入了WebDataBinder。web

WebDataBinder不須要咱們本身去新建,WebDataBinder繼承了spring-context中的DataBinder,DataBinder中定義了屬性編輯器註冊的方法spring

源碼1.1編程

@Override
    //針對某個類型
	public void registerCustomEditor(Class<?> requiredType, PropertyEditor propertyEditor) {
		getPropertyEditorRegistry().registerCustomEditor(requiredType, propertyEditor);
	}

	@Override
    //針對某個屬性
	public void registerCustomEditor(Class<?> requiredType, String field, PropertyEditor propertyEditor) {
		getPropertyEditorRegistry().registerCustomEditor(requiredType, field, propertyEditor);
	}

DataBinder還有個很是重要的做用就是用來作數據驗證(JSR-303驗證框架) 這邊就不作展開了安全


 

spring-bean已經給咱們提供了一些經常使用的屬性編輯器,在org.springframework.beans.propertyeditors包下如圖圖1.1所示:mvc

圖1.1app

 

 

2.WebDataBinder源碼解剖

2.1 源碼解刨

咱們先開看一段普通bean參數解析器ModelAttributeMethodProcessor中的一段代碼,在解析參數的時候,都會由一個工廠類WebDataBinderFactory來建立一個WebDataBinder,而這個工廠類WebDataBinderFactory是從哪裏來的呢?請看遨遊springmvc之HandlerAdapter源碼2.2.3,WebDataBinderFactory在其實現類InitBinderDataBinderFactory中實現了初始化如源碼2.1.2,在        WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);這句代碼執行的時候,它首先會執行controller加了@initBinder的方法(binderMethods),經過for循環咱們知道binderMethods能夠是多個,因此咱們能夠controller加入多個綁定的方法。框架

源碼2.1.1編輯器

@Override
	public final Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {

		String name = ModelFactory.getNameForParameter(parameter);
		Object attribute = (mavContainer.containsAttribute(name) ? mavContainer.getModel().get(name) :
				createAttribute(name, parameter, binderFactory, webRequest));

		if (!mavContainer.isBindingDisabled(name)) {
			ModelAttribute ann = parameter.getParameterAnnotation(ModelAttribute.class);
			if (ann != null && !ann.binding()) {
				mavContainer.setBindingDisabled(name);
			}
		}

		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());
			}
		}

		// Add resolved attribute and BindingResult at the end of the model
		Map<String, Object> bindingResultModel = binder.getBindingResult().getModel();
		mavContainer.removeAttributes(bindingResultModel);
		mavContainer.addAllAttributes(bindingResultModel);

		return binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
	}

源碼2.1.2ide

/**
	 * Initialize a WebDataBinder with {@code @InitBinder} methods.
	 * If the {@code @InitBinder} annotation specifies attributes names, it is
	 * invoked only if the names include the target object name.
	 * @throws Exception if one of the invoked @{@link InitBinder} methods fail.
	 */
    @Override
	public void initBinder(WebDataBinder binder, NativeWebRequest request) throws Exception {
		for (InvocableHandlerMethod binderMethod : this.binderMethods) {
			if (isBinderMethodApplicable(binderMethod, binder)) {
                //@InitBinder的方法必須是void
				Object returnValue = binderMethod.invokeForRequest(request, null, binder);
				if (returnValue != null) {
					throw new IllegalStateException("@InitBinder methods should return void: " + binderMethod);
				}
			}
		}
	}

全部的binder最終都會執行bind方法,它最終將會把綁定出錯的信息也給返回到bindResult也就是咱們的數據驗證

源碼2.1.3

/**
	 * Bind the parameters of the given request to this binder's target,
	 * also binding multipart files in case of a multipart request.
	 * <p>This call can create field errors, representing basic binding
	 * errors like a required field (code "required"), or type mismatch
	 * between value and bean property (code "typeMismatch").
	 * <p>Multipart files are bound via their parameter name, just like normal
	 * HTTP parameters: i.e. "uploadedFile" to an "uploadedFile" bean property,
	 * invoking a "setUploadedFile" setter method.
	 * <p>The type of the target property for a multipart file can be MultipartFile,
	 * byte[], or String. The latter two receive the contents of the uploaded file;
	 * all metadata like original file name, content type, etc are lost in those cases.
	 * @param request request with parameters to bind (can be multipart)
	 * @see org.springframework.web.multipart.MultipartHttpServletRequest
	 * @see org.springframework.web.multipart.MultipartFile
	 * @see #bind(org.springframework.beans.PropertyValues)
	 */
	public void bind(ServletRequest request) {
		MutablePropertyValues mpvs = new ServletRequestParameterPropertyValues(request);
		MultipartRequest multipartRequest = WebUtils.getNativeRequest(request, MultipartRequest.class);
		if (multipartRequest != null) {
			bindMultipart(multipartRequest.getMultiFileMap(), mpvs);
		}
		addBindValues(mpvs, request);
		doBind(mpvs);
	}

 

3.PropertyEditor原理

上面講了一堆關於WebDataBinder的東西,咱們再來學習下WebDataBinder中註冊的屬性編輯器PropertyEditor

PropertyEditor是jdk下java.beans下的用於圖形用戶接口(GUI)的一個接口,spring並非直接使用PropertyEditor而是用PropertyEditorSupport來實現屬性的轉換,由於PropertyEditorSupport已經提供了一些方法的默認實現,而且屏蔽了一些如paintValue的方法。

那麼spring是如何將PropertyEditor嵌入到上線文當中呢?

它是經過PropertyEditorRegistry接口,而PropertyEditorRegistrySupport則實現了PropertyEditorRegistry,而且加入了圖1.1中的一些默認屬性編輯器的支持

private void createDefaultEditors() {
		this.defaultEditors = new HashMap<Class<?>, PropertyEditor>(64);

		// Simple editors, without parameterization capabilities.
		// The JDK does not contain a default editor for any of these target types.
		this.defaultEditors.put(Charset.class, new CharsetEditor());
		this.defaultEditors.put(Class.class, new ClassEditor());
		this.defaultEditors.put(Class[].class, new ClassArrayEditor());
		this.defaultEditors.put(Currency.class, new CurrencyEditor());
		this.defaultEditors.put(File.class, new FileEditor());
		this.defaultEditors.put(InputStream.class, new InputStreamEditor());
		this.defaultEditors.put(InputSource.class, new InputSourceEditor());
		this.defaultEditors.put(Locale.class, new LocaleEditor());
		this.defaultEditors.put(Pattern.class, new PatternEditor());
		this.defaultEditors.put(Properties.class, new PropertiesEditor());
		this.defaultEditors.put(Reader.class, new ReaderEditor());
		this.defaultEditors.put(Resource[].class, new ResourceArrayPropertyEditor());
		this.defaultEditors.put(TimeZone.class, new TimeZoneEditor());
		this.defaultEditors.put(URI.class, new URIEditor());
		this.defaultEditors.put(URL.class, new URLEditor());
		this.defaultEditors.put(UUID.class, new UUIDEditor());
		if (zoneIdClass != null) {
			this.defaultEditors.put(zoneIdClass, new ZoneIdEditor());
		}

		// Default instances of collection editors.
		// Can be overridden by registering custom instances of those as custom editors.
		this.defaultEditors.put(Collection.class, new CustomCollectionEditor(Collection.class));
		this.defaultEditors.put(Set.class, new CustomCollectionEditor(Set.class));
		this.defaultEditors.put(SortedSet.class, new CustomCollectionEditor(SortedSet.class));
		this.defaultEditors.put(List.class, new CustomCollectionEditor(List.class));
		this.defaultEditors.put(SortedMap.class, new CustomMapEditor(SortedMap.class));

		// Default editors for primitive arrays.
		this.defaultEditors.put(byte[].class, new ByteArrayPropertyEditor());
		this.defaultEditors.put(char[].class, new CharArrayPropertyEditor());

		// The JDK does not contain a default editor for char!
		this.defaultEditors.put(char.class, new CharacterEditor(false));
		this.defaultEditors.put(Character.class, new CharacterEditor(true));

		// Spring's CustomBooleanEditor accepts more flag values than the JDK's default editor.
		this.defaultEditors.put(boolean.class, new CustomBooleanEditor(false));
		this.defaultEditors.put(Boolean.class, new CustomBooleanEditor(true));

		// The JDK does not contain default editors for number wrapper types!
		// Override JDK primitive number editors with our own CustomNumberEditor.
		this.defaultEditors.put(byte.class, new CustomNumberEditor(Byte.class, false));
		this.defaultEditors.put(Byte.class, new CustomNumberEditor(Byte.class, true));
		this.defaultEditors.put(short.class, new CustomNumberEditor(Short.class, false));
		this.defaultEditors.put(Short.class, new CustomNumberEditor(Short.class, true));
		this.defaultEditors.put(int.class, new CustomNumberEditor(Integer.class, false));
		this.defaultEditors.put(Integer.class, new CustomNumberEditor(Integer.class, true));
		this.defaultEditors.put(long.class, new CustomNumberEditor(Long.class, false));
		this.defaultEditors.put(Long.class, new CustomNumberEditor(Long.class, true));
		this.defaultEditors.put(float.class, new CustomNumberEditor(Float.class, false));
		this.defaultEditors.put(Float.class, new CustomNumberEditor(Float.class, true));
		this.defaultEditors.put(double.class, new CustomNumberEditor(Double.class, false));
		this.defaultEditors.put(Double.class, new CustomNumberEditor(Double.class, true));
		this.defaultEditors.put(BigDecimal.class, new CustomNumberEditor(BigDecimal.class, true));
		this.defaultEditors.put(BigInteger.class, new CustomNumberEditor(BigInteger.class, true));

		// Only register config value editors if explicitly requested.
		if (this.configValueEditorsActive) {
			StringArrayPropertyEditor sae = new StringArrayPropertyEditor();
			this.defaultEditors.put(String[].class, sae);
			this.defaultEditors.put(short[].class, sae);
			this.defaultEditors.put(int[].class, sae);
			this.defaultEditors.put(long[].class, sae);
		}
	}

 

通常狀況下PropertyEditorRegistry是由它的子類BeanWrapperImpl或者DataBinder來實現

BeanWrapperImpl實現了javaBean規範中的setter和getter方法,並且它也擁有了父類PropertyEditorSupport中的屬性編輯器

setter、getter方法能夠以下實現如:

Person p = new Person();
        BeanWrapperImpl bw = new BeanWrapperImpl(p);
        bw.setPropertyValue("name","ws");
        System.out.println(p.getName());//ws

除了一些基礎的屬性編輯器,咱們有時還須要加入一些特製的屬性編輯器。spring提供一個類將第三方的屬性編輯器專門注入到spring上下文,它就是CustomEditorConfigurer。

customEditors是一個Map,key是Class,而value就是PropertyEditor

@Override
	public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
		if (this.propertyEditorRegistrars != null) {
			for (PropertyEditorRegistrar propertyEditorRegistrar : this.propertyEditorRegistrars) {
				beanFactory.addPropertyEditorRegistrar(propertyEditorRegistrar);
			}
		}
		if (this.customEditors != null) {
			for (Map.Entry<Class<?>, Class<? extends PropertyEditor>> entry : this.customEditors.entrySet()) {
				Class<?> requiredType = entry.getKey();
				Class<? extends PropertyEditor> propertyEditorClass = entry.getValue();
				beanFactory.registerCustomEditor(requiredType, propertyEditorClass);
			}
		}
	}

因此咱們能夠經過配置注入自定義屬性編輯器

<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
        <property name="customEditors">
            <map>
                <entry key="com.kings.template.entity.Telephone" value="com.kings.template.mvc.propeditor.TelephonePropertyEditor"/>
            </map>
        </property>
    </bean>

以上配置是普通spring項目使用PropertyEditor的例子,那麼Springmvc是如何實現的呢?

還記得上面的WebDataBinder嗎?它是DataBind的子類,而DataBind則是PropertyEditorRegistry的子類,因此WebDataBinder具有註冊屬性編輯器的特性。

springmvc利用WebDataBinder註冊屬性編輯器,在咱們控制器中加入以下代碼,就能使該控制器得到屬性轉化的功能

@InitBinder
    public void initBinder(WebDataBinder binder){
        binder.registerCustomEditor(Telephone.class,new TelephonePropertyEditor());
    }

可是上面的方法只能在當前的控制器下起做用

有時候咱們但願有一個全局的屬性轉換器,那麼咱們就須要用到一個叫WebBindingInitializer的接口,而且在實現方法initBinder中加入自定義的PropertyEditor

public class MyWebBindingInitializer implements WebBindingInitializer {
    
    @Override
    public void initBinder(WebDataBinder binder, WebRequest request) {
        binder.registerCustomEditor(Telephone.class,new TelephonePropertyEditor());
    }
}

 

加入了自定義的WebBindingInitializer咱們須要配置來啓用它

<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
    <property name="webBindingInitializer">
        <bean class="com.kings.template.mvc.MyWebBindingInitializer"/>
    </property>
</bean>

可是請注意

若是你配置了<mvc:annotation-driven/>,那麼上面的配置必須放在<mvc:annotation-driven/>前

若是你不用<mvc:annotation-driven/>,那麼能夠這麼配置

<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping"/>
    <bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
        <property name="webBindingInitializer">
            <bean class="com.kings.template.mvc.MyWebBindingInitializer"/>
        </property>
    </bean>

 

4.實現自定義PropertyEditor

PropertyEditor接口方法以下

public interface PropertyEditor {

    void setValue(Object value);

    Object getValue();

    boolean isPaintable();

    void paintValue(java.awt.Graphics gfx, java.awt.Rectangle box);

    String getJavaInitializationString();

    String getAsText();

    void setAsText(String text) throws java.lang.IllegalArgumentException;

    String[] getTags();

    java.awt.Component getCustomEditor();

    boolean supportsCustomEditor();

    void addPropertyChangeListener(PropertyChangeListener listener);

    void removePropertyChangeListener(PropertyChangeListener listener);
}

咱們發如今PropertyEditor中有一大堆方法,可是咱們不須要"",由於咱們只須要繼承PropertyEditorSupport,而後主要關注如下幾個方法

void setValue(Object value);//設置屬性值
Object getValue();//獲取屬性值
String getAsText(); //把屬性值轉換成String
void setAsText(String text);//把String轉換成屬性值

 

來一個簡單的例子

實體類

加入了一個叫Telephone的類

@Data
public class Person {
    private String name;
    
    private Telephone telephone;
}

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Telephone {
    
    private String areaCode;
    
    private String phoneNumber;
    
    @Override
    public String toString() {
        return areaCode+"-"+phoneNumber;
    }
}

屬性編輯器

public class TelephonePropertyEditor extends PropertyEditorSupport{
    
    @Override
    public void setAsText(String text) throws IllegalArgumentException {
        if(text.matches("\\d{3,4}-\\d{7,8}")){
            String[] telephoneArray = text.split("-");
            setValue(new Telephone(telephoneArray[0],telephoneArray[1]));
        } else {
            throw new IllegalArgumentException("錯誤的電話號碼");
        }
    }
    
    @Override
    public String getAsText() {
        return ((Telephone)getValue()).toString();
    }
}

控制器

TelephonePropertyEditor將url上telephone=010-12345678的參數轉化成Telephone實體

//註冊屬性編輯器
    @InitBinder
    public void initBinder(WebDataBinder binder){
        binder.registerCustomEditor(Telephone.class,new TelephonePropertyEditor());
    }   
 
    @RequestMapping (value="/bind/1",method= RequestMethod.GET)
    @ResponseBody
    public String detail(Person p) {
        return p.getTelephone().toString();
    }

 

5.總結

花了這麼多文字修飾了PropertyEditor,結果它是spring3以前的玩意,好尷尬!竟然把缺點放在最後,讓大家看完了一整篇!整的想噴垃圾話

由於它有以下缺點:

(一、PropertyEditor被設計爲只能String<——>Object之間轉換,不能任意對象類型<——>任意類型,如咱們常見的Long時間戳到Date類型的轉換是辦不到的;

(二、PropertyEditor是線程不安全的,也就是有狀態的,所以每次使用時都須要建立一個,不可重用;

(三、PropertyEditor不是強類型的,setValue(Object)能夠接受任意類型,所以須要咱們本身判斷類型是否兼容;

(四、須要本身編程實現驗證,Spring3支持更棒的註解驗證支持;

(五、在使用SpEL表達式語言或DataBinder時,只能進行String<--->Object之間的類型轉換;

(六、不支持細粒度的類型轉換/格式化,如UserModel的registerDate須要轉換/格式化相似「2012-05-01」的數據,而OrderModel的orderDate須要轉換/格式化相似「2012-05-01 15:11:13」的數據,由於你們都爲java.util.Date類型,所以不太容易進行細粒度轉換/格式化

spring3以後出了更高級的類型換換系統Converter。

 

發現一個機智的導航😳

相關文章
相關標籤/搜索