1
votes

Imagine I have two microservices and I want to implement the BFF (Backend for the Frontend) pattern within a Spring REST controller which uses WebFlux.

The domain objects from the 2 remote services are:

public class Comment {
    private Long id;
    private String text;
    private Long authorId;
    private Long editorId;
}

public class Person {
    private Long id;
    private String firstName;
    private String lastName;
}

and the API Composer must return objects of following type:

public class ComposedComment {
    private String text;
    private String authorFullName;
    private String editorFullName;
}

For the sake of semplicity I wrote a Controller which simulates all the services in one.

@RestController
@RequestMapping("/api")
public class Controller {

    private static final List<Comment> ALL_COMMENTS = Arrays.asList(//
            new Comment(1L, "Bla bla", 1L, null), //
            new Comment(2L, "lorem ipsum", 2L, 3L), //
            new Comment(3L, "a comment", 2L, 1L));
    private static final Map<Long, Person> PERSONS;

    static {
        PERSONS = new HashMap<>();
        PERSONS.put(1L, new Person(1L, "John", "Smith"));
        PERSONS.put(2L, new Person(2L, "Paul", "Black"));
        PERSONS.put(3L, new Person(3L, "Maggie", "Green"));
    }

    private WebClient clientCommentService = WebClient.create("http://localhost:8080/api");
    private WebClient clientPersonService = WebClient.create("http://localhost:8080/api");

    @GetMapping("/composed/comments")
    public Flux<ComposedComment> getComposedComments() {
     //This is the tricky part
    }

    private String extractFullName(Map<Long, Person> map, Long personId) {
        Person person = map.get(personId);
        return person == null ? null : person.getFirstName() + " " + person.getLastName();
    }

    @GetMapping("/comments")
    public ResponseEntity<List<Comment>> getAllComments() {
        return new ResponseEntity<List<Comment>>(ALL_COMMENTS, HttpStatus.OK);
    }

    @GetMapping("/persons/{personIds}")
    public ResponseEntity<List<Person>> getPersonsByIdIn(@PathVariable("personIds") Set<Long> personIds) {
        List<Person> persons = personIds.stream().map(id -> PERSONS.get(id)).filter(person -> person != null)
                .collect(Collectors.toList());
        return new ResponseEntity<List<Person>>(persons, HttpStatus.OK);
    }
}

My problem is I have just began with Reactor and I am not really sure of what I am doing.. This is the current version of my composer method:

@GetMapping("/composed/comments")
public Flux<ComposedComment> getComposedComments() {
    Flux<Comment> commentFlux = clientCommentService.get().uri("/comments").retrieve().bodyToFlux(Comment.class);
    Set<Long> personIds = commentFlux.toStream().map(comment -> Arrays.asList(comment.getAuthorId(), comment.getEditorId())).flatMap(Collection::stream).filter(Objects::nonNull).collect(Collectors.toSet());
    Map<Long, Person> personsById = clientPersonService.get().uri("/persons/{ids}", personIds.stream().map(Object::toString).collect(Collectors.joining(","))).retrieve().bodyToFlux(Person.class).collectMap(Person::getId).block();
    return commentFlux.map(
            comment -> new ComposedComment(
                    comment.getText(),
                    extractFullName(personsById, comment.getAuthorId()),
                    extractFullName(personsById, comment.getEditorId()))
    );
}

It works, nevertheless I know I should make several transformations with map, flatMap and zip instead of invoking block() and toStream()... Can you please help me to rewrite this method correctly? :)

2
Avoid using toStream or other blocking operator. In your Spring 5 webflux environment you are never forced to block.Daniel Jipa
Hi @DanielJipa, thanks for your answer. The problem is, I cannot create an URI without first blocking on the mono/flux to get the String I will use as a uriVariables, as the WebClient.UriSpec doesn't have any reactive uri method: docs.spring.io/spring/docs/current/javadoc-api/org/… Or am I missing something? Thanks!Federico
Your domain model seems strange and also concatenate the personids to send it to the request path. As I understand there are multiple personids on the comment.Daniel Jipa
If thats the case on each comment on the flatMap concatenate the ids and send it to the personService.Daniel Jipa
yes it's right: each comment has 2 personIds: one for the author (which is never null) and one for the editor (which may be null if the comment didn't get edited). The idea is to collect all the comments from the commentService and then in 1 single further call collect all the persons which authored/modified any comments...Federico

2 Answers

1
votes

You are returning null in your controller. Replace it by returning the reactive stream instead.

return commentFlux.flatMap(comment -> ....)
....

Your controller signature return a

Flux<ComposedComment>

so make sure in the last return, you have to use flatMap or map to transform them to ComposedComment. You could think it as a promise chain, where you can do many flatMap, map in the implementation to transform to a final dataset.

Don't use subscribe in these situations, subscribe is suitable for demonstrating the invoking process of the reactive stream or in somewhere in the app where the result calling the method not needed directly as this controller

At this time you just return a reactive stream by using map, flatMap, collect, zip...... Just return the reactive stream (Mono, Flux<>) then spring-webflux will invoke them.

1
votes

You should try the zip operator to compose the two publishers. And don't subscribe to the flux if you want to return it.

If you can't use the zip because the second publisher depends on results from the first then use flatMap.

You can use the flatMap like this:

commentsFlux.flatMap(comment -> personService.getPersonsByIds(comment.getPersonId1() + "," + comment.getPersonId2())
                                      //at this moment you have scope on both
                                      .map(listOfTwoPersons -> new Composed(listOfTwoPersons, comment))

N.B. I didn't worked with webflux client and I am just guessing from your working example it knows to wrap to a Flux/Mono even if you return an entity or list of entities.