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.

Robert Bain
  • 9,113
  • 8
  • 44
  • 63
Damian
  • 593
  • 6
  • 27
  • Can you please specify your problem? _since it uses ThreadLocal_ isn't a problem in itself. – a better oliver Feb 17 '15 at 08:56
  • 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. – Damian Feb 18 '15 at 09:38
  • 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. – a better oliver Feb 18 '15 at 10:02

2 Answers2

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.

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.

Andrej Urvantsev
  • 466
  • 4
  • 15