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