1. Introduction
In this tutorial, we consider a new feature of Spring MVC that allows us to specify the web requests using usual Java interfaces.
2. Overview
Usually, when defining a controller in Spring MVC, we decorate its methods with various annotations that specify the request: the URL of the endpoint, the HTTP request method, the path variables, and so on.
We can, for example, introduce the /save/{id} endpoint using said annotations on an otherwise plain method:
@PostMapping("/save/{id}") @ResponseBody public Book save(@RequestBody Book book, @PathVariable int id) { // implementation }
Naturally, this is not a problem at all when we have just one controller that handles the requests. The situation changes a bit when we have various controllers with the same method signatures.
For example, we might have two different versions of the controller – due to migration or similar – that have the same method signatures. In that case, we’d have a considerable amount of duplicated annotations that accompany the method definitions. Obviously, it would violate the DRY (don’t repeat yourself) principle.
If this situation would take place for pure Java classes, we’d simply define an interface and make the classes implement this interface. In the controllers, the main burden on the methods is not due to the method signatures, but due to the method annotations.
Spring 5.1, though, introduced a new feature:
Controller parameter annotations get detected on interfaces as well: Allowing for complete mapping contracts in controller interfaces.
Let’s investigate how we can use this feature.
3. Controller’s Interface
3.1. Context Setup
We illustrate the new feature by using an example of a very simple REST application that manages books. It’ll consist of just one controller with methods that allow us to retrieve and modify the books.
In the tutorial, we concentrate only on the issues related to the feature. All implementation issues of the application can be found in our GitHub repository.
3.2. Interface
Let’s define a usual Java interface in which we define not only the signatures of the methods but also the type of web requests they are supposed to handle:
@RequestMapping("/default") public interface BookOperations { @GetMapping("/") List<Book> getAll(); @GetMapping("/{id}") Optional<Book> getById(@PathVariable int id); @PostMapping("/save/{id}") public void save(@RequestBody Book book, @PathVariable int id); }
Notice that we may have a class-level annotation as well as method-level ones. Now, we can create a controller that implements this interface:
@RestController @RequestMapping("/book") public class BookController implements BookOperations { @Override public List<Book> getAll() {...} @Override public Optional<Book> getById(int id) {...} @Override public void save(Book book, int id) {...} }
We should still add the class-level annotation @RestController or @Controller to our controller. Defined in this way, the controller inherits all annotations related to the mapping the web requests.
In order to check that the controller now works as expected, let’s run the application and hit the getAll() method by making the corresponding request:
curl http://localhost:8080/book/
Even though the controller implements the interface, we may further fine-tune it by adding web request annotations. We may do that in a way as we did it for the interface: either at the class level or at the method level. In fact, we’ve used this possibility when defining the controller:
@RequestMapping("/book") public class BookController implements BookOperations {...}
If we add web request annotations to the controller, they’ll take precedence over the interface’s ones. In other words, Spring interprets the controller interfaces in a way similar to how Java deals with inheritance.
We define all common web request properties in the interface, but in the controller, we may always fine-tune them.
3.3. Caution Note
When we have an interface and various controllers that implement it, we may end up with a situation when a web request may be handled by more than one method. Naturally, Spring will throw an exception:
Caused by: java.lang.IllegalStateException: Ambiguous mapping.
If we decorate the controller with @RequestMapping, we may reduce the risk of ambiguous mappings.
4. Conclusion
In this tutorial, we’ve considered a new feature introduced in Spring 5.1. Now, when Spring MVC controllers implement an interface, they do this not only in the standard Java way but also inherit all web request related functionality defined in the interface.
As always, we may find the corresponding code snippets on our GitHub repository.