Dive into Spring MVC @ModelAttribute Annotation

In Spring MVC, the @ModelAttribute annotation can be applied to methods or method parameters. The @ModelAttribute method is usually used to fill in some public required attributes or data, such as several preset states of a drop-down list, several types of pets, or to obtain a command object required for HTML form rendering.

A method is marked with @ModelAttribute

The @ModelAttribute marked on the method indicates that the method is used to add one or more attributes to the model. Such a method can accept the same parameter types as the @RequestMapping annotation, but cannot be directly mapped to a specific request.

In the same controller, the method marked with @ModelAttribute will actually be called before the @RequestMapping method.

Example:

// Add one attribute
// The return value of the method is added to the model under the name "account"
// You can customize the name via @ModelAttribute("myAccount")

@ModelAttribute
public Account addAccount(@RequestParam String number) {
    return accountManager.findAccount(number);
}

// Add multiple attributes

@ModelAttribute
public void populateModel(@RequestParam String number, Model model) {
    model.addAttribute(accountManager.findAccount(number));
    // add more ...
}

The @ModelAttribute method is usually used to fill in some public required attributes or data, such as several preset states of a drop-down list, several types of pets, or to obtain a command object required for HTML form rendering, such as Account Wait.

There are two styles of @ModelAttribute annotation methods:

  • In the first way of writing, the method will add an attribute by default by returning a value;
  • In the second way of writing, the method receives a Model object and then can add any number of properties to it.

You can choose one of the two styles according to your needs.

A controller can have multiple @ModelAttribute methods. All these methods in the same controller will be called before the @RequestMapping method.

@ModelAttribute methods can also be defined in classes annotated by @ControllerAdvice, and this @ModelAttribute can take effect on many controllers at the same time.

What about when the property name is not explicitly specified?

In this case, the framework will give a default name based on the property’s type. For example, if the method returns an object of type Account, the default property name is “account”. The default value can be changed by setting the value of the @ModelAttribute annotation. When adding attributes directly to the Model, use the appropriate overloaded method addAttribute(..) – that is, the method with or without the attribute name.

The @ModelAttribute annotation can also be used on @RequestMapping methods. In this case, the return value of the @RequestMapping method will be interpreted as an attribute of the model, rather than a view name. At this time, the view name will be determined by the view naming convention.

Method parameters are marked with @ModelAttribute

The @ModelAttribute annotation can be used on both methods and method parameters.

The @ModelAttribute marked on the method parameter indicates that the value of the method parameter will be obtained from the model. If not found in the model, the parameter will be instantiated first and then added to the model. After it exists in the model, all parameters with matching names in the request will be filled into this parameter.

This is called data binding in Spring MVC, a very useful feature, we don’t have to manually convert these field data from table data every time.

@RequestMapping(path = "/owners/{ownerId}/pets/{petId}/edit", method = RequestMethod.POST)
public String processSubmit(@ModelAttribute Pet pet) { 
...
}

Taking the above code as an example, where might this instance of the Pet type come from? There are several possibilities:

  • It may be because the use of @SessionAttributes annotation already exists in the model
  • It may be due to the use of @ModelAttribute methods in the same controller that already exists in the model – as described in the previous section
  • It may be obtained from URI template variables and type conversions (explained in detail below)
  • It may be instantiated by calling its own default constructor

The @ModelAttribute method is often used to fetch an attribute value from the database, which may be passed in the middle of the request through the @SessionAttributes annotation. In some cases, it is more convenient to use URI template variables and type conversions to get an attribute. Here is an example:

@RequestMapping(path = "/accounts/{account}", method = RequestMethod.PUT)
public String save(@ModelAttribute("account") Account account) {
...
}

In this example, the name of the model attribute (“account”) matches the name of the URI template variable. If you configure a converter Converter<String, Account> that can convert an account value of String type into an instance of Account type, then the above code can work very well without writing an additional @ModelAttribute method.

The next step is data binding. The WebDataBinder class can match request parameters—including string query parameters and form fields—to model attributes by name. Successfully matched fields will undergo a type conversion (from the String type to the target field type) when needed, and then be filled into the corresponding attributes of the model.

After data binding, some errors may occur, such as failure to provide necessary fields, errors in the type conversion process, and so on. If you want to check for these errors, you can declare a BindingResult parameter immediately after the parameter marked with @ModelAttribute:

@RequestMapping(path = "/owners/{ownerId}/pets/{petId}/edit", method = RequestMethod.POST)
public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result) {
    if (result.hasErrors()) {
        return "petForm";
    }
 ...
}

After getting the BindingResult parameter, you can check for errors, and you can use Spring’s <errors> form tag to display error messages on the same form.

BindingResult is used to record errors in the data binding process, so in addition to data binding, you can also pass this object to your own custom validator to call validation. This allows errors during data binding and validation to be collected and returned to the user:

@RequestMapping(path = "/owners/{ownerId}/pets/{petId}/edit", method = RequestMethod.POST)
public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result) {

    new PetValidator().validate(pet, result);
    if (result.hasErrors()) {
        return "petForm";
    }
...
}

Or you can add a @Valid annotation of the JSR-303 specification so that the validator will be called automatically.

@RequestMapping(path = "/owners/{ownerId}/pets/{petId}/edit", method = RequestMethod.POST)
public String processSubmit(@Valid @ModelAttribute("pet") Pet pet, BindingResult result) {

    if (result.hasErrors()) {
        return "petForm";
    }
...
}