spring-hateoas
spring-hateoas copied to clipboard
Reactive Support in Spring HATEOAS documentation
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
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?
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());
}
Any Hints?
I'm struggling with Spring HATEOAS and WebFlux too. Definitely need some examples in the documentation ...
I converted my service back to non-reactive :-/
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.
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
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
SomeModeltoFooDto - Inside the flatmap we start by creating a list of
Mono<Link> - Then we create a
Flux<Links>out of that list ofMono<Link> - And then we collect them as a
Mono<List<Link>> - Finally we map that
Mono<List<Link>>into aEntityModelwith ourFooDto - 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, 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 useflatMap(...)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 theMono<Link>into aFlux<Link>and thencollectList()will generate aMono<List<Link>>with the Flux's contents- Next step is a bit backwards: use
map(...)to get theList<Link>from theMonoand create a newMono<EntityModel<Thing>>with the list of links added to theEntityModel - Now our
Flux<EntityModel<Thing>> thingModelFluxcontains a reactive Flux for the entities to be returned - The following lines will wrap those EntityModels in a CollectionModel
- First create a
Mono<Link> selfLinkas usual - Use
collectList()to convert theFlux<EntityModel<Thing>>previously obtained to aMono<List<EntityModel<Thing>>> - FInally use
flatMap(...)to get the list of entities from the Mono and use it to createCollectionModelwrapped in a Mono. Since we're returning just one link, we don't need theFlux.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"
}
}
}