Sometimes you run into something that makes you think twice. While programming a Spring MVC controller I wanted to have an optional path parameter. It is possible just not as obvious as you would expect.
Let’s assume we have a controller that serves persons based on a substring and all persons if you specify no substring at all. A straightforward way of implementing the controller could be something like:
@RestController
@AllArgsConstructor
public class PersonController {
private PersonRepository personRepository;
@RequestMapping(path = "/persons/{substring}")
public List<Person> getMatchingPersons(@PathVariable(name = "substring") String substring) {
return personRepository.getPersons()
.filter(p -> p.getName().toLowerCase().contains(substring.toLowerCase()))
.collect(Collectors.toList());
}
@RequestMapping(path = "/persons")
public List<Person> getPersons() {
return personRepository.getPersons().collect(Collectors.toList());
}
In taking a closer look I noticed the /persons
mapping is practically the same as /persons/{substring}
. The only difference is the path variable {substring}
which is present or not. The @PathVariable
annotation has the option to make the path variable optional using @PathVariable(required=false)
. This makes the path argument optional, but consequently the substring
method parameter gets assigned null
, which leads to less readable code due to a null check now needed to not break the code.
@RestController
@AllArgsConstructor
public class PersonController {
private PersonRepository personRepository;
@RequestMapping(path = {"/persons/{substring}", "/persons"})
public List<Person> getPersons(@PathVariable(name = "substring", required = false) String substring) {
final String sub = substring == null ? "" : substring;
return personRepository.getPersons()
.filter(p -> p.getName().toLowerCase().contains(sub.toLowerCase()))
.collect(Collectors.toList());
}
}
In this case it is not too ugly as the default is really simple, but it remains added clutter. Combining the Optional
introduced in Java 8 and the fact Spring MVC from version 4 and up allows for path variable binding to an Optional
, I came up with a little bit more elegant solution.
@RestController
@AllArgsConstructor
public class PersonController {
private PersonRepository personRepository;
@RequestMapping(path = {"/persons/{substring}", "/persons"})
public List<Person> getPersons(@PathVariable(name = "substring", required = false) Optional<String> substring) {
return personRepository.getPersons()
.filter(p -> p.getName().toLowerCase().contains(substring.orElse("").toLowerCase()))
.collect(Collectors.toList());
}
}
This way I effectively created an optional path variable with a default. It boiled down to changing the path variable mapping to @PathVariable(name = “substring”, required = false)
, updating the request mapping to @RequestMapping(path = {“/persons/{substring}”, “/persons”})
and adding a sensible default using the Optional.orElse(T other)
method. If you need a not so straighforward default you can use the Optional.orElseGet(Supplier<? extends T> other)
method to produce it for you.
Combining the path mappings makes the code in my opinion cleaner. I have to write less request mapping methods. Not everybody likes using Optional as a method parameter, but I guess that is a matter of taste. I personally find writing less code preferable over avoiding the use of optionals.