使用 Java 11 HttpClient 和带有 Jackson 的自定义 BodyHandler 反序列化 JSON 停止并且不会继续
Deserializing JSON using Java 11 HttpClient and custom BodyHandler with Jackson halts and will not proceed
我在直接使用 Java 11 HttpClient::send
和自定义 HttpResponse.BodyHandler
将 JSON 反序列化为自定义对象时遇到问题。我在回答 .
时遇到了这个问题
我正在使用的版本:
- OpenJDK 11
- 杰克逊 2.9.9.3
我创建了一个简单的泛型 JsonBodyHandler
class 它实现了 HttpResponse.BodyHandler
:
public class JsonBodyHandler<W> implements HttpResponse.BodyHandler<W> {
private final Class<W> wClass;
public JsonBodyHandler(Class<W> wClass) {
this.wClass = wClass;
}
@Override
public HttpResponse.BodySubscriber<W> apply(HttpResponse.ResponseInfo responseInfo) {
return asJSON(wClass);
}
}
asJSON
方法定义为:
public static <W> HttpResponse.BodySubscriber<W> asJSON(Class<W> targetType) {
HttpResponse.BodySubscriber<String> upstream = HttpResponse.BodySubscribers.ofString(StandardCharsets.UTF_8);
return HttpResponse.BodySubscribers.mapping(
upstream,
(String body) -> {
try {
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.readValue(body, targetType);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
}
所以它 returns 一个自定义的 HttpResponse.BodySubscriber
获取正文 String
然后应用从 JSON 到给定 targetType
的映射
测试代码:
public static void main(String[] args) throws URISyntaxException, IOException, InterruptedException {
HttpRequest request = HttpRequest.newBuilder(new URI("https://jsonplaceholder.typicode.com/todos/1"))
.header("Accept", "application/json")
.build();
Model model = HttpClient.newHttpClient()
.send(request, new JsonBodyHandler<>(Model.class))
.body();
System.out.println(model);
}
还有 Model
class :
public class Model {
private String userId;
private String id;
private String title;
private boolean completed;
//getters setters constructors toString
}
输出符合预期:
Model{userId='1', id='1', title='delectus aut autem', completed=false}
但是,当我将 asJSON
方法更改为首先读取 InputStream
而不是 String
时:
public static <W> HttpResponse.BodySubscriber<W> asJSON(Class<W> targetType) {
HttpResponse.BodySubscriber<InputStream> upstream = HttpResponse.BodySubscribers.ofInputStream();
return HttpResponse.BodySubscribers.mapping(
upstream,
(InputStream is) -> {
try (InputStream stream = is) {
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.readValue(stream, targetType);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
}
它在使用 ObjectMapper
调用读取值后挂起并且它没有继续(我已经检查它成功地从端点获得响应,状态代码是 200)但是它只是挂起。有谁知道可能是什么问题?
我刚刚在官方 OpenJDK 错误站点上找到 which has the same problem but with GZIPInputStream
. It turns out that HttpResponse.BodySubscribers.mapping
is buggy and it does not work as documented. Here is the link。它已针对 OpenJDK 13 修复。因此,一种解决方法是使用 HttpResponse.BodySubscribers::ofString
而不是 HttpResponse.BodySubscribers::ofInputStream
作为 HttpResponse.BodySubscribers::mapping
的上游 - 在我的问题中显示了如何执行此操作。
或者更好的解决方案,正如@daniel 在评论中指出的那样,是 return a Supplier
而不是模型 class:
public static <W> HttpResponse.BodySubscriber<Supplier<W>> asJSON(Class<W> targetType) {
HttpResponse.BodySubscriber<InputStream> upstream = HttpResponse.BodySubscribers.ofInputStream();
return HttpResponse.BodySubscribers.mapping(
upstream,
inputStream -> toSupplierOfType(inputStream, targetType));
}
public static <W> Supplier<W> toSupplierOfType(InputStream inputStream, Class<W> targetType) {
return () -> {
try (InputStream stream = inputStream) {
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.readValue(stream, targetType);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
};
}
JsonBodyHandler
也用Supplier
:
public class JsonBodyHandler<W> implements HttpResponse.BodyHandler<Supplier<W>> {
private final Class<W> wClass;
public JsonBodyHandler(Class<W> wClass) {
this.wClass = wClass;
}
@Override
public HttpResponse.BodySubscriber<Supplier<W>> apply(HttpResponse.ResponseInfo responseInfo) {
return asJSON(wClass);
}
}
然后我们可以这样称呼它:
public static void main(String[] args) throws URISyntaxException, IOException, InterruptedException {
HttpRequest request = HttpRequest.newBuilder(new URI("https://jsonplaceholder.typicode.com/todos/1"))
.header("Accept", "application/json")
.build();
Model model = HttpClient.newHttpClient()
.send(request, new JsonBodyHandler<>(Model.class))
.body()
.get();
System.out.println(model);
}
中甚至提倡这样做
The mapping function is executed using the client's executor, and can therefore be used to map any response body type, including blocking InputStream
. However, performing any blocking operation in the mapper function runs the risk of blocking the executor's thread for an unknown amount of time (at least until the blocking operation finishes), which may end up starving the executor of available threads. Therefore, in the case where mapping to the desired type might block (e.g. by reading on the InputStream), then mapping to a Supplier
of the desired type and deferring the blocking operation until Supplier::get
is invoked by the caller's thread should be preferred.
在科特林中:
val objectMapper = ObjectMapper()
fun jsonBodyHandler(): HttpResponse.BodyHandler<JsonNode> {
val jsonNodeSubscriber = BodySubscribers.mapping(BodySubscribers.ofByteArray()) {
objectMapper.readTree(it)
}
return HttpResponse.BodyHandler { jsonNodeSubscriber }
}
使用 Jackson TypeReference
我可以单独使用泛型来完成,不需要多余的 Class<T>
参数。
package com.company;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.net.http.HttpResponse;
import java.util.function.Supplier;
public class JsonBodyHandler<W> implements HttpResponse.BodyHandler<Supplier<W>> {
public JsonBodyHandler() {
}
private static <W> Supplier<W> toSupplierOfType(InputStream inputStream) {
return () -> {
try (InputStream stream = inputStream) {
return new ObjectMapper().readValue(stream, new TypeReference<W>() {
});
} catch (IOException e) {
throw new UncheckedIOException(e);
}
};
}
private static <W> HttpResponse.BodySubscriber<Supplier<W>> asJSON() {
return HttpResponse.BodySubscribers.mapping(
HttpResponse.BodySubscribers.ofInputStream(),
JsonBodyHandler::toSupplierOfType);
}
@Override
public HttpResponse.BodySubscriber<Supplier<W>> apply(HttpResponse.ResponseInfo responseInfo) {
return JsonBodyHandler.asJSON();
}
}
并在使用中
package com.company;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.time.Duration;
import java.util.List;
public class Main {
private static final HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://myApi"))
.timeout(Duration.ofMinutes(1))
.header("Content-Type", "application/json")
.build();
private static final HttpClient client = HttpClient.newBuilder().build();
public static void main(String[] args) throws InterruptedException {
client.sendAsync(Main.request, new JsonBodyHandler<List<MyDto>>())
.thenAccept(response -> {
List<MyDto> myDtos = response.body().get();
System.out.println(myDtos);
}).join();
}
}
我在直接使用 Java 11 HttpClient::send
和自定义 HttpResponse.BodyHandler
将 JSON 反序列化为自定义对象时遇到问题。我在回答
我正在使用的版本:
- OpenJDK 11
- 杰克逊 2.9.9.3
我创建了一个简单的泛型 JsonBodyHandler
class 它实现了 HttpResponse.BodyHandler
:
public class JsonBodyHandler<W> implements HttpResponse.BodyHandler<W> {
private final Class<W> wClass;
public JsonBodyHandler(Class<W> wClass) {
this.wClass = wClass;
}
@Override
public HttpResponse.BodySubscriber<W> apply(HttpResponse.ResponseInfo responseInfo) {
return asJSON(wClass);
}
}
asJSON
方法定义为:
public static <W> HttpResponse.BodySubscriber<W> asJSON(Class<W> targetType) {
HttpResponse.BodySubscriber<String> upstream = HttpResponse.BodySubscribers.ofString(StandardCharsets.UTF_8);
return HttpResponse.BodySubscribers.mapping(
upstream,
(String body) -> {
try {
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.readValue(body, targetType);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
}
所以它 returns 一个自定义的 HttpResponse.BodySubscriber
获取正文 String
然后应用从 JSON 到给定 targetType
的映射
测试代码:
public static void main(String[] args) throws URISyntaxException, IOException, InterruptedException {
HttpRequest request = HttpRequest.newBuilder(new URI("https://jsonplaceholder.typicode.com/todos/1"))
.header("Accept", "application/json")
.build();
Model model = HttpClient.newHttpClient()
.send(request, new JsonBodyHandler<>(Model.class))
.body();
System.out.println(model);
}
还有 Model
class :
public class Model {
private String userId;
private String id;
private String title;
private boolean completed;
//getters setters constructors toString
}
输出符合预期:
Model{userId='1', id='1', title='delectus aut autem', completed=false}
但是,当我将 asJSON
方法更改为首先读取 InputStream
而不是 String
时:
public static <W> HttpResponse.BodySubscriber<W> asJSON(Class<W> targetType) {
HttpResponse.BodySubscriber<InputStream> upstream = HttpResponse.BodySubscribers.ofInputStream();
return HttpResponse.BodySubscribers.mapping(
upstream,
(InputStream is) -> {
try (InputStream stream = is) {
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.readValue(stream, targetType);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
}
它在使用 ObjectMapper
调用读取值后挂起并且它没有继续(我已经检查它成功地从端点获得响应,状态代码是 200)但是它只是挂起。有谁知道可能是什么问题?
我刚刚在官方 OpenJDK 错误站点上找到 GZIPInputStream
. It turns out that HttpResponse.BodySubscribers.mapping
is buggy and it does not work as documented. Here is the link。它已针对 OpenJDK 13 修复。因此,一种解决方法是使用 HttpResponse.BodySubscribers::ofString
而不是 HttpResponse.BodySubscribers::ofInputStream
作为 HttpResponse.BodySubscribers::mapping
的上游 - 在我的问题中显示了如何执行此操作。
或者更好的解决方案,正如@daniel 在评论中指出的那样,是 return a Supplier
而不是模型 class:
public static <W> HttpResponse.BodySubscriber<Supplier<W>> asJSON(Class<W> targetType) {
HttpResponse.BodySubscriber<InputStream> upstream = HttpResponse.BodySubscribers.ofInputStream();
return HttpResponse.BodySubscribers.mapping(
upstream,
inputStream -> toSupplierOfType(inputStream, targetType));
}
public static <W> Supplier<W> toSupplierOfType(InputStream inputStream, Class<W> targetType) {
return () -> {
try (InputStream stream = inputStream) {
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.readValue(stream, targetType);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
};
}
JsonBodyHandler
也用Supplier
:
public class JsonBodyHandler<W> implements HttpResponse.BodyHandler<Supplier<W>> {
private final Class<W> wClass;
public JsonBodyHandler(Class<W> wClass) {
this.wClass = wClass;
}
@Override
public HttpResponse.BodySubscriber<Supplier<W>> apply(HttpResponse.ResponseInfo responseInfo) {
return asJSON(wClass);
}
}
然后我们可以这样称呼它:
public static void main(String[] args) throws URISyntaxException, IOException, InterruptedException {
HttpRequest request = HttpRequest.newBuilder(new URI("https://jsonplaceholder.typicode.com/todos/1"))
.header("Accept", "application/json")
.build();
Model model = HttpClient.newHttpClient()
.send(request, new JsonBodyHandler<>(Model.class))
.body()
.get();
System.out.println(model);
}
中甚至提倡这样做
The mapping function is executed using the client's executor, and can therefore be used to map any response body type, including blocking
InputStream
. However, performing any blocking operation in the mapper function runs the risk of blocking the executor's thread for an unknown amount of time (at least until the blocking operation finishes), which may end up starving the executor of available threads. Therefore, in the case where mapping to the desired type might block (e.g. by reading on the InputStream), then mapping to aSupplier
of the desired type and deferring the blocking operation untilSupplier::get
is invoked by the caller's thread should be preferred.
在科特林中:
val objectMapper = ObjectMapper()
fun jsonBodyHandler(): HttpResponse.BodyHandler<JsonNode> {
val jsonNodeSubscriber = BodySubscribers.mapping(BodySubscribers.ofByteArray()) {
objectMapper.readTree(it)
}
return HttpResponse.BodyHandler { jsonNodeSubscriber }
}
使用 Jackson TypeReference
我可以单独使用泛型来完成,不需要多余的 Class<T>
参数。
package com.company;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.net.http.HttpResponse;
import java.util.function.Supplier;
public class JsonBodyHandler<W> implements HttpResponse.BodyHandler<Supplier<W>> {
public JsonBodyHandler() {
}
private static <W> Supplier<W> toSupplierOfType(InputStream inputStream) {
return () -> {
try (InputStream stream = inputStream) {
return new ObjectMapper().readValue(stream, new TypeReference<W>() {
});
} catch (IOException e) {
throw new UncheckedIOException(e);
}
};
}
private static <W> HttpResponse.BodySubscriber<Supplier<W>> asJSON() {
return HttpResponse.BodySubscribers.mapping(
HttpResponse.BodySubscribers.ofInputStream(),
JsonBodyHandler::toSupplierOfType);
}
@Override
public HttpResponse.BodySubscriber<Supplier<W>> apply(HttpResponse.ResponseInfo responseInfo) {
return JsonBodyHandler.asJSON();
}
}
并在使用中
package com.company;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.time.Duration;
import java.util.List;
public class Main {
private static final HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://myApi"))
.timeout(Duration.ofMinutes(1))
.header("Content-Type", "application/json")
.build();
private static final HttpClient client = HttpClient.newBuilder().build();
public static void main(String[] args) throws InterruptedException {
client.sendAsync(Main.request, new JsonBodyHandler<List<MyDto>>())
.thenAccept(response -> {
List<MyDto> myDtos = response.body().get();
System.out.println(myDtos);
}).join();
}
}