I'm trying to write a generic function to do some Webflux operations and I'm getting a class cast exception that I can't figure out
* @param <T> Type of the contract
* @param <U> Return type of this method
* @param <V> Return type from the service
public <T, U, V> U sendRequest(String url, T contract, Function<V, U> transform) {
ParameterizedTypeReference<T> contractType = new ParameterizedTypeReference<T>() {};
ParameterizedTypeReference<V> returnType = new ParameterizedTypeReference<V>() {};
final WebClient.ResponseSpec foo = webClient.post()
.uri(url)
.body(Mono.just(contract), contractType)
.retrieve();
Mono<V> mono = foo.bodyToMono(returnType);
final Mono<U> trans = mono.map(m -> transform.apply(m));
return trans.block();
}
This code works fine in its non-generic form. But when I call this generic method with something like this
requestRunner.<Contract, String, ViewModel>sendRequest(url, contract, v->(String)v.getResult().get("outputString"));
I get an exception:
java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to com.torchai.service.common.ui.ViewModel
at com.torchai.service.orchestration.service.RequestRunner.lambda$sendRequest$0(RequestRunner.java:44)
I'm running version 2.4.5 of SpringBoot, so I don't believe this applies:https://github.com/spring-projects/spring-framework/issues/20574
Just for a little more context, in the example above, ViewModel (generic type <V>
) is the format that the service returns its data. I'm then extracting just the piece I need, in this case a string (generic type <U>
) The lambda function that is passed in gets the relevant string from the Response. But for some reason, the Mono is not being mapped properly to ViewModel. If I take out the map() and just return the ViewModel, it appears to work.
Again, if I do this in a non-generic way, it works fine. I can do the map() step and it properly returns a String
UPDATE
Just want to make it clear that this works fine with a non generic version like this:
public String sendRequest(String url, Contract contract, Function<ViewModel, String> transform) {
ParameterizedTypeReference<Contract> contractType = new ParameterizedTypeReference<Contract>() {};
ParameterizedTypeReference<ViewModel> returnType = new ParameterizedTypeReference<ViewModel>() {};
final WebClient.ResponseSpec foo = webClient.post()
.uri(url)
.body(Mono.just(contract), contractType)
.retrieve();
Mono<ViewModel> mono = foo.bodyToMono(returnType);
final Mono<String> trans = mono.map(m -> transform.apply(m));
return trans.block();
}
It is called this way
requestRunner.<Contract, String, ViewModel>sendRequest(textExtractorUrl, cloudContract, v -> (String) v.getResult().get("outputString"));
It correctly returns a string, which is exactly what I wanted from the generic version
I was able to reproduce this issue locally and started some debugging but it is really hard to see what/where something happens in the webflux code.
The type declaration ViewModel
for V
is only known at the caller but not in the method sendRequest
. It seems that the use of ParameterizedTypeReference<V>
is the problem. In this case V
is just a generic placeholder for the real type, so spring just captures the V
in a ParameterizedTypeReference
and does not know that the response should be deserialized into an instance of ViewModel
. Instead it tries to find a deserializer for the type V
and the closest it could find is the type LinkedHashMap
. So in your case you ended up with a top level LinkedHashMap
(instead of ViewModel
) containing a key result
with a value of type LinkedHashMap
which contains your result entries. Thats why your are getting the ClassCastException
.
I have removed the ParameterizedTypeReference<V>
and used the explicit Class<V>
version instead and it works.
With this version you don't have to put in the generic types by yourself, just provide the parameters to the method and the generics are automatically derived from context.
public <T, U, V> U sendRequest(String url, T contract, Class<V> responseType, Function<V, U> transform)
{
WebClient.ResponseSpec foo = webClient.post()
.uri(url)
.body(Mono.just(contract), contract.getClass())
.retrieve();
Mono<V> mono = foo.bodyToMono(responseType);
Mono<U> trans = mono.map(transform);
return trans.block();
}
Call:
requestRunner.sendRequest(url, contract, ViewModel.class, v -> (String) v.getResult().get("outputString"));
This is my minimal reproducable example if someone wants to do a deeper investigation.
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
DemoApplication.java
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
Controller.java
@RestController
public class Controller
{
private static class ViewModel
{
private LinkedHashMap<String, Object> result = new LinkedHashMap<>();
public LinkedHashMap<String, Object> getResult()
{
return result;
}
public void setResult(LinkedHashMap<String, Object> result)
{
this.result = result;
}
}
@PostMapping("/fetchViewModel")
public ViewModel fetchViewModel()
{
ViewModel result = new ViewModel();
result.getResult().put("outputString", "value");
return result;
}
@GetMapping("/start")
public Mono<String> startSuccess()
{
return this.sendRequest("fetchViewModel", "contract", ViewModel.class, v -> (String) v.getResult().get("outputString"));
}
private <T, U, V> Mono<U> sendRequest(String url, T contract, Class<V> responseType, Function<V, U> transform)
{
WebClient webClient = WebClient.create("http://localhost:8080/");
WebClient.ResponseSpec foo = webClient.post()
.uri(url)
.body(Mono.just(contract), contract.getClass())
.retrieve();
Mono<V> mono = foo.bodyToMono(responseType);
Mono<U> trans = mono.map(transform);
return trans;
}
@GetMapping("/startClassCastException")
public Mono<String> startClassCastException()
{
return this.<String, String, ViewModel> sendRequestClassCastException("fetchViewModel", "contract", v -> (String) v.getResult().get("outputString"));
}
private <T, U, V> Mono<U> sendRequestClassCastException(String url, T contract, Function<V, U> transform)
{
ParameterizedTypeReference<T> contractType = new ParameterizedTypeReference<T>() {};
ParameterizedTypeReference<V> responseType = new ParameterizedTypeReference<V>() {};
WebClient webClient = WebClient.create("http://localhost:8080/");
WebClient.ResponseSpec foo = webClient.post()
.uri(url)
.body(Mono.just(contract), contractType)
.retrieve();
Mono<V> mono = foo.bodyToMono(responseType);
Mono<U> trans = mono.map(transform); // ClassCastException in Lambda
return trans;
}
}