Master the use of @InitBinder from a theoretical level [Enjoy Spring MVC]

Posted by Langridge on Wed, 11 Sep 2019 18:54:16 +0200

Every sentence

King Zhang Yining: Daughter, you can play with this heap of gold medals, but my silver can't be played for you.If you want to play silver, go to your Uncle Wang Hao. He has a lot of silver.

Preface

In order to describe the most complex data binding section of Spring MVC, I have done enough before. This part of the knowledge here leaves a learning entrance for small partners, so you can have a look if you are interested: Talk about data binding in Spring --- WebDataBinder, ServletRequestDataBinder, WebBindingInitializer... [Enjoy Spring]

The @InitBinder annotation was introduced after Spring 2.5 for data binding, setting up data converters, etc., literally meaning "Initialize Binder".

With regard to the concept of data binders, the previous lesson focused on the details, where the default companion is familiar~

In the web project of Spring MVC, I believe that small partners often encounter some tricky issues in front-end to back-end value transfer: for example, the most classic problems:

  • How does the front end of a Date type (or LocalDate type) pass?Can the backend be received with the Date type?
  • String type, how can I ensure that there are no spaces at both ends of the value passed in the previous paragraph?(99.99% of the extra space is wood useful)

Looking at this article, you can gracefully solve these seemingly difficult questions.

---

Note: There are two common solutions for Date type delivery in the industry:

  1. Use timestamp
  2. Using String Strings (a universal scheme for passing values)

The user always feels less elegant and less object-oriented in both ways.So here's a piece of black technology: using @InitBinder to easily bind data of all kinds of data types (we Java is a strongly typed language and object-oriented, is it too low if you use strings for everything?)

>Normal string, int, long automatically binds to parameters, but custom format spring does not know how to bind. So inherit PropertyEditorSupport, implement your own property editor PropertyEditor, bind to WebDataBinder (binder.registerCustomEditor), and override the method AssetText

@InitBinder principle

This article starts with principles and then with cases so that you can get a thorough understanding of the use of this note.

1. When did @InitBinder take effect?
This is the foreshadowing pen of the previous article: Spring uses WebDataBinder for data conversion when binding request parameters to HandlerMethod (in this case, RequestParamMethodArgumentResolver):

// The parent of RequestParamMethodArgumentResolver is it, and the resolveArgument method is on the parent
// Subclasses simply need to implement the abstract method resolveName, which is to take a value from the request based on the name
AbstractNamedValueMethodArgumentResolver: 

    @Override
    @Nullable
    public final Object resolveArgument( ... ) {
        ...
        Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest);
        ...
        if (binderFactory != null) {
            // Create a WebDataBinder
            WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name);
            // Complete data conversion (such as String to Date, String to Date, etc.)
            arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter);
            ...
        }
        ...
        return arg;
    }

It takes value from a request by requesting: request.getParameterValues(name).

2. The data binding factories used by the web environment are:ServletRequestDataBinderFactory
Although mentioned in the previous lessons, it is necessary to go through this again briefly for consistency:

// @since 3.1 org.springframework.web.bind.support.DefaultDataBinderFactory 
public class DefaultDataBinderFactory implements WebDataBinderFactory {

    @Override
    @SuppressWarnings("deprecation")
    public final WebDataBinder createBinder(NativeWebRequest webRequest, @Nullable Object target, String objectName) throws Exception {
        WebDataBinder dataBinder = createBinderInstance(target, objectName, webRequest);
        
        // WebBindingInitializer initializer parses here for global effect
        if (this.initializer != null) {
            this.initializer.initBinder(dataBinder, webRequest);
        }
        // Resolves the @InitBinder annotation, which is a protected empty method and is given to a subclass to replicate the implementation
        // InitBinderDataBinderFactory replicates it
        initBinder(dataBinder, webRequest);
        return dataBinder;
    }
}

public class InitBinderDataBinderFactory extends DefaultDataBinderFactory {
    // Save all,
    private final List<InvocableHandlerMethod> binderMethods;
    ...
    @Override
    public void initBinder(WebDataBinder dataBinder, NativeWebRequest request) throws Exception {
        for (InvocableHandlerMethod binderMethod : this.binderMethods) {
            if (isBinderMethodApplicable(binderMethod, dataBinder)) {
                // invokeForRequest is a no-fuss method, just like calling a normal controller method
                // Method parameters can also be written in a variety of formats ~~~
                Object returnValue = binderMethod.invokeForRequest(request, null, dataBinder);
            
                // The @InitBinder annotated method must return void
                if (returnValue != null) {
                    throw new IllegalStateException("@InitBinder methods must not return a value (should be void): " + binderMethod);
                }
            }
        }
    }

    // dataBinder.getObjectName() finally works here by matching by this name
    // That is, you can make the @InitBinder annotation work only on data bindings with specified participating names ~~~
    // This ObjectName of the dataBinder, on the other hand, is usually the name of the entry (the value specified in the comment ~~)

    // The parameter name is dataBinder, so here's a simple filter ~~~~~
    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()));
    }
}

The WebBindingInitializer interface mode takes precedence over the @InitBinder annotation mode (API mode is globally, annotation mode may not be necessary, so it is more flexible)

The subclass ServletRequestDataBinderFactory does one thing: new ExtendedServletRequestDataBinder(target, objectName)
ExtendedServletRequestDataBinder does just one thing: it handles the path variable.

The binderMethods are constructors that represent all the methods associated with this request labeled @InitBinder, so you need to know how its instances were created, and that's the next step.

3. Creation of ServletRequestDataBinderFactory
Any request comes in and is ultimately handled by the HandlerAdapter.handle() method, which is created as follows:

public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean {
    ...
    @Override
    protected ModelAndView handleInternal(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
        ...
        // Processing requests is ultimately a way to execute the controller and get a ModelAndView
        mav = invokeHandlerMethod(request, response, handlerMethod);
        ...
    }
    
    // The way to execute the controller is complex.But in this article, I'm only interested in creating a WebDataBinderFactory, the first sentence of which is
    @Nullable
    protected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
        WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
        ...
    }

    // Create a WebDataBinderFactory 
    // Global methods first (execute first) and then execute the class's own
    private WebDataBinderFactory getDataBinderFactory(HandlerMethod handlerMethod) throws Exception {
        // handlerType: The class in which the method resides (the class in which the controller method resides, also known as xxController)
        // Thus, the scope of this note is at the class level.This will be used as the key to cache
        Class<?> handlerType = handlerMethod.getBeanType();
        Set<Method> methods = this.initBinderCache.get(handlerType);
        if (methods == null) { // Cache missed, go to selectMethods to find all methods labeled @InitBinder~~~
            methods = MethodIntrospector.selectMethods(handlerType, INIT_BINDER_METHODS);
            this.initBinderCache.put(handlerType, methods); // Cache
        }
        
        // Note here: Methods are eventually packaged as InvocableHandlerMethod, which gives them the ability to execute
        List<InvocableHandlerMethod> initBinderMethods = new ArrayList<>();
        
        // Looked up above for this class, now start to see that there is @InitBinder in the global
        // Global methods first (put in the global first, then personalized ~~~so small details: Covered effect yo~~)
        // initBinderAdviceCache is a cached LinkedHashMap (ordered oh~~), which caches classes that work globally.
        // For example, @ControllerAdvice, note the distinction between `RequestBodyAdvice', `ResponseBodyAdvice`

        // methodSet: Indicates that within a class you can define more than N methods labeled @InitBinder~~~
        this.initBinderAdviceCache.forEach((clazz, methodSet) -> {
            
            // Simply put, `RestControllerAdvice'can specify properties such as basePackages to see if this class can be scanned ~~~
            if (clazz.isApplicableToBeanType(handlerType)) {
            
                // This resolveBean() is a bit interesting: it holds a Bean that will get a Bean () if it is a BeanName
                // Most of the time it's BeanName, which is ~~when initializing @ControllerAdvice
                Object bean = clazz.resolveBean();
                for (Method method : methodSet) {
                    // createInitBinderMethod: Adapt Method to an executable InvocableHandlerMethod
                    
                    // The feature is that the HandlerMethodArgumentResolverComposite of this class is passed in
                    // Of course, there are DataBinderFactory and ParameterNameDiscoverer, and so on.
                    initBinderMethods.add(createInitBinderMethod(bean, method));
                }
            }
        });
        // Next step: Conditionally label the method with @InitBinder
        for (Method method : methods) {
            Object bean = handlerMethod.getBean();
            initBinderMethods.add(createInitBinderMethod(bean, method));
        }

        // protected method, just one line of code: new ServletRequestDataBinderFactory(binderMethods, getWebBindingInitializer())
        return createDataBinderFactory(initBinderMethods);
    }
    ...
}

At this point, the whole @InitBinder parsing process is fully understood.I would like to say the following about this process:

  • For binderMethods, a new one (with the first penalty) is created each time a request comes in, either from the global (Advice) or from the Controller class
  • It will not be overwritten if the method name on Controller is the same as the method name labeled on Advice (because the classes are different)
  • About the execution of a method annotated with @InitBinder, which is similar to the execution controller method in that it calls the InvocableHandlerMethod#invokeForRequest method, so you can draw your own analogy

    The core of current method execution is nothing more than the parsing and encapsulation of parameters, that is, the understanding of HandlerMethodArgumentResolver.It is highly recommended that you refer to it This series All articles~

With the support of these basic theories, the next step is, of course, to use Demo Show.

Use cases for @InitBinder

I throw out two requirements with @InitBinder:

  1. Request all strings in trim once
  2. yyyy-MM-dd is a format where strings can be received directly in the Date type (it is not elegant to receive strings before converting them yourself)

To fulfill these two requirements, I need to first customize two property editors:

1,StringTrimmerEditor

public class StringTrimmerEditor extends PropertyEditorSupport {

    // Represents the property object as a string so that the external property editor can visually display it.Returns null by default, indicating that the property cannot be represented as a string
    //@Override
    //public String getAsText() {
    //    Object value = getValue();
    //    return (value != null ? value.toString() : null);
    //}

    // Updates the internal value of an attribute with a string typically passed in from the external property editor
    // Handle incoming requests: test is the value you passed in (not super.getValue() oh~)
    @Override
    public void setAsText(String text) throws IllegalArgumentException {
        text = text == null ? text : text.trim();
        setValue(text);
    }
}

Description: Spring has built-in org.springframework.beans.propertyeditors.StringTrimmerEditor, which is not assembled by default and can be used directly if you need it (I'll use it here for demonstration).What are Spring's built-in registrations?Refer to the PropertyEditorRegistrySupport#createDefaultEditors method
Spring's property editor is different from traditional property editors for IDE development. They do not have a UI interface and are responsible for converting text configuration values in the configuration file to the corresponding values of the Bean property, so Spring's property editor is not a JavaBean property editor in the traditional sense.

2,CustomDateEditor
For this property editor, you can do it yourself, just like I do.This article provides a direct use of Spring, see: org.springframework.beans.propertyeditors.CustomDateEditor

// @since 28.04.2003
// @see java.util.Date
public class CustomDateEditor extends PropertyEditorSupport {
    ...
    @Override
    public void setAsText(@Nullable String text) throws IllegalArgumentException {
        ...
        setValue(this.dateFormat.parse(text));
        ...
    }
    ...
    @Override
    public String getAsText() {
        Date value = (Date) getValue();
        return (value != null ? this.dateFormat.format(value) : "");
    }
}

Once defined, how do I use it?There are two ways:

  1. API approach WebBindingInitializer, see Here This article is brief.
    1. The property editor that overrides the initBinder registration is a global property editor that works for all ontrollers (global)
  2. @InitBinder comment mode

Use @InitBinder on the Controller class as follows:

@Controller
@RequestMapping
public class HelloController {

    @InitBinder
    public void initBinder(WebDataBinder binder) {
        //binder.setDisallowedFields("name"); //Unbound name attribute
        binder.registerCustomEditor(String.class, new StringTrimmerEditor());

        // Use Spring's built-in CustomDateEditor here
        DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, true));
    }

    @ResponseBody
    @GetMapping("/test/initbinder")
    public String testInitBinder(String param, Date date) {
        return param + ":" + date;
    }
}

Request: /test/initbinder? Param= ds&date=2019-12-12.The result is ds:Thu Dec 12 00:00:00 CST 2019, which is expected.

Note that if date returns a value of ds: null for null (because I set it to allow null)
But if you're not in yyyy-MM-dd format, you're throwing it wrong (formatting exception)

The @InitBinder method for this example only works with the current Controller.For global effectiveness, use @ControllerAdvice/WebBindingInitializer.
The @ControllerAdvice allows you to place global configurations for controllers in the same place. Methods annotated with the @ControllerAdvice class can be annotated on methods using @ExceptionHandler, @InitBinder, @ModelAttribute, etc., which is valid for all methods within controllers annotated with @RequestMapping (about global)In this way, I suggest you practice ~).

Role of @InitBinder's value attribute

Get it you may not know, it also has a value attribute, and it is an array

public @interface InitBinder {
    // On which model key does the method used to qualify secondary annotation labels work
    String[] value() default {};
}

Speaker: If a value value is specified, then only the method parameter name (or model name) matches the annotation method will be executed (if not specified).

@Controller
@RequestMapping
public class HelloController {

    @InitBinder({"param", "user"})
    public void initBinder(WebDataBinder binder, HttpServletRequest request) {
        System.out.println("current key: " + binder.getObjectName());
    }

    @ResponseBody
    @GetMapping("/test/initbinder")
    public String testInitBinder(String param, String date,
                                 @ModelAttribute("user") User user, @ModelAttribute("person") Person person) {
        return param + ":" + date;
    }
}

Request: /test/initbinder?Param=fsx&date=2019&user.name=demoUser, console print:

Current key:param
 Current key:user

The effect of the value attribute is clearly seen in the printed results~

It should be noted that although there is a key here that is user.name, the User object will not be encapsulated to this value (because the request.getParameter('user') does not have this key ~).How to solve???Need to bind prefix, principle can refer to Here

Other scenarios

The scenarios illustrated above are the most common scenarios for this note, which you must master.It also has some legendary techniques to digest with a little partner who has the power:

If you submit two Model data at a time, they have duplicate properties.Examples are as follows:

@Controller
@RequestMapping
public class HelloController {

    @Getter
    @Setter
    @ToString
    public static class User {
        private String id;
        private String name;
    }

    @Getter
    @Setter
    @ToString
    public static class Addr {
        private String id;
        private String name;
    }

    @InitBinder("user")
    public void initBinderUser(WebDataBinder binder) {
        binder.setFieldDefaultPrefix("user.");
    }

    @InitBinder("addr")
    public void initBinderAddr(WebDataBinder binder) {
        binder.setFieldDefaultPrefix("addr.");
    }

    @ResponseBody
    @GetMapping("/test/initbinder")
    public String testInitBinder(@ModelAttribute("user") User user, @ModelAttribute("addr") Addr addr) {
        return user + ":" + addr;
    }
}

Request: /test/initbinder? User.id=1&user.name=demoUser&addr.id=10&addr.name=Beijing Haidian District, the result is: HelloController.User(id=1, name=demoUser):HelloController.Addr(id=10, name=Beijing Haidian District)

As to why prefixed binds, here is a brief description:
1. The ModelAttributeMethodProcessor#resolveArgument relies on the attribute = createAttribute(name, parameter, binderFactory, webRequest) method to encapsulate and transform data.
2. createAttribute first requests.getParameter (attributeName) to see if there is a value in the request domain (null in this case), and then reflects back to the resolveArgument method to create an empty instance.
3. Continue to use the WebDataBinder to complete data value binding for this empty object, at which point these FieldDefaultPrefix es will come into play.The method of execution is: bindRequestParameters(binder, webRequest), which is actually ((WebRequestDataBinder) binder).bind(request);.Not unfamiliar with the principles of the bind method~
4. After the encapsulation of Model data is completed, the @Valid check is carried out.

Reference parsing class: ModelAttributeMethodProcessor's handling of the parameter part

summary

This article summarizes the use of the @InitBinder comment from a theoretical point of view. Although this comment does not appear to be very popular in the current environment, I still expect my little buddies to understand it, especially in the scenarios I've illustrated in this article.

Finally, I summarize the notes below for your reference:

  1. The @InitBinder labeled method is executed multiple times, once per request (first penalty)
  2. All @InitBinder s in the Controller instance are valid only for the current Controller
  3. The @InitBinder value property controls the key in the Model model, not the method name (not all effects are represented by writing)
  4. The @InitBinder labeled method cannot have a return value (only void or returnValue=null)
  5. @InitBinder is invalid for @RequestBody, a message converter-based request parameter
    1. Because @InitBinder is used to initialize DataBinder data binding, type conversion, and so on, while @RequestBody does its data parsing and message converter during conversion, it will not work even if you customize the property editor (its WebDataBinder is only used for data validation, not for data binding and summing)Data conversion.Its data binding conversion, if json, is usually handed over to jackson)
  6. Only AbstractNamedValueMethodArgumentResolver will call binder.convertIfNecessary for data conversion so that the property editor will take effect

==If you are interested in source analysis such as Spring, SpringBoot, MyBatis, you can add me wx:fsx641385712 and invite you to join the group manually==
==If you are interested in source analysis such as Spring, SpringBoot, MyBatis, you can add me wx:fsx641385712 and invite you to join the group manually==

Topics: Java Spring Attribute less