3

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

Peter Kronenberg
  • 878
  • 10
  • 32
  • 1
    I have no real idea why this `ClassCastException` is happening but what I would do in this case is, set an exception breakpoint for `ClassCastException` and if the debugger stops I go backwards the call stack and try to find out where the `LinkedHashMap` instance is coming from. An alternative to the exception breakpoint would be to change your generic signature to `Object` instead of `ViewModel`. Then you can set a breakpoint in your lambda function without directly raising the `ClassCastExcetion`. Now you can verify if you get a `LinkedHashMap` and if yes go backwards the callstack again. – Ogod Jun 05 '21 at 16:11
  • Well, I Know the LinkedHashMap is coming from the Mono. I don't know why it's not doing what it normally does in bodyToMono(). But I'll try that and see what it tells me – Peter Kronenberg Jun 05 '21 at 16:36
  • `return trans.block();`? – K.Nicholas Jun 05 '21 at 17:58
  • 1
    @PeterKronenberg I have reworked my answer. You can give it a try. – Ogod Jun 06 '21 at 09:00

2 Answers2

1

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;
    }
}
Ogod
  • 878
  • 1
  • 7
  • 15
  • 1
    `.block()` won't run in a WebFlux application. – K.Nicholas Jun 05 '21 at 18:29
  • @K.Nicholas yes you are right but the questioner says that the code works fine if he doesn't go the generic way, so I assume that he is not in an reactive spring environment and just uses `WebClient` as a replacement for `RestTemplate`. – Ogod Jun 05 '21 at 18:32
  • Point taken, but I am loath to encourage `.block()` in any fashion. You can see I don't use it in my example. – K.Nicholas Jun 05 '21 at 18:35
  • 1
    Not to mention the comment `I'm running version 2.4.5 of SpringBoot`. – K.Nicholas Jun 05 '21 at 18:37
  • The application I'm writing is using SpringBoot. Some of the calls are synchronous and some are asynchronous. For the synchronous calls, I just use block(). Am I not using it correctly? – Peter Kronenberg Jun 05 '21 at 21:14
0

This works so you're probably not getting a ViewModel back from your webClient body but rather the map of parameters (or something) from the ViewModel.

public class Play {
    private ViewModel viewModel = new ViewModel(); 
    public static void main(String[] args) {
        new Play().run();
    }
    static class ViewModel {
        private Map<String, Object> result;
        public ViewModel() {
            result = new LinkedHashMap<>(); 
            result.put("outputString", "acb");
        }
        public Map<String, Object> getResult() {
            return result;
        }
    }
    private void run() {
        this.<String, String, ViewModel>sendRequest("http", "contract", v->(String)v.getResult().get("outputString"))
        .subscribe(System.out::println);
    }
    public <T, U, V> Mono<U> sendRequest(String url, T contract, Function<V, U> transform) {
        @SuppressWarnings("unchecked")
        Mono<V> mono =  Mono.just((V)viewModel);

      final Mono<U> trans = mono.map(m -> transform.apply(m));
      return trans; 
    }
}

I.e., if I change to:

        @SuppressWarnings("unchecked")
        Mono<V> mono =  Mono.just((V)viewModel.getResult());

Then I get

reactor.core.Exceptions$ErrorCallbackNotImplemented: java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to fluxjunk.Play$ViewModel Caused by: java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to fluxjunk.Play$ViewModel at fluxjunk.Play.lambda$2(Play.java:32)

K.Nicholas
  • 10,956
  • 4
  • 46
  • 66
  • I'm not quite sure what this is supposed to show. Clearly in the second example, you are trying to cast `viewModel.getResult()` into a `ViewModel` and that doesn't work. Updating my issue to show a working non-generic version and the non-working generic version – Peter Kronenberg Jun 05 '21 at 23:38