1. 程式人生 > >最全面闡述WebDataBinder理解Spring的資料繫結

最全面闡述WebDataBinder理解Spring的資料繫結

每篇一句

不要總問低階的問題,這樣的人要麼懶,不願意上網搜尋,要麼笨,一點獨立思考的能力都沒有

相關閱讀

【小家Spring】聊聊Spring中的資料繫結 --- DataBinder本尊(原始碼分析)

【小家Spring】聊聊Spring中的資料繫結 --- 屬性訪問器PropertyAccessor和實現類DirectFieldAccessor的使用

【小家Spring】聊聊Spring中的資料繫結 --- BeanWrapper以及Java內省Introspector和PropertyDescriptor


對Spring感興趣可掃碼加入wx群:`Java高工、架構師3群`(文末有二維碼)

前言

上篇文章聊了DataBinder,這篇文章繼續聊聊實際應用中的資料繫結主菜:WebDataBinder

在上文的基礎上,我們先來看看DataBinder它的繼承樹:

從繼承樹中可以看到,web環境統一對資料繫結DataBinder進行了增強。

畢竟資料繫結的實際應用場景:不誇張的說99%情況都是web環境~

WebDataBinder

它的作用就是從web request裡(注意:這裡指的web請求,並不一定就是ServletRequest請求喲~)把web請求的parameters繫結到JavaBean上~

Controller方法的引數型別可以是基本型別,也可以是封裝後的普通Java型別。若這個普通Java型別沒有宣告任何註解,則意味著它的每一個屬性

都需要到Request中去查詢對應的請求引數。

// @since 1.2
public class WebDataBinder extends DataBinder {

    // 此欄位意思是:欄位標記  比如name -> _name
    // 這對於HTML複選框和選擇選項特別有用。
    public static final String DEFAULT_FIELD_MARKER_PREFIX = "_";
    // !符號是處理預設值的,提供一個預設值代替空值~~~
    public static final String DEFAULT_FIELD_DEFAULT_PREFIX = "!";
    
    @Nullable
    private String fieldMarkerPrefix = DEFAULT_FIELD_MARKER_PREFIX;
    @Nullable
    private String fieldDefaultPrefix = DEFAULT_FIELD_DEFAULT_PREFIX;
    // 預設也會繫結空的檔案流~
    private boolean bindEmptyMultipartFiles = true;

    // 完全沿用父類的兩個構造~~~
    public WebDataBinder(@Nullable Object target) {
        super(target);
    }
    public WebDataBinder(@Nullable Object target, String objectName) {
        super(target, objectName);
    }

    ... //  省略get/set
    // 在父類的基礎上,增加了對_和!的處理~~~
    @Override
    protected void doBind(MutablePropertyValues mpvs) {
        checkFieldDefaults(mpvs);
        checkFieldMarkers(mpvs);
        super.doBind(mpvs);
    }

    protected void checkFieldDefaults(MutablePropertyValues mpvs) {
        String fieldDefaultPrefix = getFieldDefaultPrefix();
        if (fieldDefaultPrefix != null) {
            PropertyValue[] pvArray = mpvs.getPropertyValues();
            for (PropertyValue pv : pvArray) {

                // 若你給定的PropertyValue的屬性名確實是以!打頭的  那就做處理如下:
                // 如果JavaBean的該屬性可寫 && mpvs不存在去掉!後的同名屬性,那就新增進來表示後續可以使用了(畢竟是預設值,沒有精確匹配的高的)
                // 然後把帶!的給移除掉(因為預設值以已經轉正了~~~)
                // 其實這裡就是說你可以使用!來給個預設值。比如!name表示若找不到name這個屬性的時,就取它的值~~~
                // 也就是說你request裡若有穿!name保底,也就不怕出現null值啦~
                if (pv.getName().startsWith(fieldDefaultPrefix)) {
                    String field = pv.getName().substring(fieldDefaultPrefix.length());
                    if (getPropertyAccessor().isWritableProperty(field) && !mpvs.contains(field)) {
                        mpvs.add(field, pv.getValue());
                    }
                    mpvs.removePropertyValue(pv);
                }
            }
        }
    }

    // 處理_的步驟
    // 若傳入的欄位以_打頭
    // JavaBean的這個屬性可寫 && mpvs木有去掉_後的屬性名字
    // getEmptyValue(field, fieldType)就是根據Type型別給定預設值。
    // 比如Boolean型別預設給false,陣列給空陣列[],集合給空集合,Map給空map  可以參考此類:CollectionFactory
    // 當然,這一切都是建立在你傳的屬性值是以_打頭的基礎上的,Spring才會預設幫你處理這些預設值
    protected void checkFieldMarkers(MutablePropertyValues mpvs) {
        String fieldMarkerPrefix = getFieldMarkerPrefix();
        if (fieldMarkerPrefix != null) {
            PropertyValue[] pvArray = mpvs.getPropertyValues();
            for (PropertyValue pv : pvArray) {
                if (pv.getName().startsWith(fieldMarkerPrefix)) {
                    String field = pv.getName().substring(fieldMarkerPrefix.length());
                    if (getPropertyAccessor().isWritableProperty(field) && !mpvs.contains(field)) {
                        Class<?> fieldType = getPropertyAccessor().getPropertyType(field);
                        mpvs.add(field, getEmptyValue(field, fieldType));
                    }
                    mpvs.removePropertyValue(pv);
                }
            }
        }
    }

    // @since 5.0
    @Nullable
    public Object getEmptyValue(Class<?> fieldType) {
        try {
            if (boolean.class == fieldType || Boolean.class == fieldType) {
                // Special handling of boolean property.
                return Boolean.FALSE;
            } else if (fieldType.isArray()) {
                // Special handling of array property.
                return Array.newInstance(fieldType.getComponentType(), 0);
            } else if (Collection.class.isAssignableFrom(fieldType)) {
                return CollectionFactory.createCollection(fieldType, 0);
            } else if (Map.class.isAssignableFrom(fieldType)) {
                return CollectionFactory.createMap(fieldType, 0);
            }
        } catch (IllegalArgumentException ex) {
            if (logger.isDebugEnabled()) {
                logger.debug("Failed to create default value - falling back to null: " + ex.getMessage());
            }
        }
        // 若不在這幾大類型內,就返回預設值null唄~~~
        // 但需要說明的是,若你是簡單型別比如int,
        // Default value: null. 
        return null;
    }

    // 單獨提供的方法,用於繫結org.springframework.web.multipart.MultipartFile型別的資料到JavaBean屬性上~
    // 顯然預設是允許MultipartFile作為Bean一個屬性  參與繫結的
    // Map<String, List<MultipartFile>>它的key,一般來說就是檔案們啦~
    protected void bindMultipart(Map<String, List<MultipartFile>> multipartFiles, MutablePropertyValues mpvs) {
        multipartFiles.forEach((key, values) -> {
            if (values.size() == 1) {
                MultipartFile value = values.get(0);
                if (isBindEmptyMultipartFiles() || !value.isEmpty()) {
                    mpvs.add(key, value);
                }
            }
            else {
                mpvs.add(key, values);
            }
        });
    }
}

單從WebDataBinder來說,它對父類進行了增強,提供的增強能力如下:

  1. 支援對屬性名以_打頭的預設值處理(自動擋,能夠自動處理所有的Bool、Collection、Map等)
  2. 支援對屬性名以!打頭的預設值處理(手動檔,需要手動給某個屬性賦預設值,自己控制的靈活性很高)
  3. 提供方法,支援把MultipartFile繫結到JavaBean的屬性上~

Demo示例

下面以一個示例來演示使用它增強的這些功能:

@Getter
@Setter
@ToString
public class Person {

    public String name;
    public Integer age;

    // 基本資料型別
    public Boolean flag;
    public int index;
    public List<String> list;
    public Map<String, String> map;

}

演示使用!手動精確控制欄位的預設值:

    public static void main(String[] args) {
        Person person = new Person();
        WebDataBinder binder = new WebDataBinder(person, "person");

        // 設定屬性(此處演示一下預設值)
        MutablePropertyValues pvs = new MutablePropertyValues();

        // 使用!來模擬各個欄位手動指定預設值
        //pvs.add("name", "fsx");
        pvs.add("!name", "不知火舞");
        pvs.add("age", 18);
        pvs.add("!age", 10); // 上面有確切的值了,預設值不會再生效

        binder.bind(pvs);
        System.out.println(person);
    }

列印輸出(符合預期):

Person(name=null, age=null, flag=false, index=0, list=[], map={})

請用此列印結果對比一下上面的結果,你是會有很多發現,比如能夠發現基本型別的預設值就是它自己。
另一個很顯然的道理:若你啥都不做特殊處理,包裝型別預設值那鐵定都是null了~

瞭解了WebDataBinder後,繼續看看它的一個重要子類ServletRequestDataBinder

ServletRequestDataBinder

前面說了這麼多,親有沒有發現還木有聊到過我們最為常見的Web場景API:javax.servlet.ServletRequest。本類從命名上就知道,它就是為此而生。

它的目標就是:data binding from servlet request parameters to JavaBeans, including support for multipart files.從Servlet Request裡把引數繫結到JavaBean裡,支援multipart。

備註:到此類為止就已經把web請求限定為了Servlet Request,和Servlet規範強綁定了。

public class ServletRequestDataBinder extends WebDataBinder {
    ... // 沿用父類構造
    // 注意這個可不是父類的方法,是本類增強的~~~~意思就是kv都從request裡來~~當然內部還是適配成了一個MutablePropertyValues
    public void bind(ServletRequest request) {
        // 內部最核心方法是它:WebUtils.getParametersStartingWith()  把request引數轉換成一個Map
        // request.getParameterNames()
        MutablePropertyValues mpvs = new ServletRequestParameterPropertyValues(request);
        MultipartRequest multipartRequest = WebUtils.getNativeRequest(request, MultipartRequest.class);
    
        // 呼叫父類的bindMultipart方法,把MultipartFile都放進MutablePropertyValues裡去~~~
        if (multipartRequest != null) {
            bindMultipart(multipartRequest.getMultiFileMap(), mpvs);
        }
        // 這個方法是本類流出來的一個擴充套件點~~~子類可以複寫此方法自己往裡繼續新增
        // 比如ExtendedServletRequestDataBinder它就複寫了這個方法,進行了增強(下面會說)  支援到了uriTemplateVariables的繫結
        addBindValues(mpvs, request);
        doBind(mpvs);
    }

    // 這個方法和父類的close方法類似,很少直接呼叫
    public void closeNoCatch() throws ServletRequestBindingException {
        if (getBindingResult().hasErrors()) {
            throw new ServletRequestBindingException("Errors binding onto object '" + getBindingResult().getObjectName() + "'", new BindException(getBindingResult()));
        }
    }
}

下面就以MockHttpServletRequest為例作為Web 請求實體,演示一個使用的小Demo。說明:MockHttpServletRequest它是HttpServletRequest的實現類~

Demo示例

    public static void main(String[] args) {
        Person person = new Person();
        ServletRequestDataBinder binder = new ServletRequestDataBinder(person, "person");

        // 構造引數,此處就不用MutablePropertyValues,以HttpServletRequest的實現類MockHttpServletRequest為例吧
        MockHttpServletRequest request = new MockHttpServletRequest();
        // 模擬請求引數
        request.addParameter("name", "fsx");
        request.addParameter("age", "18");

        // flag不僅僅可以用true/false  用0和1也是可以的?
        request.addParameter("flag", "1");

        // 設定多值的
        request.addParameter("list", "4", "2", "3", "1");
        // 給map賦值(Json串)
        // request.addParameter("map", "{'key1':'value1','key2':'value2'}"); // 這樣可不行
        request.addParameter("map['key1']", "value1");
        request.addParameter("map['key2']", "value2");

        //// 一次性設定多個值(傳入Map)
        //request.setParameters(new HashMap<String, Object>() {{
        //    put("name", "fsx");
        //    put("age", "18");
        //}});

        binder.bind(request);
        System.out.println(person);
    }

列印輸出:

Person(name=fsx, age=18, flag=true, index=0, list=[4, 2, 3, 1], map={key1=value1, key2=value2})

完美。

思考題:小夥伴可以思考為何給Map屬性傳值是如上,而不是value寫個json就行呢?

ExtendedServletRequestDataBinder

此類程式碼不多但也不容小覷,它是對ServletRequestDataBinder的一個增強,它用於把URI template variables引數新增進來用於繫結。它會去從request的HandlerMapping.class.getName() + ".uriTemplateVariables";這個屬性裡查詢到值出來用於繫結~~~

比如我們熟悉的@PathVariable它就和這相關:它負責把引數從url模版中解析出來,然後放在attr上,最後交給ExtendedServletRequestDataBinder進行繫結~~~

介於此:我覺得它還有一個作用,就是定製我們全域性屬性變數用於繫結~

向此屬性放置值的地方是:AbstractUrlHandlerMapping.lookupHandler() --> chain.addInterceptor(new UriTemplateVariablesHandlerInterceptor(uriTemplateVariables)); --> preHandle()方法 -> exposeUriTemplateVariables(this.uriTemplateVariables, request); -> request.setAttribute(URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriTemplateVariables);

// @since 3.1
public class ExtendedServletRequestDataBinder extends ServletRequestDataBinder {
    ... // 沿用父類構造

    //本類的唯一方法
    @Override
    @SuppressWarnings("unchecked")
    protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) {
        // 它的值是:HandlerMapping.class.getName() + ".uriTemplateVariables";
        String attr = HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE;

        // 注意:此處是attr,而不是parameter
        Map<String, String> uriVars = (Map<String, String>) request.getAttribute(attr);
        if (uriVars != null) {
            uriVars.forEach((name, value) -> {
                
                // 若已經存在確切的key了,不會覆蓋~~~~
                if (mpvs.contains(name)) {
                    if (logger.isWarnEnabled()) {
                        logger.warn("Skipping URI variable '" + name + "' because request contains bind value with same name.");
                    }
                } else {
                    mpvs.addPropertyValue(name, value);
                }
            });
        }
    }
}

可見,通過它我們亦可以很方便的做到在每個ServletRequest提供一份共用的模版屬性們,供以繫結~

此類基本都沿用父類的功能,比較簡單,此處就不寫Demo了(Demo請參照父類)~

說明:ServletRequestDataBinder一般不會直接使用,而是使用更強的子類ExtendedServletRequestDataBinder

WebExchangeDataBinder

它是Spring5.0後提供的,對Reactive程式設計的Mono資料繫結提供支援,因此暫略~

data binding from URL query params or form data in the request data to Java objects

MapDataBinder

它位於org.springframework.data.web是和Spring-Data相關,專門用於處理targetMap<String, Object>型別的目標物件的繫結,它並非一個public類~

它用的屬性訪問器是MapPropertyAccessor:一個繼承自AbstractPropertyAccessor的私有靜態內部類~(也支援到了SpEL哦)

WebRequestDataBinder

它是用於處理Spring自己定義的org.springframework.web.context.request.WebRequest的,旨在處理和容器無關的web請求資料繫結,有機會詳述到這塊的時候,再詳細說~


如何註冊自己的PropertyEditor來實現自定義型別資料繫結?

通過前面的分析我們知道了,資料繫結這一塊最終會依託於PropertyEditor來實現具體屬性值的轉換(畢竟request傳進來的都是字串嘛~)

一般來說,像String, int, long會自動繫結到引數都是能夠自動完成繫結的,因為前面有說,預設情況下Spring是給我們註冊了N多個解析器的:

public class PropertyEditorRegistrySupport implements PropertyEditorRegistry {

    @Nullable
    private Map<Class<?>, PropertyEditor> defaultEditors;

    private void createDefaultEditors() {
        this.defaultEditors = new HashMap<>(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());
        ...
        // 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));
        ...
        // 這裡就部全部枚舉出來了
    }
}

雖然預設註冊支援的Editor眾多,但是依舊發現它並沒有對Date型別、以及Jsr310提供的各種事件、日期型別的轉換(當然也包括我們的自定義型別)。
因此我相信小夥伴都遇到過這樣的痛點:Date、LocalDate等型別使用自動繫結老不方便了,並且還經常傻傻搞不清楚。所以最終很多都無奈選擇了語義不是非常清晰的時間戳來傳遞

演示Date型別的資料繫結Demo:

@Getter
@Setter
@ToString
public class Person {

    public String name;
    public Integer age;

    // 以Date型別為示例
    private Date start;
    private Date end;
    private Date endTest;

}

    public static void main(String[] args) {
        Person person = new Person();
        DataBinder binder = new DataBinder(person, "person");

        // 設定屬性
        MutablePropertyValues pvs = new MutablePropertyValues();
        pvs.add("name", "fsx");

        // 事件型別繫結
        pvs.add("start", new Date());
        pvs.add("end", "2019-07-20");
        // 試用試用標準的事件日期字串形式~
        pvs.add("endTest", "Sat Jul 20 11:00:22 CST 2019");


        binder.bind(pvs);
        System.out.println(person);
    }

列印輸出:

Person(name=fsx, age=null, start=Sat Jul 20 11:05:29 CST 2019, end=null, endTest=Sun Jul 21 01:00:22 CST 2019)

結果是符合我預期的:start有值,end沒有,endTest卻有值。
可能小夥伴對start、end都可以理解,最詫異的是endTest為何會有值呢???
此處我簡單解釋一下處理步驟:

  1. BeanWrapper呼叫setPropertyValue()給屬性賦值,傳入的value值都會交給convertForProperty()方法根據get方法的返回值型別進行轉換~(比如此處為Date型別)
  2. 委託給this.typeConverterDelegate.convertIfNecessary進行型別轉換(比如此處為string->Date型別)
  3. this.propertyEditorRegistry.findCustomEditor(requiredType, propertyName);找到一個合適的PropertyEditor(顯然此處我們沒有自定義Custom處理Date的PropertyEditor,返回null)
  4. 回退到使用ConversionService,顯然此處我們也沒有設定,返回null
  5. 回退到使用預設的editor = findDefaultEditor(requiredType);(注意:此處只根據型別去找了,因為上面說了預設不處理了Date,所以也是返回null)
  6. 最終的最終,回退到Spring對Array、Collection、Map的預設值處理問題,最終若是String型別,都會呼叫BeanUtils.instantiateClass(strCtor, convertedValue)也就是有參構造進行初始化~~~(請注意這必須是String型別才有的權利)
    1. 所以本例中,到最後一步就相當於new Date("Sat Jul 20 11:00:22 CST 2019"),因為該字串是標準的時間日期串,所以是闊儀的,也就是endTest是能被正常賦值的~

通過這個簡單的步驟分析,解釋了為何end沒值,endTest有值了。
其實通過回退到的最後一步處理,我們還可以對此做巧妙的應用。比如我給出如下的一個巧用例子:

@Getter
@Setter
@ToString
public class Person {
    private String name;
    // 備註:child是有有一個入參的構造器的
    private Child child;
}

@Getter
@Setter
@ToString
public class Child {
    private String name;
    private Integer age;
    public Child() {
    }
    public Child(String name) {
        this.name = name;
    }
}

    public static void main(String[] args) {
        Person person = new Person();
        DataBinder binder = new DataBinder(person, "person");

        // 設定屬性
        MutablePropertyValues pvs = new MutablePropertyValues();
        pvs.add("name", "fsx");

        // 給child賦值,其實也可以傳一個字串就行了 非常的方便   Spring會自動給我們new物件
        pvs.add("child", "fsx-son");
        
        binder.bind(pvs);
        System.out.println(person);
    }

列印輸出:

Person(name=fsx, child=Child(name=fsx-son, age=null))

完美。


廢話不多說,下面我通過自定義屬性編輯器的手段,來讓能夠支援處理上面我們傳入2019-07-20這種非標準的時間字串。

我們知道DataBinder本身就是個PropertyEditorRegistry,因此我只需要自己註冊一個自定義的PropertyEditor即可:

1、通過繼承PropertyEditorSupport實現一個自己的處理Date的編輯器:

public class MyDatePropertyEditor extends PropertyEditorSupport {

    private static final String PATTERN = "yyyy-MM-dd";

    @Override
    public String getAsText() {
        Date date = (Date) super.getValue();
        return new SimpleDateFormat(PATTERN).format(date);
    }

    @Override
    public void setAsText(String text) throws IllegalArgumentException {
        try {
            super.setValue(new SimpleDateFormat(PATTERN).parse(text));
        } catch (ParseException e) {
            System.out.println("ParseException....................");
        }
    }
}

2、註冊進DataBinder並執行

    public static void main(String[] args) {
        Person person = new Person();
        DataBinder binder = new DataBinder(person, "person");
        binder.registerCustomEditor(Date.class, new MyDatePropertyEditor());
        //binder.registerCustomEditor(Date.class, "end", new MyDatePropertyEditor());

        // 設定屬性
        MutablePropertyValues pvs = new MutablePropertyValues();
        pvs.add("name", "fsx");

        // 事件型別繫結
        pvs.add("start", new Date());
        pvs.add("end", "2019-07-20");
        // 試用試用標準的事件日期字串形式~
        pvs.add("endTest", "Sat Jul 20 11:00:22 CST 2019");


        binder.bind(pvs);
        System.out.println(person);
    }

執行列印如下:

ParseException....................
Person(name=fsx, age=null, start=Sat Jul 20 11:41:49 CST 2019, end=Sat Jul 20 00:00:00 CST 2019, endTest=null)

結果符合預期。不過對此結果我仍舊丟擲如下兩個問題供小夥伴自行思考:
1、輸出了ParseException....................
2、start有值,endTest值卻為null了

理解這塊最後我想說:通過自定義編輯器,我們可以非常自由、高度定製化的完成自定義型別的封裝,可以使得我們的Controller更加容錯、更加智慧、更加簡潔。有興趣的可以運用此塊知識,自行實踐~

WebBindingInitializer和WebDataBinderFactory

WebBindingInitializer

WebBindingInitializer:實現此介面重寫initBinder方法註冊的屬性編輯器是全域性的屬性編輯器,對所有的Controller都有效。

可以簡單粗暴的理解為:WebBindingInitializer為編碼方式,@InitBinder為註解方式(當然註解方式還能控制到只對當前Controller有效,實現更細粒度的控制)

觀察發現,Spring對這個介面的命名很有意思:它用的Binding正在進行時態~

// @since 2.5   Spring在初始化WebDataBinder時候的回撥介面,給呼叫者自定義~
public interface WebBindingInitializer {

    // @since 5.0
    void initBinder(WebDataBinder binder);

    // @deprecated as of 5.0 in favor of {@link #initBinder(WebDataBinder)}
    @Deprecated
    default void initBinder(WebDataBinder binder, WebRequest request) {
        initBinder(binder);
    }

}

此介面它的內建唯一實現類為:ConfigurableWebBindingInitializer,若你自己想要擴充套件,建議繼承它~

public class ConfigurableWebBindingInitializer implements WebBindingInitializer {
    private boolean autoGrowNestedPaths = true;
    private boolean directFieldAccess = false; // 顯然這裡是false

    // 下面這些引數,不就是WebDataBinder那些可以配置的屬性們嗎?
    @Nullable
    private MessageCodesResolver messageCodesResolver;
    @Nullable
    private BindingErrorProcessor bindingErrorProcessor;
    @Nullable
    private Validator validator;
    @Nullable
    private ConversionService conversionService;
    // 此處使用的PropertyEditorRegistrar來管理的,最終都會被註冊進PropertyEditorRegistry嘛
    @Nullable
    private PropertyEditorRegistrar[] propertyEditorRegistrars;

    ... //  省略所有get/set
    
    // 它做的事無非就是把配置的值都放進去而已~~
    @Override
    public void initBinder(WebDataBinder binder) {
        binder.setAutoGrowNestedPaths(this.autoGrowNestedPaths);
        if (this.directFieldAccess) {
            binder.initDirectFieldAccess();
        }
        if (this.messageCodesResolver != null) {
            binder.setMessageCodesResolver(this.messageCodesResolver);
        }
        if (this.bindingErrorProcessor != null) {
            binder.setBindingErrorProcessor(this.bindingErrorProcessor);
        }
        // 可以看到對校驗器這塊  內部還是做了容錯的
        if (this.validator != null && binder.getTarget() != null && this.validator.supports(binder.getTarget().getClass())) {
            binder.setValidator(this.validator);
        }
        if (this.conversionService != null) {
            binder.setConversionService(this.conversionService);
        }
        if (this.propertyEditorRegistrars != null) {
            for (PropertyEditorRegistrar propertyEditorRegistrar : this.propertyEditorRegistrars) {
                propertyEditorRegistrar.registerCustomEditors(binder);
            }
        }
    }
}

此實現類主要是提供了一些可配置項,方便使用。注意:此介面一般不直接使用,而是結合InitBinderDataBinderFactoryWebDataBinderFactory等一起使用~

WebDataBinderFactory

顧名思義它就是來創造一個WebDataBinder的工廠。

// @since 3.1   注意:WebDataBinder 可是1.2就有了~
public interface WebDataBinderFactory {
    // 此處使用的是Spring自己的NativeWebRequest   後面兩個引數就不解釋了
    WebDataBinder createBinder(NativeWebRequest webRequest, @Nullable Object target, String objectName) throws Exception;
}

它的繼承樹如下:

DefaultDataBinderFactory

public class DefaultDataBinderFactory implements WebDataBinderFactory {
    @Nullable
    private final WebBindingInitializer initializer;
    // 注意:這是唯一建構函式
    public DefaultDataBinderFactory(@Nullable WebBindingInitializer initializer) {
        this.initializer = initializer;
    }

    // 實現介面的方法
    @Override
    @SuppressWarnings("deprecation")
    public final WebDataBinder createBinder(NativeWebRequest webRequest, @Nullable Object target, String objectName) throws Exception {

        WebDataBinder dataBinder = createBinderInstance(target, objectName, webRequest);
        
        // 可見WebDataBinder 建立好後,此處就會回撥(只有一個)
        if (this.initializer != null) {
            this.initializer.initBinder(dataBinder, webRequest);
        }
        // 空方法 子類去實現,比如InitBinderDataBinderFactory實現了詞方法
        initBinder(dataBinder, webRequest);
        return dataBinder;
    }

    //  子類可以複寫,預設實現是WebRequestDataBinder
    // 比如子類ServletRequestDataBinderFactory就複寫了,使用的new ExtendedServletRequestDataBinder(target, objectName)
    protected WebDataBinder createBinderInstance(@Nullable Object target, String objectName, NativeWebRequest webRequest) throws Exception 
        return new WebRequestDataBinder(target, objectName);
    }
}

按照Spring一貫的設計,本方法實現了模板動作,子類只需要複寫對應的動作即可達到效果。

InitBinderDataBinderFactory

它繼承自DefaultDataBinderFactory,主要用於處理標註有@InitBinder的方法做初始繫結~

// @since 3.1
public class InitBinderDataBinderFactory extends DefaultDataBinderFactory {
    
    // 需要注意的是:`@InitBinder`可以標註N多個方法~  所以此處是List
    private final List<InvocableHandlerMethod> binderMethods;

    // 此子類的唯一建構函式
    public InitBinderDataBinderFactory(@Nullable List<InvocableHandlerMethod> binderMethods, @Nullable WebBindingInitializer initializer) {
        super(initializer);
        this.binderMethods = (binderMethods != null ? binderMethods : Collections.emptyList());
    }

    // 上面知道此方法的呼叫方法生initializer.initBinder之後
    // 所以使用註解它生效的時機是在直接實現介面的後面的~
    @Override
    public void initBinder(WebDataBinder dataBinder, NativeWebRequest request) throws Exception {
        for (InvocableHandlerMethod binderMethod : this.binderMethods) {
            // 判斷@InitBinder是否對dataBinder持有的target物件生效~~~(根據name來匹配的)
            if (isBinderMethodApplicable(binderMethod, dataBinder)) {
                // 關於目標方法執行這塊,可以參考另外一篇@InitBinder的原理說明~
                Object returnValue = binderMethod.invokeForRequest(request, null, dataBinder);

                // 標註@InitBinder的方法不能有返回值
                if (returnValue != null) {
                    throw new IllegalStateException("@InitBinder methods must not return a value (should be void): " + binderMethod);
                }
            }
        }
    }

    //@InitBinder有個Value值,它是個陣列。它是用來匹配dataBinder.getObjectName()是否匹配的   若匹配上了,現在此註解方法就會生效
    // 若value為空,那就對所有生效~~~
    protected boolean isBinderMethodApplicable(HandlerMethod initBinderMethod, WebDataBinder dataBinder) {
        InitBinder ann = initBinderMethod.getMethodAnnotation(InitBinder.class);
        Assert.state(ann != null, "No InitBinder annotation");
        String[] names = ann.value();
        return (ObjectUtils.isEmpty(names) || ObjectUtils.containsElement(names, dataBinder.getObjectName()));
    }
}
ServletRequestDataBinderFactory

它繼承自InitBinderDataBinderFactory,作用就更明顯了。既能夠處理@InitBinder,而且它使用的是更為強大的資料繫結器:ExtendedServletRequestDataBinder

// @since 3.1
public class ServletRequestDataBinderFactory extends InitBinderDataBinderFactory {
    public ServletRequestDataBinderFactory(@Nullable List<InvocableHandlerMethod> binderMethods, @Nullable WebBindingInitializer initializer) {
        super(binderMethods, initializer);
    }
    @Override
    protected ServletRequestDataBinder createBinderInstance(
            @Nullable Object target, String objectName, NativeWebRequest request) throws Exception  {
        return new ExtendedServletRequestDataBinder(target, objectName);
    }
}

此工廠是RequestMappingHandlerAdapter這個介面卡預設使用的一個數據繫結器工廠,而RequestMappingHandlerAdapter卻又是當下使用得最頻繁、功能最強大的一個介面卡

總結

WebDataBinderSpringMVC中使用,它不需要我們自己去建立,我們只需要向它註冊引數型別對應的屬性編輯器PropertyEditorPropertyEditor可以將字串轉換成其真正的資料型別,它的void setAsText(String text)方法實現資料轉換的過程。

好好掌握這部分內容,這在Spring MVC中結合@InitBinder註解一起使用將有非常大的威力,能一定程度上簡化你的開發,提高效率

知識交流

若文章格式混亂,可點選:原文連結-原文連結-原文連結-原文連結-原文連結

==The last:如果覺得本文對你有幫助,不妨點個讚唄。當然分享到你的朋友圈讓更多小夥伴看到也是被作者本人許可的~==

若對技術內容感興趣可以加入wx群交流:Java高工、架構師3群
若群二維碼失效,請加wx號:fsx641385712(或者掃描下方wx二維碼)。並且備註:"java入群" 字樣,會手動邀請入群

相關推薦

全面闡述WebDataBinder理解Spring資料

每篇一句 不要總問低階的問題,這樣的人要麼懶,不願意上網搜尋,要麼笨,一點獨立思考的能力都沒有 相關閱讀 【小家Spring】聊聊Spring中的資料繫結 --- DataBinder本尊(原始碼分析) 【小家Spring】聊聊Spring中的資料繫結 --- 屬性訪問器PropertyAccessor和

Vue.js實戰學習-Vue.js理解資料

1.Vue.js是什麼?     Vue.js是一個漸進式的javaScript框架,在專案中,可以選擇從不同的維度去使用它。 2.使用的模式:     MVVM模式:Model-View-ViewModel,當View(檢視層)變化時,會自動

如何妙用Spring 資料機制?

前言 在剖析完 「Spring Boot 統一資料格式是怎麼實現的? 」文章之後,一直覺得有必要說明一下 Spring's Data Binding Mechanism 「Spring 資料繫結機制」。 預設情況下,Spring 只知道如何轉換簡單資料型別。比如我們提交的 int、String 或 boole

Spring MVC 資料和表單標籤庫

資料繫結是將使用者輸入繫結到領域模型的一種特性。 資料繫結的好處: 1. 型別總是為 String 的 HTTP 請求引數,可用於填充不同型別的物件屬性。 2. 當輸入驗證失敗時,會重新生成一個 HTML 表單。 為了高效的使用資料繫結,還需要 Spring 的表單標籤庫。表單標籤庫中包含了可以用在

Spring MVC 資料流程分析

1.    資料繫結流程原理★ ①   Spring MVC 主框架將 ServletRequest  物件及目標方法的入參例項傳遞給 WebDataBinderFactory 例項,以建立 DataBinder 例項物件 ②

ko資料,取不到彈出外層html,jquery $("#id",body)逗號分隔的選擇器取到

專案用knockoutjs和requirejs進行資料繫結,做到一個refer彈出層時,在本地是好的,但是生產環境要巢狀到另一個系統的iframe裡,這樣彈出層談到他們iframe最外層,就獲取不到我們自己的html。無法進行資料繫結,所以cto採取了此種方法 var bo

Spring 5 官方文件》5. 驗證、資料和型別轉換

原文連結 譯者:14shadow43 5 驗證、資料繫結和型別轉換 5.1 介紹 JSR-303/JSR-349 Bean Validation 在設定支援方面,Spring Framework 4.0支援Bean Validation 1.0(JSR-303)和Bean Validation

Spring MVC---資料和表單標籤

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transi

理解angular2.0雙向資料中的 $event

app/hero-form.component.html (excerpt) COPY CODE <inputtype="text"class="form-control"id="name

d3筆記(1) - d3元素選擇器及資料理解

以下貼出程式碼,讀者可以通過控制檯輸出體會選擇器和資料繫結。 <!doctype html> <html> <head> <script src="d3.js"></script> </head> <body&

簡要理解vue的mvvm模式中的雙向資料

mvvm(Model-View-ViewModel)模式: 由檢視(View)、檢視模型(ViewModel)、模型(Model)三部分組成,結構如下圖。 通過這三部分實現UI邏輯、呈現邏輯和狀態控制、資料與業務邏輯的分離。 使用MVVM模式有幾大好處

理解$watch ,$apply 和 $digest --- 理解資料過程

注 這篇博文主要是寫給新手的,是給那些剛剛開始接觸Angular,並且想了解資料幫定是如何工作的人。如果你已經對Angular比較瞭解了,那強烈建議你直接去閱讀原始碼。 Angular使用者都想知道資料繫結是怎麼實現的。你可能會看到各種各樣的詞彙:$watch</code>,<code

Vue雙向資料理解

一、什麼是MVVM框架   所謂MVVM,是Model - View - ViewModel 的簡寫。   我的理解是頁面上所看到的就是View。 使用Vue.js或者是其他MVVM框架,是在操作ViewModel,實現View - ViewModel的雙向互動。 然後Mod

spring mvc 中通過controller 傳遞物件給jsp,並且資料,在修改值後回傳物件給controller

在controller 中需要指定 sessionAttribute的key @sessionattributes註解應用到Controller上面,可以將Model中的屬性同步到session當中。 當需要清除session當中的值得時候,我們只需要在

spring mvc 資料 400錯誤

情景:使用在方法中繫結資料的時候,開啟連結,出現400錯誤。 @RequestMapping(value = "editItemSubmit") public String editItemSubmit(int id, Items item) {

vue中關於checkbox資料v-model指令的個人理解

vue.js為開發者提供了很多便利的指令,其中v-model用於表單的資料繫結很常見,下面是最常見的例子:<div id='myApp'>    <input type="text" v-model="msg"><br>    {{msg}

Spring MVC 自定義資料 報http 406錯誤

前臺時間(如2013-08-12 18:10:23)傳到後臺srpingMVC 進行繫結到javaBean的util.date 時會報資料繫結失敗,不能從String 轉換到Date 型別。 現在我寫了一個自定議資料繫結類 package com.ltkj.zhepg.

spring mvc 資料問題 提交表單提示HTTP status 400, The request sent by the client was syntactically incorrect

我們在spring mvc 中controller方法中的引數,spring mvc會自動為我們進行資料繫結。 spring mvc 方法中不一定要全部都有 form表單提交的屬性, 也可以有 請求屬性中 沒有的引數(這時候只會把對應不上的引數設為null),

spring mvc 多個bean,或一個bean多個物件的資料

一、前臺傳遞不同類不同物件 1、屬性名不同,可直接封裝進controller方法的物件引數(經驗證) 2、屬性名有重複,可在重複的類中設定一個值型別,後臺再去將值型別值賦值給例項變數(經驗證) 二、同一類多個物件集合 方法1、Json方式 方法2、新建一個類,該

spring mvc使用@InitBinder 標籤對錶單資料

在SpringMVC中,bean中定義了Date,double等型別,如果沒有做任何處理的話,日期以及double都無法繫結。 解決的辦法就是使用spring mvc提供的@InitBinder標籤 在我的專案中是在BaseController中增加方法initBinde