I want to understand the flow of Spring processing request parameters

I sometimes wondered what happened to the process of binding the request parameter using @ModelAttribute to the object, so I will summarize the result. It is often not mentioned in the documentation, and the results of reading the source code are summarized.

You can also get request parameters with @RequestParam, but this time it is out of scope.

environment

Where @ModelAttribute can be used

--Method --Handler method arguments

Actually, it is not necessary to add @ModelAttribute to the argument of the Handler method. In the default behavior, if you specify a class that is judged as false in BeanUtils.isSimpleProperty in the argument of the Handler method, the behavior is the same as when @ ModelAttribute is given. Specifically, other than primitive types and their wrappers, ʻEnum, String, CharSequence, Number, Date, ʻURI, ʻURL, Locale, Class, arrays, etc. class of. However, it has the lowest priority, so if another HandlerMethodArgumentResolver` can resolve the argument, it will be resolved there.

ModelAttributeMethodProcessor

In Spring, HandlerAdapter calls the Handler method. There are several implementation classes, but if you are using @ RequestMapiing, RequestMappingHandlerAdapter is responsible for calling the Handler method. It is this class that resolves the value passed to the argument of the Handler method and handles the return value, and it is nice to specify Model or RedirectAttributes as the argument of the Handler method. Is working hard.

So, it is the role of the implementation class of HandlerMethodArgumentResolver to actually resolve the value passed to the argument, and when @ModelAttribute is specified, ModelAttributeMethodProcessor is processing.

Behavior when @ModelAttribute is given to the method

This was not the main subject, but I will also summarize the operation of the method with @ModelAttribute.

The process of storing the return value of the method in Model is performed before executing the Handler method. (Strictly speaking, it is ModelMap in ModelAndViewContainer, but it seems to be confusing, so I will call it Model.) For example, suppose you create the following Controller.

@Controller
public class HelloController {}
    @ModelAttribute
    public User setUp() {
        return new User();
    }

    @GetMapping("/")
    public String index() {
        return "hello";
    }
}

The behavior at that time is as follows. (Strictly speaking, it's just an image.)

@Controller
public class HelloController {
    @GetMapping("/")
    public String index(Model model) {
        model.addAttribute(new User());
        return "hello";
    }
}

When the attribute name is specified in @ModelAttriubte as shown below,

@ModelAttribute("hoge")
public User setUp() {
    return new User();
}

The image added to the first argument of model.addAttribute ().

@GetMapping("/")
public String index(Model model) {
    model.addAttribute("hoge", new User());
    return "index";
}

By the way, it is the class called ModelFactory that calls the method with @ModelAttribute.

Behavior when @ModelAttribute is added to the argument of the Handler method

The main subject is from here.

Get and create objects

First, get the specified object from Model. If value or name is specified for @ModelAttribute, the key for obtaining from Model can be specified. If omitted, Spring is automatically determined from the class name.

There are several patterns that can be obtained from Model, such as storing in Model with the method with @ModelAttribute mentioned above, using Flash scope at the time of redirect, or using @SessionAttribute to set the Session scope. If you are using it, it may be stored in Model before executing the Handler method (rather, before resolving the value passed to the argument).

If the object cannot be obtained from Model, create the object. If there is one constructor, it will be used, if there are multiple constructors, the default constructor will be used, but if there is no default constructor, it will die. When using a constructor with arguments, the process of binding request parameters is inserted, which will be described later.

Request parameter binding

Bind the value of request parameter to the acquired or generated object. A value is bound to the field of the object that matches the name of the request parameter.

Overwrites the value of the object, but does nothing for the value not included in the request parameter.

@Controller
public class HelloController {
    @ModelAttribute
    public User setUp() {
        User user = new User();
        user.setName("Name");
        user.setEmail("Email");
        return user;
    }

    @GetMapping("/")
    public String index(User user) {
        return "hello";
    }
}

Suppose ʻUser has fields name and ʻemail. Then, as shown below, only name is added to the request parameter and sent.

curl http://localhost:8080?name=hogehoge

The name of the ʻUser object becomes hogehoge, and ʻemail becomes mail, which is not overwritten by null.

When creating an object with a constructor with parameters

When creating an object using a constructor with parameters, try binding data at that timing. In this case, bind the request parameter that matches the value specified in the constructor parameter name or @ConstructorProperties instead of the field name.

public class User {
    private String name;
    private String email;

    public User(String n, String e) {
        this.name = n;
        this.email = e;
    }

    public String getName() {
        return name;
    }

    public String getEmail() {
        return email;
    }
}

In the case of the above constructor, the request parameter name must be n or ʻe`.

public class User {
    private String name;
    private String email;

    @ConstructorProperties({"name", "email"})
    public User(String n, String e) {
        this.name = n;
        this.email = e;
    }

    public String getName() {
        return name;
    }

    public String getEmail() {
        return email;
    }
}

If you use @ConstructorProperties as above, the request parameter name can be name or ʻemail. If there is @ConstructorProperties, it will be prioritized, so it will not work with n or ʻe.

However, after the object creation is completed, the process of binding the request parameters as usual is executed, so if there is a setter or if you are using DataBinder that allows direct field access, there. The result of is given priority.

For example, if you create the following class

public class User {
    private String name;
    private String email;

    public User(String name, String email) {
        this.name = name + "hoge";
        this.email = email + "fuga";
    }

    public String getName() {
        return name;
    }

    public String getEmail() {
        return email;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setEmail(String email) {
        this.email = email;
    }
}

In the constructor, hoge and fuga are given and held in the field, but since setter is executed after that, hoge and fuga are given to the value to be finally bound. It has not been.

Suppress request parameter binding

By changing binding of @ ModelAttribute to false, it is possible to suppress the binding of request parameters.

@Controller
public class HelloController {
    @ModelAttribute
    public User setUp() {
        User user = new User();
        user.setName("Name");
        user.setEmail("Email");
        return user;
    }

    @GetMapping("/")
    public String index(@ModelAttribute(binding = false) User user) {
        return "hello";
    }
}

Even if the following request is sent in the above state, the name of the object of ʻUser remains name and ʻemail remains mail.

curl http://localhost:8080?name=hogehoge&email=fugafuga

Validation

If validation is required, such as when @Validated is added to the argument, it is executed.

As a result of validation, if there is an error and there is no ʻErrors in the argument immediately after the Handler method, BindingExceptionis thrown, and if there is, the result is held asBindingResult. (ʻErrors is the parent interface of BindingResult)

By the way, when setting BindingResult as an argument in the Handler method, you have to be careful in order because of the implementation here.

The story goes awry, but if you set the Handler method argument to BindingResult, a class called ʻErrorsMethodArgumentResolverresolves the argument value. This class checks if the last element stored inModel is BindingResult, and if so, sets it as an argument. Therefore, it may not work properly unless it is executed immediately after performing validation and storing BindingResult in Model. Therefore, it seems that the argument of the Handler method must have BindingResult immediately after the argument with @Validated`.

After investigating this, I thought that it is possible to realize code that divides the object that binds the request parameter into two and obtains the validation result of each as follows. The validation result of ʻUser is stored in result1, and the validation result of ʻOtherObj is stored in result2.

@Controller
public class HelloController {
    @GetMapping("/")
    public String index(@Validated User user, BindingResult result1, @Validated OtherObj other, BindingResult result2) {

        return "index";
    }
}

When using a constructor with parameters

When using a constructor with parameters, type conversion is performed as needed at the timing of object creation. At that time, if a field that cannot be type-converted is included, the result is retained as BindingResult, but subsequent binding processing and validation are not performed. In other words, request parameter binding processing by setter and field check by Bean Validation are not performed.

public class User {
    @NotEmpty
    private String name;
    private Integer age;

    public User(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    //getter omitted
}

@Controller
public class HelloController {
    @GetMapping("/")
    public String index(@Validated User user, BindingResult result) {
        return "index";
    }
}

When you create a class like the one above, send the following request.

curl http://localhost:8080?age=hogehoge

The originally expected validation result is that name is empty and ʻage cannot be converted to ʻInteger, but only the latter can be detected. Both can be detected by removing the parameterized constructor and changing it to use the default constructor and setter.

Store in Model

Store the object created by the above process and BindingResult in Model. If the attribute name is specified in @ModelAttribute, that value is used as the key.

at the end

I see. When using a constructor with parameters, you have to be careful because the operation may be slightly different.

Recommended Posts

I want to understand the flow of Spring processing request parameters
I want to control the default error message of Spring Boot
I want to output the day of the week
I want to var_dump the contents of the intent
[Spring Boot] I investigated how to implement post-processing of the received request.
05. I tried to stub the source of Spring Boot
I tried to reduce the capacity of Spring Boot
I want to know the answer of the rock-paper-scissors app
I want to display the name of the poster of the comment
Control the processing flow of Spring Batch with JavaConfig.
I want to see the contents of Request without saying four or five
I want to be aware of the contents of variables!
I want to return the scroll position of UITableView!
I want to expand the clickable part of the link_to method
I want to change the log output settings of UtilLoggingJdbcLogger
I want to narrow down the display of docker ps
[Ruby] I want to reverse the order of the hash table
I want to temporarily disable the swipe gesture of UIPageViewController
The story of Collectors.groupingBy that I want to keep for posterity
I want to limit the input by narrowing the range of numbers
I examined the concept of the process to understand how Docker works
I want to change the value of Attribute in Selenium of Ruby
I investigated the internal processing of Retrofit
I want to know the Method of the Controller where the Exception was thrown in the ExceptionHandler of Spring Boot
I want to display the number of orders for today using datetime.
I want to display images with REST Controller of Java and Spring!
I want to know the JSP of the open portlet when developing Liferay
[Active Admin] I want to customize the default create and update processing
[Ruby] I want to extract only the value of the hash and only the key
Confirmation and refactoring of the flow from request to controller in [httpclient]
I examined the flow of TCP communication with Spring Integration (client edition)
I examined the flow of TCP communication with Spring Integration (server edition)
I want to pass the argument of Annotation and the argument of the calling method to aspect
I want to get the field name of the [Java] field. (Old tale tone)
[Rails] Articles for beginners to organize and understand the flow of form_with
I want you to use Enum # name () for the Key of SharedPreference
The story of raising Spring Boot 1.5 series to 2.1 series
I want to truncate after the decimal point
I want to perform aggregation processing with spring-batch
I want to get the value in Ruby
[RxSwift] I want to deepen my understanding by following the definition of Observable
Part 2: Understand (roughly) the process flow of OAuth 2.0 Login supported by Spring Security 5
I want to get the value of Cell transparently regardless of CellType (Apache POI)
I want to control the start / stop of servers and databases with Alexa
03. I sent a request from Spring Boot to the zip code search API
Part 3: Understand (deeply) the process flow of OAuth 2.0 Login supported by Spring Security 5
I want to separate the handling of call results according to the API caller (call trigger)
I want to recursively get the superclass and interface of a certain class
[Ruby] I want to make a program that displays today's day of the week!
Rails The concept of view componentization of Rails that I want to convey to those who want to quit
I want to call a method of another class
[JavaScript] I want to limit the processing by checking the input items when the specified time elapses at the time of focus out.
[Java] I want to calculate the difference from the date
I want to embed any TraceId in the log
I tried to check the operation of http request (Put) with Talented API Tester
I want to get the information of the class that inherits the UserDetails class of the user who is logged in with Spring Boot.
I tried to summarize the state transition of docker
Understand while reading the article! Summary of contents that Elasticsearch beginners want to suppress
Understand the characteristics of Scala in 5 minutes (Introduction to Scala)
I want to judge the range using the monthly degree
I want to dark mode with the SWT app