Skip to content

Instantly share code, notes, and snippets.

@domdorn
Last active June 14, 2022 07:52
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save domdorn/e5e5222ccfea5ffb123cada437c692c8 to your computer and use it in GitHub Desktop.
Save domdorn/e5e5222ccfea5ffb123cada437c692c8 to your computer and use it in GitHub Desktop.
spring boot / web: allow vavr javaslang future as return type
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
}
....
}
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
}
...
}
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;
}
}
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());
}
};
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment