Spring MVC + DeferredResult add Hateoas stuff
Asked Answered
L

2

5

For the rest interface the Spring MVC + RxJava + DeferredResult returned from controllers is used.

I am thinking about adding Hateoas support to the endpoints. The natural choice would be the Spring Hateoas. The problem is that Spring Hateoas would not work in the asynchronous/multi-threading environment since it uses ThreadLocal.

Is there any way to workaround that constraint? I do not think so but maybe someone has any suggestions.

Has anyone used other APIs to add Hateoas support to the rest endpoints?

Thank you.

Langtry answered 16/2, 2015 at 11:27 Comment(3)
Can you please specify your problem? since it uses ThreadLocal isn't a problem in itself.Impercipient
Actually the fact that ThreadLocal is used is the problem. The request is processed in different threads. I mean, business logic behind the controller is executed in a reactive environment (RxJava). When it returns asynchronously, it is a different thread that the original one. When Spring Hateoas tries to fetch request attributes which are stored in ThreadLocal it gets null.Langtry
The problem would then be how it is used. I am not aware of any part of Spring HATEOAS accessing request attributes. It wouldn't make much sense, but I'm happy to be proved wrong. Spring MVC itself supports asynchronous processing. A concrete example would definitely help to find the problem.Impercipient
L
4

So the solution I've used is to closure in the request attributes and then apply them as part of a lift operator

public class RequestContextStashOperator<T> implements Observable.Operator<T, T> {

    private final RequestAttributes attributes;

    /**
     * Spring hateoas requires the request context to be set but observables may happen on other threads
     * This operator will reapply the context of the constructing thread on the execution thread of the subscriber
     */
    public RequestContextStashOperator() {
        attributes = RequestContextHolder.currentRequestAttributes();
    }
    @Override
    public Subscriber<? super T> call(Subscriber<? super T> subscriber) {
        return new Subscriber<T>() {
            @Override
            public void onCompleted() {
                subscriber.onCompleted();
            }

            @Override
            public void onError(Throwable e) {
                subscriber.onError(e);
            }

            @Override
            public void onNext(T t) {
                RequestContextHolder.setRequestAttributes(attributes);
                subscriber.onNext(t);
            }
        };
    }
}

which you can then use on an observable like

lift(new RequestContextStashOperator<>())

as long as the object is created in the same thread as the request. You can then use a map after in the observable chain to map your object up to being a resource and add your hateoas links in.

Lunn answered 9/11, 2015 at 15:12 Comment(1)
This was super helpful. Right now I'd like to click the upvote like 10 times to celebrate. Thx.Penton
Z
3

So answer is a bit late, but probably someone will find it useful. You are right about ThreadLocal - if you generate hateoas links in different thread, then it fails with exception. I found some kind of workaround for this:

@RequestMapping(path = "/{id}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
DeferredResult<ResponseEntity<ProductResource>> example(@PathVariable("id") final String productId, final HttpServletRequest request) {

    final DeferredResult<ResponseEntity<ProductResource>> deferredResult = new DeferredResult<>();

    request.setAttribute(WebUtils.INCLUDE_CONTEXT_PATH_ATTRIBUTE, request.getContextPath());
    final RequestAttributes requestAttributes = new ServletRequestAttributes(request);

    productByIdQuery.byId(UUID.fromString(productId)).subscribe(productEntity -> {
        RequestContextHolder.setRequestAttributes(requestAttributes);
            deferredResult.setResult(result, HttpStatus.OK))
    }, deferredResult::setErrorResult);

    return deferredResult;
}

So as you see, I save RequestAttributes so I can set them later in the callback. This solves just part of the problem - you'll get another exception because you'll loose contextPath attribute. To avoid this save it explicitly:

request.setAttribute(WebUtils.INCLUDE_CONTEXT_PATH_ATTRIBUTE, request.getContextPath());

After those changes everything seems to work, but looks messy of course. I hope that somebody can provide more elegant solution.

Zambrano answered 28/10, 2015 at 14:31 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.