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.
@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.
@ModelAttribute
is given to the methodThis 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
.
@ModelAttribute
is added to the argument of the Handler methodThe main subject is from here.
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.
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 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.
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
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 as
BindingResult. (ʻ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 in
Model 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, 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 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.
I see. When using a constructor with parameters, you have to be careful because the operation may be slightly different.
Recommended Posts