0

I have the following code, which "works"...so far. By "works", I mean that the Flux<DemoPOJO> is being returned by service.getAll(), and the "hasElements().subscribe(this::foo)" results in foo() generating output that correctly reflects whether the Flux<DemoPOJO> has any elements.

The desired end state is to return a ServerResponse object, wrapping the Flux<DemoPOJO>, which reflects whether the returned Flux is empty or "hasElements".

My problem is that Mono.subscribe() returns a reactor.core.Disposable, and I want to somehow get to a Mono<ServerResponse>. Or, am I "barking up the wrong tree"?

Add Note: I've seen some examples using Flux.flatMap(), but this seems problematic if the returned Flux has a lot of elements (i.e., checking hasElements() seems a lot better than potentially flat-mapping all the elements).

@Component
public class DemoPOJOHandler {

    public static final String PATH_VAR_ID = "id";

    @Autowired
    private DemoPOJOService service;

    public Mono<ServerResponse> getAll(ServerRequest request) {
        Mono<ServerResponse> response = null;
        Flux<DemoPOJO>       entities = service.getAll();

        entities.hasElements().subscribe(this::foo);
        // just return something, for now
        return ServerResponse.ok().build();
    }

    private Mono<ServerRequest> foo(Boolean hasElements) {
        System.out.println("DEBUG >> Mono has elements -> " + hasElements);
        return Mono.empty();
    }
}

Here is the DemoPOJOService implementation...

@Component
public class DemoPOJOService {

    @Autowired
    private DemoPOJORepo demoPOJORepo;

    public Flux<DemoPOJO> getAll() {
        return Flux.fromArray(demoPOJORepo.getAll());
    }

    // more implementation, omitted for brevity
}

And, here is the DemoPOJORepo implementation...

@Component
public class DemoPOJORepo {

    private static final int NUM_OBJS =20;

    private static DemoPOJORepo demoRepo = null;

    private Map<Integer, DemoPOJO> demoPOJOMap;

    private DemoPOJORepo() {
        initMap();
    }

    public static DemoPOJORepo getInstance() {
        if (demoRepo == null) {
            demoRepo = new DemoPOJORepo();
        }
        return demoRepo;
    }

    public DemoPOJO[] getAll() {
        return demoPOJOMap.values().toArray(new DemoPOJO[demoPOJOMap.size()]);
    }

    // more implementation, omitted for brevity

    private void initMap() {
        demoPOJOMap = new TreeMap<Integer, DemoPOJO>();

        for(int ndx=1; ndx<( NUM_OBJS + 1 ); ndx++) {
            demoPOJOMap.put(ndx, new DemoPOJO(ndx, "foo_" + ndx, ndx+100));
        }
    }
}
Martin Tarjányi
  • 8,863
  • 2
  • 31
  • 49
SoCal
  • 801
  • 1
  • 10
  • 23

3 Answers3

2

My revised DemoPOJOHandler is below. It seems to correctly return either a ServerResponse.ok() that wraps the 'Flux' returned by service.getAll(), or a ServerResponse.noContent() if the Flux was empty.

While this "works", and seems much better than what I previously had, any improvements, comments, or suggestions are greatly appreciated, as I'm still trying to wrap my head around Reactor.

@Component
public class DemoPOJOHandler {

    public static final String PATH_VAR_ID = "id";

    @Autowired
    private DemoPOJOService service;

    public Mono<ServerResponse> getAll(ServerRequest request) {
        Flux<DemoPOJO> entities = service.getAll();

        return entities.hasElements().flatMap(hasElement -> {
            return hasElement ? ServerResponse.ok()
                                              .contentType(MediaType.APPLICATION_JSON)
                                              .body(entities, DemoPOJO.class)
                              : ServerResponse.noContent().build();
            });
    }
}
miken32
  • 42,008
  • 16
  • 111
  • 154
SoCal
  • 801
  • 1
  • 10
  • 23
1

@SoCal your answer might seem to work, but it suffers from a downside: getAll() DB call is made twice.

The difficulty is that you can only decide on the status code once you've started receiving data.

But since you don't seem to really need the asynchronous nature of the body (you're not streaming the individual elements but producing a one shot JSON response), you could in this case collect the whole result set and map it to a response.

So call the DB, collect the elements in a Mono<List>, map that to A) an 404 empty response if the list is empty or B) a 200 successful JSON response otherwise (notice the use of syncBody):

@Component
public class DemoPOJOHandler {

    public static final String PATH_VAR_ID = "id";

    @Autowired
    private DemoPOJOService service;

    public Mono<ServerResponse> getAll(ServerRequest request) {
        Flux<DemoPOJO> entities = service.getAll();
        Mono<List<DemoPOJO>> collected = entities.collectList();

        return collected.map(list -> list.isEmpty() ? 
            ServerResponse.noContent().build() :
            ServerResponse.ok()
                .contentType(MediaType.APPLICATION_JSON)
                .syncBody(list)
        );
    }
}

Side note: I think ResponseEntity is the preferred type for annotated controllers rather than ServerResponse, see https://docs.spring.io/spring/docs/current/spring-framework-reference/web-reactive.html#webflux-ann-responseentity.

Simon Baslé
  • 27,105
  • 5
  • 69
  • 70
  • Thanks for the input/feedback! As a newb, many questions and false-starts. Some follow-on questions follow... – SoCal Jul 23 '19 at 14:25
  • You are correct that the example code does not use any of the asynch capabilities of `Flux`. However, that "is coming", and this code is "just to get used to Monos and Fluxes. Does this mean that, when actually using asynch capabilities, the notion of returning any indication of availability of content is not feasible? – SoCal Jul 23 '19 at 14:30
  • For what it's worth, the REST endpoint is being implemented using routes and controllers (vice annotations). This seems like, downstream, it will provide more flexibility in the implementation. Does this change your advice about `ResponseEntity` vice `ServerResponse`? – SoCal Jul 23 '19 at 14:32
  • I'm probably being dense, but I don't see a second invocation of *`service.getAll()`*. Can you point me to it? If you're referring to *`entities.hasElements().flatMap()`*, my understanding is that *`flatMap()`* is being invoked on the *Mono* being returned by *`entities.hasElements()`*. Grazi for the help! – SoCal Jul 23 '19 at 14:37
  • I note the use of *`syncBody`*, and have seen it used elsewhere. Is there an advantage (performance or conformance) to this style? I ask because, while it is more verbose and could be considered "clunky", I like the "documentation aspect" of explicitly specifying the class. – SoCal Jul 23 '19 at 14:41
  • for getAll() it is more like the DB will be queried twice, once per _subscription_ to the `getAll()` `Mono`: once per the `hasElements()` operator and once per SPR in the `body`. In the case of my answer, using `body` instead of `syncBody` would mean you need to turn a collection _that you already have_ into a `Publisher`. Outside of readability, it brings no benefit and adds a bit of overhead by wrapping the collection. – Simon Baslé Jul 24 '19 at 16:05
  • for `ResponseEntity`, if you plan on using the functional stack then of course you're on the right track with `ServerResponse`. – Simon Baslé Jul 24 '19 at 16:06
  • for the async capabilities of `Flux`, what I meant is that on the wire you're not transmitting the individual elements one by one, like say with Server Sent Events. So the Spring Framework would gather all the data before sending it over in one big JSON object anyway, hence the feasibility of my proposed approach. – Simon Baslé Jul 24 '19 at 16:07
-2

First of all, you are not responsible for subscribing to flux in the controller. Fetching the data and returning it from the controller is just a small part of whole pipeline. It basically means that you need to provide only business logic, and the framework adds other transformations to your data. For example, it serializes the response and then, after it's finished, subscribes to it.

Subscribing to the flux in the business code means that you start another pipeline, which might be totally independent from the data returned from the controller, but it just happened to be subscribed there. If you would had the same Flux and you would subscribe it somewhere else, the result would be exactly the same.

To sum up: you need to take return value of entities.hasElements() (which is Mono<Boolean>) and wrap bool into response:

 public Mono<ServerResponse> getAll(ServerRequest request) {
        Mono<ServerResponse> response = null;
        Flux<DemoPOJO>       entities = service.getAll();

        return entities.hasElements()
           .flatMap(this::foo);

    }

    private Mono<ServerResponse> foo(Boolean hasElements) {
        System.out.println("DEBUG >> Mono has elements -> " + hasElements);
        return ServerResponse.ok().syncBody(hasElements);
    }
AlgoTrading
  • 55
  • 2
  • 2
  • 9
arap
  • 499
  • 2
  • 6