Let’s see an example of RestController.
@RestController
@RequestMapping("/api/whiskies")
public class WhiskyController {
private final WhiskyRepository postRepository;
public WhiskyController(WhiskyRepository postRepository) {
this.postRepository = postRepository;
}
@GetMapping
public List<Whisky> findAll() {
return postRepository.findAll();
}
@GetMapping("/{id}")
public Whisky findById(@PathVariable("id") UUID id) {
return postRepository.findById(id).orElseThrow(()->new ElementNotFoundException(id));
}
}
This controller defines 2 GET endpoints :
Let’s see the RestControllerAdvice that handles the errors.
@RestControllerAdvice
public class ExceptionHandlerAdvice {
@ExceptionHandler(ElementNotFoundException.class)
public ProblemDetail handlePostNotFoundException(ElementNotFoundException exception) throws URISyntaxException {
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, exception.getMessage());
problemDetail.setProperty("id", exception.getId());
problemDetail.setType(new URI("http://localhost:8080/problems/post-not-found"));
return problemDetail;
}
}
When an ElementNotFound is thrown the controller advice intercepts the exception and allows us to return a proper response.
Many of us are used to that king of Spring Programming but there is a better way Router function.
We need to define a RouterFunction bean to define our routes.
@Configuration(proxyBeanMethods = false)
public class RouterConfiguration {
@Bean
public RouterFunction<ServerResponse> whiskiesRouter(WhiskyRepository wr) {
return route()
.GET("/api/whiskies", req -> ok().body(wr.findAll()))
.GET("/api/whiskies/{id}", req -> {
var id = UUID.fromString(req.pathVariable("id"));
return ok().body(wr.findById(id).orElseThrow(() -> new ElementNotFoundException(id)));
})
.onError(ElementNotFoundException.class,
(e, req) -> {
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, e.getMessage());
problemDetail.setProperty("id", ((ElementNotFoundException) e).getId());
try {
problemDetail.setType(new URI("http://localhost:8080/problems/post-not-found"));
problemDetail.setInstance(new URI(req.requestPath().toString()));
} catch (URISyntaxException ex) {
throw new RuntimeException(ex);
}
return EntityResponse.fromObject(problemDetail)
.status(HttpStatus.NOT_FOUND)
.build();
})
.build();
}
}
The RouterFunction takes a ServerRequest and returns a ServerResponse.
It’s a much nicer way to the all the routes especially when there are many controllers.
But still, something is missing, we need to define handler functions to avoid too much code in the definition of our routes.
Let’s define 2 handlers :
@Component
public class WhiskyHandler {
private final WhiskyRepository whiskyRepository;
public WhiskyHandler(WhiskyRepository whiskyRepository) {
this.whiskyRepository = whiskyRepository;
}
public ServerResponse getWhiskies(ServerRequest serverRequest) {
return ServerResponse
.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(whiskyRepository.findAll());
}
public ServerResponse getAWhiskyById(ServerRequest serverRequest) {
var id = UUID.fromString(serverRequest.pathVariable("id"));
return ServerResponse
.ok()
.body(whiskyRepository.findById(id)
.orElseThrow(() -> new ElementNotFoundException(id)));
}
}
It’s pretty straightforward and similar to the code from the RestController we started with. The thing missing is the api routes.
And that the thing I like. The code building the response and the routes are not in the same place, so we can have a single piece of code to see all the routes.
@Component
public class ErrorHandler {
public ServerResponse elementNotFoundHandler(Throwable e, ServerRequest serverRequest) {
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, e.getMessage());
problemDetail.setProperty("id", ((ElementNotFoundException) e).getId());
try {
problemDetail.setType(new URI("http://localhost:8080/problems/post-not-found"));
problemDetail.setInstance(new URI(serverRequest.requestPath().toString()));
} catch (URISyntaxException ex) {
throw new RuntimeException(ex);
}
return EntityResponse.fromObject(problemDetail)
.status(HttpStatus.NOT_FOUND)
.build();
}
}
Let’s rework the RouterFunction bean to properly use our handlers.
@Configuration(proxyBeanMethods = false)
public class RouterConfiguration {
@Bean
public RouterFunction<ServerResponse> whiskiesRouter(WhiskyHandler whiskyHandler, ErrorHandler errorHandler) {
return route()
.GET("/api/whiskies", whiskyHandler::getWhiskies)
.GET("/api/whiskies/{id}", whiskyHandler::getAWhiskyById)
.onError(ElementNotFoundException.class,errorHandler::elementNotFoundHandler)
.build();
}
}
Compared to the RestController way, we can really see what are the routes of our api.