Use VAVR.io / JavaSLang Futures in Spring-Boot Controllers

25 May
2020

As I’m using VAVR (formerly known as JavaSLang) in my current Spring-Boot project, I was looking for a way to use the futures returned by my services directly instead of converting them to CompletionStages.

My old controller code looked like this:

import io.micrometer.core.annotation.Timed;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.vavr.control.Option;
import lombok.AllArgsConstructor;
import lombok.Value;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import java.util.Date;
import java.util.concurrent.CompletableFuture;
@RestController
@Slf4j
@Validated
@AllArgsConstructor
public class ControllerOld implements ControllerHelpers {
final Service service;
@Timed
@GetMapping(path = "${mvc.context-path}/v3/scenario/{scenarioId}/assets/", name = "getAssetList")
@Operation(operationId = "getAssetList", method = "GET",
responses = {
@ApiResponse(responseCode = "200", ref = "#/components/schemas/GetAssetListResponse", description = "success"),
@ApiResponse(responseCode = "400", description = "failure")
},
tags = {"assets"}
)
public CompletableFuture<ResponseEntity<GetAssetListResponse>> getAssetList(
@PathVariable("scenarioId") @Valid ScenarioId scenarioId
) {
val correlationId = CorrelationId.random();
val userId = UserId.of("STATIC") ;
val serviceResponse = service.getScenarioAssets(
new GetScenarioAssetsQuery(
correlationId,
userId,
scenarioId,
)
);
return serviceResponse.map(either > either.map(GetAssetListController.GetAssetListResponse::fromServiceResponse)
.getOrElseThrow(left > badRequest(correlationId, "Failed to get asset list", left.getError())))
.map(ResponseEntity::ok)
.toCompletableFuture(); // converting to completableFuture for spring support
}
}
view raw ControllerOld.java hosted with ❤ by GitHub

as you can see, the return type of the controller is set to be a CompletionStage and we have to convert the vavr-future to that data type manually.

Wouldn’t it be much nicer, to simple be able to return the Future<T> type of vavr directly? This is how the controller should look like!

import io.micrometer.core.annotation.Timed;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.vavr.concurrent.Future;
import io.vavr.control.Option;
import lombok.AllArgsConstructor;
import lombok.Value;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import java.util.Date;
@RestController
@Slf4j
@Validated
@AllArgsConstructor
public class ControllerNew implements ControllerHelpers {
final Service service;
@Timed
@GetMapping(path = "${mvc.context-path}/v3/scenario/{scenarioId}/assets/", name = "getAssetList")
@Operation(operationId = "getAssetList", method = "GET",
responses = {
@ApiResponse(responseCode = "200", ref = "#/components/schemas/GetAssetListResponse", description = "success"),
@ApiResponse(responseCode = "400", description = "failure")
},
tags = {"assets"}
)
public Future<ResponseEntity<GetAssetListResponse>> getAssetList(
@PathVariable("scenarioId") @Valid ScenarioId scenarioId
) {
val correlationId = CorrelationId.random();
val userId = UserId.of("STATIC") ;
val serviceResponse = service.getScenarioAssets(
new GetScenarioAssetsQuery(
correlationId,
userId,
scenarioId,
)
);
return serviceResponse.map(either > either.map(GetAssetListController.GetAssetListResponse::fromServiceResponse)
.getOrElseThrow(left > badRequest(correlationId, "Failed to get asset list", left.getError())))
.map(ResponseEntity::ok)
; // no more converting to completableFuture
}
….
}
view raw ControllerNew.java hosted with ❤ by GitHub

Awesome, so no more converting to CompletableFuture. But how do we get there?

As long as there is no published project containing the following file, we have to add it ourselves:

package com.dominikdorn.web.configuration;
import io.vavr.concurrent.Future;
import org.springframework.core.MethodParameter;
import org.springframework.lang.Nullable;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.async.DeferredResult;
import org.springframework.web.context.request.async.WebAsyncUtils;
import org.springframework.web.method.support.AsyncHandlerMethodReturnValueHandler;
import org.springframework.web.method.support.ModelAndViewContainer;
// implementing AsyncHandlerMethodReturnValueHandler makes sure that this ReturnValueHandler is processed before
// normal value handlers (which would transform the future to a json value)
// see https://github.com/spring-projects/spring-framework/issues/17674 for details
public class VavrFutureHandlerMethodReturnValueHandler implements AsyncHandlerMethodReturnValueHandler {
public VavrFutureHandlerMethodReturnValueHandler() {
}
@Override
public boolean supportsReturnType(MethodParameter returnType) {
Class<?> type = returnType.getParameterType();
return Future.class.isAssignableFrom(type);
}
@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
if (returnValue == null) {
mavContainer.setRequestHandled(true);
return;
}
DeferredResult<?> result;
if (returnValue instanceof Future) {
result = adaptVavrFuture((Future<?>) returnValue);
} else {
// Should not happen…
throw new IllegalStateException("Unexpected return value type: " + returnValue);
}
WebAsyncUtils.getAsyncManager(webRequest).startDeferredResultProcessing(result, mavContainer);
}
private DeferredResult<Object> adaptVavrFuture(Future<?> future) {
DeferredResult<Object> result = new DeferredResult<>();
future
.onSuccess(result::setResult)
.onFailure(result::setErrorResult);
return result;
}
@Override
public boolean isAsyncReturnValue(Object returnValue, MethodParameter returnType) {
return returnValue instanceof Future;
}
}

We’re specifying our own “HandlerMethodReturnValueHandler” . Important here is that it implements the “AsyncHandlerMethodReturnValueHandler” interface which makes sure that Spring will handle this handler before normal handlers that e.g. transform values to json.

After that, we simply need to register this Handler with Spring. Most likely, you already have a WebMvcConfigurer in your project. If yes, just add the “addReturnValueHandlers” method to your existing one or create a completely new one like I did here:

package com.dominikdorn.web.configuration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
@Configuration
public class WebMvcConfiguration {
@Bean
public WebMvcConfigurer corsConfigurer() {
// … configure all kinds of things here, like how to handle cors etc.
return new WebMvcConfigurer() {
@Override
public void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> handlers) {
handlers.add(new VavrFutureHandlerMethodReturnValueHandler());
}
};
}
}

Enjoy using your vavr-futures in your Spring Boot applications now.

In an upcoming post I’ll probably look on how to effectively work with the Either type of Vavr (because I can’t stand these nasty exceptions just to return expected error states!)

Like what you see? Let me know in the comments!

Comment Form

top