使用 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 反序列化为自定义对象时遇到问题。我在回答 .

时遇到了这个问题

我正在使用的版本:

我创建了一个简单的泛型 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);

}

OpenJDK 13 docs):

中甚至提倡这样做

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();
    }
}