spring-hateoas icon indicating copy to clipboard operation
spring-hateoas copied to clipboard

Reactive Support in Spring HATEOAS documentation

Open pcornelissen opened this issue 3 years ago • 9 comments

I am trying to use spring hateoas in a reactive application. The documentation is still a todo tag, so I explored the code myself and I'm having a hard time to understand how that is supposed to be used.

In regular webMVC workd I have the model assemblers that create an entitymodel or the collection in a blocking fashion.

In reactive I have Mono and Flux as returntypes.

Mono should be easy, Mono<EntityModel<MyEntity>>, so basically I just add a map(...) to the producing chain to create the wrapper as soon as the entity is present. But the WebFluxBuilder.linkTo returns a Mono, which I don't understand at all. It fetches information readily present on the system and there is no need in any way to return a mono, it just could return the links based on the provided information.

So I end up doing something like that:

@Component
public class ContentItemResourceAssembler implements ReactiveRepresentationModelAssembler<ContentItem, EntityModel<ContentItem>> {
    @Override
    public Mono<EntityModel<ContentItem>> toModel(ContentItem entity, ServerWebExchange exchange) {
        return Mono.just(EntityModel.of(entity,
                linkTo(methodOn(ContentReadController.class).getItems(entity.getTenantId(), entity.getId())).withSelfRel().toMono().block(),
                linkTo(methodOn(ContentBrowseController.class).getItems(entity.getTenantId(), null)).withRel("whatever").toMono().block()
        ));
    }
}

It gets worse with collections, the assembler consumes a flux and returns a mono of the collectionModel with the items, which I assume is due to the fact that there is currently no elegant way to return surrounding wrapper with a streamed content. So basically no streaming, when you want to return the collection with collection links. I assume to be able to have a special Flux with links which remains reactive, but is able to pass the surrounding links would require to enhance the reactor code to allow the reactive part to happen in a deeper nested part of the answer. Am I right?

So basically my "problem" boils down to:

  • Why does the webflux linkto return reactive types?
  • Wouldn't it make sense to support the flux<T> to flux<EntityModel<T>> case somehow to have that in an easy way?

pcornelissen avatar Mar 28 '22 19:03 pcornelissen

Example how one might have to use the mono from the builder:

  @PostMapping
    public Mono<ResponseEntity<Void>> getItems(@PathVariable UUID tenantId, @RequestBody ContentItem item) {
        item.setTenantId(tenantId);
        return contentItemRepository.insert(item)
                .doOnSuccess(contentItem -> log.info("Item add: {}", contentItem))
                .doOnError(throwable -> log.error("Could not save! {}", item, throwable))
                .publishOn(Schedulers.boundedElastic())
                .map(contentItem -> ResponseEntity.created(
                        Objects.requireNonNull(linkTo(methodOn(ContentReadController.class).getItems(tenantId, contentItem.getId())).withSelfRel().toMono()
                                .map(Link::toUri)
                                .blockOptional(Duration.ofSeconds(1))
                                .orElseThrow())
                ).build());
    }

Would it just return the link, the above could could be simplified:

  @PostMapping
    public Mono<ResponseEntity<Void>> getItems(@PathVariable UUID tenantId, @RequestBody ContentItem item) {
        item.setTenantId(tenantId);
        return contentItemRepository.insert(item)
                .doOnSuccess(contentItem -> log.info("Item add: {}", contentItem))
                .doOnError(throwable -> log.error("Could not save! {}", item, throwable))
                .map(contentItem -> ResponseEntity.created(
                        linkTo(methodOn(ContentReadController.class).getItems(tenantId, contentItem.getId())).withSelfRel().toUri())
                ).build());
    }

pcornelissen avatar Mar 29 '22 15:03 pcornelissen

Any Hints?

pcornelissen avatar May 29 '22 09:05 pcornelissen

I'm struggling with Spring HATEOAS and WebFlux too. Definitely need some examples in the documentation ...

CEDDM avatar Jun 21 '22 15:06 CEDDM

I converted my service back to non-reactive :-/

pcornelissen avatar Jun 22 '22 06:06 pcornelissen

As a workaround, you can use WebMvcLinkBuilder class instead of WebFluxLinkBuilder. WebMvcLinkBuilder generates Link, not Mono<Link>. It works fine with WebFlux in most cases. I only had issues when I used params parameter in @GetMapping annotation.

However, I really would like to see how to properly use WebFluxLinkBuilder.

kdebski85 avatar Aug 12 '22 08:08 kdebski85

That doesn't work, because the Request context, which is used to generate the link is handles differently in webflux. At east I tried to use the webmvc classes a while ago and it didn't work. But I assume, this is still the case.

pcornelissen avatar Aug 12 '22 08:08 pcornelissen

@pcornelissen

Maybe a better approach instead of blocking calls is something like this:

Flux<EntityModel<FooDto>> entityModelFlux = Flux.from(FlowAdapters.toPublisher(this.topic.replay()))
                .map(model -> FooDto.fooDtoBuilder()
                        .prop(model.getRecordInfo().getId())
                        .build())
                .flatMap(dto -> {
                    List<Mono<Link>> links = List.of(
                            linkTo(methodOn(DataImportController.class).get()).withSelfRel().toMono(),
                            linkTo(methodOn(DataImportController.class).foo()).withSelfRel().toMono()
                    );
                    return Flux.concat(links)
                            .collectList()
                            .map(link -> EntityModel.of(dto, link));
                });

Breakbown:

  • The first line is irrelevant, it just returns a Flux<SomeModel>
  • The first .map converts SomeModel to FooDto
  • Inside the flatmap we start by creating a list of Mono<Link>
  • Then we create a Flux<Links> out of that list of Mono<Link>
  • And then we collect them as a Mono<List<Link>>
  • Finally we map that Mono<List<Link>> into a EntityModel with our FooDto
  • The end result is a Flux<EntityModel<FooDto>> that you can return from a reactive @Controller

Result is something like this:

[
  {
    "prop": "e7f167a0-c915-4bb1-bc3b-dbbab87d05dd",
    "_links": {
      "self": [
        {
          "href": "http://localhost:8080/get"
        },
        {
          "href": "http://localhost:8080/import/excel"
        }
      ]
    }
  },
...
]

Hope it helps someone.

zeidoo avatar Dec 04 '22 21:12 zeidoo

@zeidoo, thanks for the guidance! After some struggle, I managed to create and serve links with the following code:

Flux<Thing> things = repository.findAll();

Flux<EntityModel<Thing>> thingModelFlux = things.flatMapSequential(thing -> {
	Mono<Link> detailsLink = linkTo(...).withRel("details").toMono();
	Mono<Link> overviewLink = linkTo(...).withRel("overview").toMono();
	Mono<List<Link>> entityLinkList = Flux.concat(detailsLink, overviewLink).collectList();
	Mono<EntityModel<Thing>> entityMono = entityLinkList.map(links -> EntityModel.of(thing, links));
	return entityMono;
});

Mono<Link> selfLink = linkTo(...).withSelfRel().toMono();
Mono<List<EntityModel<Thing>>> thingModelList = thingModelFlux.collectList();
return thingModelList.flatMap(entities -> selfLink.map(link -> CollectionModel.of(entities, link)));

Breakdown:

  • Retrieve our data (Flux<Thing> things) from a reactive repository, or from wherever your source data is
  • Use flatMapSequential(...) to process the data in the order they were retrieved, if the order is not important one may use flatMap(...) instead
  • Next we create a big lambda to create an EntityModel for each thing from the source Flux
  • Create as many links as needed, returning their Mono<Link> as separate variables or in a list as you did
  • Flux.concat(...).collectList() will convert the Mono<Link> into a Flux<Link> and then collectList() will generate a Mono<List<Link>> with the Flux's contents
  • Next step is a bit backwards: use map(...) to get the List<Link> from the Mono and create a new Mono<EntityModel<Thing>> with the list of links added to the EntityModel
  • Now our Flux<EntityModel<Thing>> thingModelFlux contains a reactive Flux for the entities to be returned
  • The following lines will wrap those EntityModels in a CollectionModel
  • First create a Mono<Link> selfLink as usual
  • Use collectList() to convert the Flux<EntityModel<Thing>> previously obtained to a Mono<List<EntityModel<Thing>>>
  • FInally use flatMap(...) to get the list of entities from the Mono and use it to create CollectionModel wrapped in a Mono. Since we're returning just one link, we don't need the Flux.concat(...).collectList() used above.

The returned value is of type Mono<CollectionModel<EntityModel<Thing>>>.

I find the syntax to do all of this very convoluted, so I'd rather extract it to a RepresentationModelAssemblerSupport. However, this support class is built for usage with MVC-style Controllers; since I am using RouterFunctions, for now I simply created separate methods to make the code a bit ~~easier to digest~~ more structured.

Complete example

public class ThingHandler {

	@Autowired
	private ThingRepository repository;

	public Mono<ServerResponse> list(ServerRequest request) {
		Flux<Thing> things = repository.findAll();
		Object bodyValue = things.flatMapSequential(this::buildEntityModel).collectList()
			.flatMap(this::buildCollectionModel);
		ParameterizedTypeReference<?> bodyType = ParameterizedTypeReference.forType(RepresentationModel.class);
		return ServerResponse.ok().contentType(MediaTypes.HAL_JSON).body(bodyValue, bodyType);
	}

	protected Mono<EntityModel<Thing>> buildEntityModel(Thing thing) {
		List<Mono<Link>> entityLinks = List.of(
			linkTo(...).withRel(...).toMono(),
			linkTo(...).withRel(...).toMono());
		return Flux.concat(entityLinks).collectList().map(links -> EntityModel.of(thing, links));
	}

	protected Mono<CollectionModel<EntityModel<Thing>>> buildCollectionModel(List<EntityModel<Thing>> entities) {
		return linkTo(...).withSelfRel().toMono().map(link -> CollectionModel.of(entities, link));
	}
}

Output

{
  "_embedded": {
    "things": [
      {
        "id": 1,
        "label": "Thing #1",
        "_links": {
          "details": {
            "href": "http://localhost:8080/rest/thing/1/details"
          },
          "overview": {
            "href": "http://localhost:8080/rest/thing/1"
          }
        }
      },
      {
        "id": 2,
        "parent": null,
        "label": "Thing #2",
        "_links": {
          "details": {
            "href": "http://localhost:8080/rest/thing/2/details"
          },
          "overview": {
            "href": "http://localhost:8080/rest/thing/2"
          }
        }
      }
    ]
  },
  "_links": {
    "self": {
      "href": "http://localhost:8080/rest/thing"
    }
  }
}

flaviocosta-net avatar Feb 10 '23 20:02 flaviocosta-net