Spring Web Reactive | 1. Spring WebFlux | 1.4. Annotated Controllers #2
1.4.4. Model
You can use the @ModelAttribute annotation.
It can create or access objects in the model and bind them to a request through WebDataBinder as method arguments for @RequestMapping methods.
As a method-level annotation in a @Controller or @ControllerAdvice class, it can initialize the model before @RequestMapping methods are called.
It can also expose return values from @RequestMapping methods as model attributes.
This section describes @ModelAttribute methods, or the second item in the previous list. A controller can have any number of @ModelAttribute methods. All such methods are invoked before @RequestMapping methods in the same controller. @ModelAttribute methods can also be shared across controllers through @ControllerAdvice. See the controller advice section for details.
@ModelAttribute methods support flexible method signatures. They support many of the same arguments as @RequestMapping methods, except for @ModelAttribute itself and request body related arguments.
The following example uses an @ModelAttribute method.
Java
@ModelAttribute
public void populateModel(@RequestParam String number, Model model) {
model.addAttribute(accountRepository.findAccount(number));
// add more ...
}
Kotlin
@ModelAttribute
fun populateModel(@RequestParam number: String, model: Model) {
model.addAttribute(accountRepository.findAccount(number))
// add more ...
}
The following example adds only one attribute.
Java
@ModelAttribute
public Account addAccount(@RequestParam String number) {
return accountRepository.findAccount(number);
}
Kotlin
@ModelAttribute
fun populateModel(@RequestParam number: String, model: Model) {
model.addAttribute(accountRepository.findAccount(number))
// add more ...
}
If a name is not explicitly specified, a default name is selected based on the type according to the Javadoc for
Conventions. You can always specify an explicit name by using the overloadedaddAttributemethod or thenameattribute of@ModelAttributefor return values.
Unlike Spring MVC, Spring WebFlux explicitly supports reactive types in the model, such as Mono<Account> or io.reactivex.Single<Account>. As shown in the following example, if an @ModelAttribute argument is declared without a wrapper, the corresponding asynchronous model attribute can be transparently resolved to its actual value, and the model can be updated, at the time the @RequestMapping method is invoked.
Java
@ModelAttribute
public void addAccount(@RequestParam String number) {
Mono<Account> accountMono = accountRepository.findAccount(number);
model.addAttribute("account", accountMono);
}
@PostMapping("/accounts")
public String handle(@ModelAttribute Account account, BindingResult errors) {
// ...
}
Kotlin
import org.springframework.ui.set
@ModelAttribute
fun addAccount(@RequestParam number: String) {
val accountMono: Mono<Account> = accountRepository.findAccount(number)
model["account"] = accountMono
}
@PostMapping("/accounts")
fun handle(@ModelAttribute account: Account, errors: BindingResult): String {
// ...
}
Model attributes with reactive type wrappers are also resolved to their actual values immediately before view rendering, and the model is updated.
@ModelAttribute can also be used as a method-level annotation on an @RequestMapping method. In that case, the return value of the @RequestMapping method is interpreted as a model attribute. This is generally unnecessary because it is the default behavior for HTML controllers, except when a String return value is interpreted as a view name. @ModelAttribute is also useful for customizing the model attribute name, as shown in the following example.
Java
@GetMapping("/accounts/{id}")
@ModelAttribute("myAccount")
public Account handle() {
// ...
return account;
}
Kotlin
@GetMapping("/accounts/{id}")
@ModelAttribute("myAccount")
fun handle(): Account {
// ...
return account
}
1.4.5. DataBinder
Web MVC
@Controller or @ControllerAdvice classes can have @InitBinder methods to initialize WebDataBinder instances. They are used for the following purposes.
Bind request parameters, such as form data or query parameters, to model objects.
Convert String-based request values, such as request parameters, path variables, headers, and cookies, to target types for controller method arguments.
Format model object values as String values when rendering HTML forms.
@InitBinder methods can register controller-specific java.beans.PropertyEditor instances or Spring Converter and Formatter components. You can also use WebFlux Java configuration to register Converter and Formatter types with the globally shared FormattingConversionService.
@InitBinder methods support many of the same arguments as @RequestMapping methods, except for @ModelAttribute command object arguments. They are typically declared with a WebDataBinder argument and a void return value. The following example uses the @InitBinder annotation.
Java
@Controller
public class FormController {
@InitBinder // (1)
public void initBinder(WebDataBinder binder) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
dateFormat.setLenient(false);
binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
}
// ...
}
Kotlin
@Controller
class FormController {
@InitBinder // (1)
fun initBinder(binder: WebDataBinder) {
val dateFormat = SimpleDateFormat("yyyy-MM-dd")
dateFormat.isLenient = false
binder.registerCustomEditor(Date::class.java, CustomDateEditor(dateFormat, false))
}
// ...
}
- (1) Use the
@InitBinderannotation.
Alternatively, when using Formatter-based setup through a shared FormattingConversionService, you can reuse the same approach to register controller-specific Formatter instances, as shown in the following example.
Java
@Controller
public class FormController {
@InitBinder
protected void initBinder(WebDataBinder binder) {
binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd")); // (1)
}
// ...
}
Kotlin
@Controller
class FormController {
@InitBinder
fun initBinder(binder: WebDataBinder) {
binder.addCustomFormatter(DateFormatter("yyyy-MM-dd")) // (1)
}
// ...
}
- (1) Add a custom formatter,
DateFormatterin this case.
1.4.6. Exception Handling
@Controller and @ControllerAdvice classes can contain @ExceptionHandler methods that handle exceptions from controller methods. The following example includes such a handler method.
Java
@Controller
public class SimpleController {
// ...
@ExceptionHandler // (1)
public ResponseEntity<String> handle(IOException ex) {
// ...
}
}
Kotlin
@Controller
class SimpleController {
// ...
@ExceptionHandler // (1)
fun handle(ex: IOException): ResponseEntity<String> {
// ...
}
}
- (1) Declare
@ExceptionHandler.
Exceptions match either the top-level exception being propagated, for example a direct IOException, or the direct cause of a top-level wrapper exception, for example an IOException wrapped in an IllegalStateException.
To match an exception type, it is preferable to declare the target exception as a method argument as in the previous example. Alternatively, the annotation declaration can narrow the matching exception types. In general, it is recommended to make the argument signature as specific as possible and declare a general root exception mapping on a prioritized @ControllerAdvice in that order. See the MVC section for details.
@ExceptionHandler methods in WebFlux support the same method arguments and return values as @RequestMapping methods, except for request body and @ModelAttribute related method arguments.
Spring’s support for the @ExceptionHandler method style in WebFlux is provided by the HandlerAdapter for @RequestMapping methods. See DispatcherHandler for details.
Exceptions for REST APIs
A common requirement for REST services is to include error information in the response body. The Spring Framework does not do this automatically because the representation of error details in the response body is application-specific. However, a @RestController can use an @ExceptionHandler method with a ResponseEntity return value to set the status and body of the response. Such a method can be declared in a @ControllerAdvice class to apply it globally.
Note that Spring WebFlux has no equivalent of Spring MVC’s
ResponseEntityExceptionHandler. This is because WebFlux only needs to createResponseStatusExceptioninstances, or subclasses, and translate them to HTTP status codes.
1.4.7. Controller Advice
Typically, @ExceptionHandler, @InitBinder, and @ModelAttribute methods apply to the @Controller class, or class hierarchy, where they are declared. If you want to apply the same methods more globally across controllers, declare them in a class annotated with @ControllerAdvice or @RestControllerAdvice.
@ControllerAdvice is annotated with @Component. This means these classes can be registered as Spring beans through component scanning. @RestControllerAdvice is a composed annotation annotated with both @ControllerAdvice and @ResponseBody. This essentially means that @ExceptionHandler methods render the response body through message conversion, rather than through view resolver handling or template rendering.
At startup, the infrastructure classes for @RequestMapping and @ExceptionHandler methods detect Spring beans annotated with @ControllerAdvice and apply their methods at runtime. Global @ExceptionHandler methods from @ControllerAdvice are applied after local methods from @Controller. By contrast, global @ModelAttribute and @InitBinder methods are applied before local methods.
By default, @ControllerAdvice methods apply to every request, that is, every controller. You can narrow the scope to a subset of controllers by using annotation attributes, as shown in the following example.
Java
// Target all Controllers annotated with @RestController
@ControllerAdvice(annotations = RestController.class)
public class ExampleAdvice1 {}
// Target all Controllers within specific packages
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}
// Target all Controllers assignable to specific classes
@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
public class ExampleAdvice3 {}
Kotlin
// Target all Controllers annotated with @RestController
@ControllerAdvice(annotations = [RestController::class])
public class ExampleAdvice1 {}
// Target all Controllers within specific packages
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}
// Target all Controllers assignable to specific classes
@ControllerAdvice(assignableTypes = [ControllerInterface::class, AbstractController::class])
public class ExampleAdvice3 {}
The selectors in the previous examples are evaluated at runtime and can negatively affect performance if used broadly. See @ControllerAdvice for details.