spring-bootspring-data-mongodbproject-reactorspring-data-mongodb-reactive

WebFlux functional endpoint, how to return a ServerResponse with retrieved data and no unnecessary block/wait?


I am totally new to reactive code and after a number of tutorials and youtube videos I am trying to setup a tiny test application using functional endpoints; a simple RouterFunction, RouterHandler and Repository. The problem is how to return an object in the ServerResponse from the respository to the caller, without causing any unnecessary blocking?

I am using Postman for testing. Here's the interesting parts of my test application:

@Configuration
public class BookRouter {
    @Autowired 
    BookRepositoryImpl bookRepository;

    @Bean
    public RouterFunction<ServerResponse> bookRoutes() {
        BookHandler bookHandler = new BookHandler(bookRepository);
        return RouterFunctions
            .nest(path("/api/books"),
                route(GET("/{group}/{name}").and(accept(ALL)), bookHandler::getBook)
            );
        }
    }

@Repository
public class BookRepositoryImpl implements BookRepository {
    private final ReactiveMongoTemplate mongoTemplate;

    @Autowired
    public BookRepositoryImpl(ReactiveMongoTemplate mongoTemplate) {
        this.mongoTemplate = mongoTemplate;
    }

    @Override
    public Mono<Book> findByName(String group, String name) {
        Query query = new Query(Criteria.where("group").is(group).and("name").is(name));
        return mongoTemplate.findOne(query, Book.class);
    }
}

public class BookHandler {
    public Mono<ServerResponse> getBook(ServerRequest request) {
        String group = request.pathVariable("group");
        String name = request.pathVariable("name");

        bookRepository
            .findByName(group, name)
            .subscribe(
                ok -> System.out.println("findByName " + ok), 
                error -> System.err.println("Error: " + error));
        return ServerResponse
            .accepted()
            .contentType(MediaType.TEXT_PLAIN)
            .bodyValue("Request queued");
    }
}

When I have the code as shown above, the expected data is printed out in subscribe(ok -> ...), but I haven't figured out how to return this data in the ServerResponse.

If I change the code in getBook() to

return setting
    .flatMap(s -> ServerResponse
        .ok()
        .contentType(MediaType.APPLICATION_JSON)
        .bodyValue(s))
    .switchIfEmpty(NOT_FOUND);

the returned bodyValue is empty, although I can see that it was retrieved from the database.

Any advice on what I am missing is most appreciated.

Update

I am using MongoDB Compass to view and verify the content of the database.

Debug logging is enabled in application.properties with logging.level.root=DEBUGso the Spring classes write some info in the terminal window. Parts of the somewhat anonymized log is as follows:

2020-09-05 21:37:02.688 DEBUG 32720 --- [ctor-http-nio-3] o.s.w.r.f.s.s.RouterFunctionMapping      : [96ef6152-1] Mapped to com.sample.book.BookRouter$$Lambda$586/0x0000000800540040@3b0bf8e0
2020-09-05 21:37:02.717 DEBUG 32720 --- [ctor-http-nio-3] o.s.d.m.core.ReactiveMongoTemplate       : findOne using query: { "group" : "Thriller", "name" : "The Shining"} fields: Document{{}} for class: class com.sample.book.Book in collection: book
2020-09-05 21:37:02.734 DEBUG 32720 --- [ctor-http-nio-3] o.s.d.m.core.ReactiveMongoTemplate       : findOne using query: { "group" : "Thriller", "name" : "The Shining"} fields: {} in db.collection: book.book
2020-09-05 21:37:02.751 DEBUG 32720 --- [ctor-http-nio-3] org.mongodb.driver.protocol.command      : Sending command '{"find": "book", "filter": {"group": "Thriller", "name": "The Shining"}, "limit": 1, "singleBatch": true, "$db": "book"}' with request id 7 to database book on connection [connectionId{localValue:2, serverValue:217}] to server localhost:27017
2020-09-05 21:37:02.766 DEBUG 32720 --- [ntLoopGroup-3-2] org.mongodb.driver.protocol.command      : Execution of command with request id 7 completed successfully in 16.24 ms on connection [connectionId{localValue:2, serverValue:217}] to server localhost:27017
2020-09-05 21:37:02.837 DEBUG 32720 --- [ntLoopGroup-3-2] o.s.http.codec.json.Jackson2JsonEncoder  : [96ef6152-1] Encoding [_id=5f53692af0a02d3af8a7fed9, group=Thriller, name=The Shining, value=in]]
2020-09-05 21:37:02.853 DEBUG 32720 --- [ctor-http-nio-3] r.n.http.server.HttpServerOperations     : [id: 0x96ef6152, L:/0:0:0:0:0:0:0:1:8088 - R:/0:0:0:0:0:0:0:1:50248] Decreasing pending responses, now 0
2020-09-05 21:37:02.879 DEBUG 32720 --- [ctor-http-nio-3] o.s.w.s.adapter.HttpWebHandlerAdapter    : [96ef6152-1] Completed 200 OK
2020-09-05 21:37:02.905 DEBUG 32720 --- [ctor-http-nio-3] r.n.http.server.HttpServerOperations     : [id: 0x96ef6152, L:/0:0:0:0:0:0:0:1:8088 - R:/0:0:0:0:0:0:0:1:50248] Last HTTP response frame
2020-09-05 21:37:02.931 DEBUG 32720 --- [ctor-http-nio-3] r.n.http.server.HttpServerOperations     : [id: 0x96ef6152, L:/0:0:0:0:0:0:0:1:8088 - R:/0:0:0:0:0:0:0:1:50248] Last HTTP packet was sent, terminating the channel

Solution

  • I found the problem. I had forgot to implement getters in the Book class holding the @Document. I am surprised that there were no error message or warning when they were missing.

    As soon as I inserted them, the result was returned as expected from this code:

    return setting
        .flatMap(s -> ServerResponse
            .ok()
            .contentType(MediaType.APPLICATION_JSON)
            .bodyValue(s))
        .switchIfEmpty(NOT_FOUND);
    

    Here is the data as returned to Postman after the fix:

    {
        "_id": "5f53692af0a02d3af8a7fed9",
        "group": "Thrillers",
        "name": "The Shining",
        "value": "in"
    }
    

    Thank you to @caco3 for helping me find the problem!

    Here is my updated Book.java.

    @Document
    @CompoundIndex(name = "group-name", def = "{'group':1, 'name':1}", unique = true) // Requires auto-index-creation in application.properties
    public class Book {
        @Id
        private String _id;
        private String group;
        private String name;
        private String value;
    
        public Book() {
        }
    
        public Book(String group, String name, String value) {
            this.group = group;
            this.name = name;
            this.value = value;
        }
    
        @Override
        public String toString() {
            StringBuilder s = new StringBuilder('[');
            s.append("_id=").append(_id);
            s.append(", group=").append(group);
            s.append(", name=").append(name);
            s.append(", value=").append(value);
            s.append(']');
    
            return s.toString();
        }
    
        public String get_id() {
            return _id;
        }
    
        public String getGroup() {
            return group;
        }
    
        public void setGroup(String group) {
            this.group = group;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public String getValue() {
            return value;
        }
    
        public void setValue(String value) {
            this.value = value;
        }
    }