July 17, 2020
There’s a lesser known way of specifying which method in a Spring controller should be executed based on the query parameters sent to it.
Before knowing about this I would have used optional parameters and if statements inside the same handler - which increases complexity and decreases readability. For example, here I’m returning a bank account and there are two ways of searching for one, by nameOnAccount
and by accountNumber
.
@RestController
@RequestMapping(value = "/accounts")
public class AccountController {
private AccountService accountService;
public AccountController(AccountService accountService) {
this.accountService = accountService;
}
@RequestMapping(method = GET)
public ResponseEntity<AccountResource> searchForAccount(
@RequestParam(value = "accountNumber", required = false) String accountNumber,
@RequestParam(value = "nameOnAccount", required = false) String nameOnAccount) {
// both params are optional so I need to manually check if neither or both exist
if ((isBlank(accountNumber) && isBlank(nameOnAccount))
|| isNotBlank(accountNumber) && isNotBlank(nameOnAccount)) {
// Returns a bad request
throw new AccountSearchException(
"Must provide either account number or name on account only.");
}
if (isNotBlank(accountNumber)) {
return accountService.getAccountByAccountNumber(accountNumber);
} else {
return accountService.getAccountByNameOnAccount(nameOnAccount);
}
}
}
The above method has more if statements than necessary for a simple handler which makes it difficult to read. We can make it simpler by using the params field in the @RequestMapping
annotation.
You can provide an array of strings in the params field of the @RequestMapping
. This field can specify whether the request query parameters should have a specific value e.g. params = "accountNumber=12345678"
or not have a specific value e.g. params = "accountNumber!=12345678"
or whether it should have the query parameter present with any value e.g. params = "accountNumber"
or not present e.g. params = "!accountNumber"
.
The example below shows how you can simplify the above code and still provide the same functionality. The other possible parameter needs to be negated so that if someone provides both query parameters they get a 400 status code back, just as they did before from the AccountSearchException
.
@RestController
@RequestMapping(value = "/accounts")
public class AccountController {
private AccountService accountService;
public AccountController(AccountService accountService) {
this.accountService = accountService;
}
@RequestMapping(method = GET, params = {"accountNumber", "!nameOnAccount" })
public ResponseEntity<AccountResource> getAccountByAccountNumber(
@RequestParam(value = "accountNumber") String accountNumber) {
return accountService.getAccountByAccountNumber(accountNumber);
}
@RequestMapping(method = GET, params = {"nameOnAccount", "!accountNumber"})
public ResponseEntity<AccountResource> getAccountByNameOnAccount(
@RequestParam(value = "nameOnAccount") String nameOnAccount) {
return accountService.getAccountByNameOnAccount(nameOnAccount);
}
}
By using the params field we’ve been able to get rid of a number of branches in our code whilst still having handlers attached to the same url mapping.
Written by Phil Hardwick