8

I'm having a controller like this one (in Kotlin):

@RestController
@RequestMapping("/")
class CustomerController (private val service: CustomerService) {
    @GetMapping("/{id}")
    fun findById(@PathVariable id: String,
                 @RequestHeader(value = IF_NONE_MATCH) versionHeader: String?): Mono<HttpEntity<KundeResource>> =
        return service.findById(id)
            .switchIfEmpty(Mono.error(NotFoundException()))
            .map {
                // ETag stuff ...
                ok().eTag("...").body(...)
            }
}

Im wondering whether there is a better approach than throwing an exception which is annotated with @ResponseStatus(code = NOT_FOUND)

Juergen Zimmermann
  • 2,084
  • 7
  • 29
  • 43

3 Answers3

8

I would like use RouteFunction instead of @RestController when Spring 5 is stable. Define a HandlerFunction to handle request, and then declare a RouteFunction to map request to the HandlerFunction:

public Mono<ServerResponse> get(ServerRequest req) {
    return this.posts
        .findById(req.pathVariable("id"))
        .flatMap((post) -> ServerResponse.ok().body(Mono.just(post), Post.class))
        .switchIfEmpty(ServerResponse.notFound().build());
}

Check the complete example codes here.

Kotlin version, define a function to handle request, the use RouteFunctionDSL to map incoming request to HandlerFuncation:

fun get(req: ServerRequest): Mono<ServerResponse> {
    return this.posts.findById(req.pathVariable("id"))
            .flatMap { post -> ok().body(Mono.just(post), Post::class.java) }
            .switchIfEmpty(notFound().build())
}

It is can be an expression, like:

fun get(req: ServerRequest): Mono<ServerResponse> = this.posts.findById(req.pathVariable("id"))
            .flatMap { post -> ok().body(Mono.just(post), Post::class.java) }
            .switchIfEmpty(notFound().build())

Check the complete example codes of Kotlin DSL here.

If you prefer traditional controllers to expose REST APIs, try this approach.

Firstly define an exception, eg. PostNotFoundException. Then throw it in controller.

 @GetMapping(value = "/{id}")
    public Mono<Post> get(@PathVariable(value = "id") Long id) {
        return this.posts.findById(id).switchIfEmpty(Mono.error(new PostNotFoundException(id)));
    }

Define an ExceptionHandler to handle the exception, and register it in HttpHandler.

@Profile("default")
@Bean
public NettyContext nettyContext(ApplicationContext context) {
    HttpHandler handler = WebHttpHandlerBuilder.applicationContext(context)
        .exceptionHandler(exceptionHandler())
        .build();
    ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(handler);
    HttpServer httpServer = HttpServer.create("localhost", this.port);
    return httpServer.newHandler(adapter).block();
}

@Bean
public WebExceptionHandler exceptionHandler() {
    return (ServerWebExchange exchange, Throwable ex) -> {
        if (ex instanceof PostNotFoundException) {
            exchange.getResponse().setStatusCode(HttpStatus.NOT_FOUND);
            return exchange.getResponse().setComplete();
        }
        return Mono.error(ex);
    };
}

Check the complete codes here. For Spring Boot users, check this sample.

Update:In the latest spring 5.2, I found the general @RestControllerAdvice works for controllers in webflux applications.

Hantsy
  • 8,006
  • 7
  • 64
  • 109
  • On your GitHub sample code here: https://github.com/hantsy/spring-reactive-sample/blob/master/routes/src/main/java/com/example/demo/PostHandler.java for the create method everything works fine but when the request body is empty it returns an HTTP 200 ?!!! Any Suggestions – Mr.Q Sep 26 '19 at 16:21
  • Add validation for the request body, see this question: https://stackoverflow.com/questions/48031647/what-is-the-best-way-to-validate-request-in-a-spring-webflux-functional-applicat, I hope there are official solutions for this. – Hantsy Sep 30 '19 at 11:35
7

You can use ResponseStatusException, just extend your exception:

public class YourLogicException extends ResponseStatusException {

public YourLogicException(String message) {
    super(HttpStatus.NOT_FOUND, message);
}

public YourLogicException(String message, Throwable cause) {
    super(HttpStatus.NOT_FOUND, message, cause);
}

And in service:

public Mono<String> doLogic(Mono<YourContext> ctx) {
    return ctx.map(ctx -> doSomething(ctx));
}

private String doSomething(YourContext ctx) {
    try {
        // some logic
    } catch (Exception e) {
        throw new YourLogicException("Exception message", e);
    }
}

And after that, you could have a pretty message:

 { "timestamp": 00000000, "path": "/endpoint", "status": 404, "error": "Not found", "message": "Exception message" }
mchernyakov
  • 196
  • 3
  • 14
5

Instead of throwing an exception the method's implementation can be changed to

fun findById(@PathVariable id: String,
             @RequestHeader(value = IF_NONE_MATCH) versionHeader: String?): Mono<ResponseEntity<KundeResource>> =
    return service.findById(id)
        .map {
            // ETag stuff ...
            ok().eTag("...").body(...)
        }
        .defaultIfEmpty(notFound().build())
Juergen Zimmermann
  • 2,084
  • 7
  • 29
  • 43