SpringBoot - WebFlux 获取 ClassCastException 的通用函数
SpringBoot - Generic function for WebFlux getting ClassCastException
我正在尝试编写一个通用函数来执行一些 Webflux 操作,但我遇到了一个我无法理解的 class 转换异常
* @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();
}
此代码在其非通用形式下运行良好。但是当我用这样的东西调用这个通用方法时
requestRunner.<Contract, String, ViewModel>sendRequest(url, contract, v->(String)v.getResult().get("outputString"));
我遇到异常:
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[=12=](RequestRunner.java:44)
我是 运行 SpringBoot 2.4.5 版本,所以我认为这不适用:https://github.com/spring-projects/spring-framework/issues/20574
只是为了提供更多上下文,在上面的示例中,ViewModel(通用类型 <V>
)是服务 return 其数据的格式。然后我只提取我需要的部分,在本例中是一个字符串(通用类型 <U>
)传入的 lambda 函数从响应中获取相关字符串。但出于某种原因,Mono 没有正确映射到 ViewModel。如果我取出 map() 和 return ViewModel,它似乎可以工作。
同样,如果我以非通用方式执行此操作,则效果很好。我可以执行 map() 步骤并且它正确 returns a String
更新
只是想说明一下,这适用于像这样的非通用版本:
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();
}
是这样叫的
requestRunner.<Contract, String, ViewModel>sendRequest(textExtractorUrl, cloudContract, v -> (String) v.getResult().get("outputString"));
它正确地return是一个字符串,这正是我想要的通用版本
这行得通,因此您可能不会从 webClient 主体返回 ViewModel
,而是从 ViewModel
.[=16 返回 map
参数(或其他内容) =]
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;
}
}
即,如果我更改为:
@SuppressWarnings("unchecked")
Mono<V> mono = Mono.just((V)viewModel.getResult());
然后我得到
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(Play.java:32)
我能够在本地重现这个问题并开始一些调试,但是很难看到 what/where webflux 代码中发生了什么。
V
的类型声明 ViewModel
仅在调用方已知,但在方法 sendRequest
中未知。似乎使用 ParameterizedTypeReference<V>
是问题所在。在这种情况下 V
只是真实类型的通用占位符,因此 spring 只是捕获 ParameterizedTypeReference
中的 V
并且不知道应该将响应反序列化为ViewModel
的实例。相反,它会尝试为类型 V
找到反序列化器,而它能找到的最接近的是类型 LinkedHashMap
。因此,在您的情况下,您最终得到了一个顶级 LinkedHashMap
(而不是 ViewModel
),其中包含一个键 result
,其值类型为 LinkedHashMap
,其中包含您的结果条目。这就是为什么你得到 ClassCastException
.
我删除了 ParameterizedTypeReference<V>
并改用显式 Class<V>
版本,它可以正常工作。
使用此版本,您不必自己输入泛型类型,只需向方法提供参数,泛型就会自动从上下文中派生。
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();
}
致电:
requestRunner.sendRequest(url, contract, ViewModel.class, v -> (String) v.getResult().get("outputString"));
如果有人想进行更深入的调查,这是我最小的可重现示例。
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;
}
}
我正在尝试编写一个通用函数来执行一些 Webflux 操作,但我遇到了一个我无法理解的 class 转换异常
* @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();
}
此代码在其非通用形式下运行良好。但是当我用这样的东西调用这个通用方法时
requestRunner.<Contract, String, ViewModel>sendRequest(url, contract, v->(String)v.getResult().get("outputString"));
我遇到异常:
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[=12=](RequestRunner.java:44)
我是 运行 SpringBoot 2.4.5 版本,所以我认为这不适用:https://github.com/spring-projects/spring-framework/issues/20574
只是为了提供更多上下文,在上面的示例中,ViewModel(通用类型 <V>
)是服务 return 其数据的格式。然后我只提取我需要的部分,在本例中是一个字符串(通用类型 <U>
)传入的 lambda 函数从响应中获取相关字符串。但出于某种原因,Mono 没有正确映射到 ViewModel。如果我取出 map() 和 return ViewModel,它似乎可以工作。
同样,如果我以非通用方式执行此操作,则效果很好。我可以执行 map() 步骤并且它正确 returns a String
更新
只是想说明一下,这适用于像这样的非通用版本:
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();
}
是这样叫的
requestRunner.<Contract, String, ViewModel>sendRequest(textExtractorUrl, cloudContract, v -> (String) v.getResult().get("outputString"));
它正确地return是一个字符串,这正是我想要的通用版本
这行得通,因此您可能不会从 webClient 主体返回 ViewModel
,而是从 ViewModel
.[=16 返回 map
参数(或其他内容) =]
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;
}
}
即,如果我更改为:
@SuppressWarnings("unchecked")
Mono<V> mono = Mono.just((V)viewModel.getResult());
然后我得到
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(Play.java:32)
我能够在本地重现这个问题并开始一些调试,但是很难看到 what/where webflux 代码中发生了什么。
V
的类型声明 ViewModel
仅在调用方已知,但在方法 sendRequest
中未知。似乎使用 ParameterizedTypeReference<V>
是问题所在。在这种情况下 V
只是真实类型的通用占位符,因此 spring 只是捕获 ParameterizedTypeReference
中的 V
并且不知道应该将响应反序列化为ViewModel
的实例。相反,它会尝试为类型 V
找到反序列化器,而它能找到的最接近的是类型 LinkedHashMap
。因此,在您的情况下,您最终得到了一个顶级 LinkedHashMap
(而不是 ViewModel
),其中包含一个键 result
,其值类型为 LinkedHashMap
,其中包含您的结果条目。这就是为什么你得到 ClassCastException
.
我删除了 ParameterizedTypeReference<V>
并改用显式 Class<V>
版本,它可以正常工作。
使用此版本,您不必自己输入泛型类型,只需向方法提供参数,泛型就会自动从上下文中派生。
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();
}
致电:
requestRunner.sendRequest(url, contract, ViewModel.class, v -> (String) v.getResult().get("outputString"));
如果有人想进行更深入的调查,这是我最小的可重现示例。
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;
}
}