发送HTTP请求是现代编程中的核心功能,也是我们学习一种编程语言时最先想做的事。在Java中,有多种方式实现此功能,比如JDK或第三方库,这篇文章将会介绍我所知的Java中HTTP客户端,如果你使用过其他的,那很好,请告诉我。这篇文章将会涉及:
JDK中:
- HttpURLConnection
- HttpClient
流行的第三方库:
- ApacheHttpClient
- OkHttp
- Retrofit
我将会使用 NASA APIs 中每天一图Astronomy Picture of the Day做为示例,所有代码都是基于Java11, 并且在GitHub 上。
JDK中API发送HTTP请求
JDK自从1.1就有一个发送HTTP请求的客户端,Java11中新增了一个客户端。如果项目中不想引入第三方Jar包,使用JDK中自带API是个好的选择。
Java 1.1 HttpURLConnection
首先,请记住,类名首字母要大写。让我们闭上眼睛,回到1997,泰坦尼克在票房上大卖并激起了人们上千种表情,Spice Girls专辑也大卖,但年度最大新闻却是Java1.1中加入HttpURLConnection。下面试是使用HttpURLConnection发送Get请求获取每日一图的代码片段:
// Create a neat value object to hold the URL
URL url = new URL("https://api.nasa.gov/planetary/apod?api_key=DEMO_KEY");
// Open a connection(?) on the URL(??) and cast the response(???)
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
// Now it's "open", we can set the request method, headers etc.
connection.setRequestProperty("accept", "application/json");
// This line makes the request
InputStream responseStream = connection.getInputStream();
// Manually converting the response body InputStream to APOD using Jackson
ObjectMapper mapper = new ObjectMapper();
APOD apod = mapper.readValue(responseStream, APOD.class);
// Finally we have the response
System.out.println(apod.title);
代码中似乎有很多冗余,并且有些代码顺序也让人困惑,比如为什么是在打开URL链接后设置Header。如果使用Post发送更复杂的请求或设置超时时间等,也有这些功能,但却非常不直观。
那什么时候使用HttpURLConnection呢?如果你支持老版本Java, 并且不能依赖第三方库可使用HttpURLConnection。尽管只有少部分开发者会遇到这种情况,但老的代码库中可能存在HttpURLConnection,现代化的方式,请向下看。
Java 11 HttpClient
自HttpURLConnection加入JDK二十多年后,《黑豹》已上市,JDK中加入了一种新的HTTP客户端java.net.http.HttpClient
。这个API更合乎逻辑,并且可以处理HTTP/2、Websockets,同时不仅能发送同步请求,也能借助CompletableFuture发送异步请求。
当发送一个HTTP请求时,大多数时候我们想要读取响应体,没有提供方便实现此功能的库无法吸引我。HttpClient 可使用BodyHandler将响应体转换成自定义类,系统中内置了一些BodyHandler,比如string、byte[]适用字节流,stream那行分割响应,等等。你也可以自定义BodyHandler,比如系统中并没有解析JSON的BodyHandler,参照Java Docs示例,基于Jackson,我实现了一个(here) 。它返回一个APOD class的Supplier,可调用get()获取结果。
同步请求:
// create a client
var client = HttpClient.newHttpClient();
// create a request
var request = HttpRequest.newBuilder(
URI.create("https://api.nasa.gov/planetary/apod?api_key=DEMO_KEY"))
.header("accept", "application/json")
.build();
// use the client to send the request
var response = client.send(request, new JsonBodyHandler<>(APOD.class));
// the response:
System.out.println(response.body().get().title);
异步请求时构造client和request的方式相同,只需用sendAsync替换send即可。
// use the client to send the request
var responseFuture = client.sendAsync(request, new JsonBodyHandler<>(APOD.class));
// We can do other things here while the request is in-flight
// This blocks until the request is complete
var response = responseFuture.get();
// the response:
System.out.println(response.body().get().title);
第三方HTTP客户端库
如果JDK内置的功能无法满足你的需求,不用担心,还有很多第三方库可使用。
Apache HttpClient
Apache Software Foundation的 HTTP客户端 存在好长时间了,它被广泛使用,很多更高层次的库使用了它。它的历史有点令人迷惑,老的Commons HttpClient 停止开发,新版HttpClient属于HttpComponents 项目,版本5.0于2020年初发布,并且支持HTTP/2, 既能发送同步请求,也能发送异步请求。
总体来开此API相对底层,需用户自己实现很多功能。下面代码调用NASA API, 似乎很好用,但是我忽略了很多在生产环境中所需的错误处理,同样,我也要使用Jackson 来解析JSON响应体。也许,你还需配置一个日志框架避免告警输出到stdout, 尽管这不是个大问题,但同样使我厌烦。不管怎样,下面是代码:
ObjectMapper mapper = new ObjectMapper();
try (CloseableHttpClient client = HttpClients.createDefault()) {
HttpGet request = new HttpGet("https://api.nasa.gov/planetary/apod?api_key=DEMO_KEY");
APOD response = client.execute(request, httpResponse ->
mapper.readValue(httpResponse.getEntity().getContent(), APOD.class));
System.out.println(response.title);
}
OkHttp
OkHttp是Square 开发的HTTP客户端,它有许多有用的功能,比如自动处理GZIP,缓存响应,发生网络故障时重试或降级至其他机器,同样它也支持HTTP/2和WebSocket, 他的API很简洁,依然没有内置解析JSON响应体的功能,所以我添加了用Jackson解析JSON的代码:
ObjectMapper mapper = new ObjectMapper();
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url("https://api.nasa.gov/planetary/apod?api_key=DEMO_KEY")
.build(); // defaults to GET
Response response = client.newCall(request).execute();
APOD apod = mapper.readValue(response.body().byteStream(), APOD.class);
System.out.println(apod.title);
这很好, 但构建在OkHttp之上的Retrofit更强大。
Retrofit
Retrofit 是Square构建在OkHttp之上的另外一个库。除了拥有OkHttp本身的功能外,它添加了一种方式可构建抽象了HTTP细节并拥有友好API的Java类。
首先,我们创建一个接口声明我们想调用APOD API的方法,在这个方法上使用注解定义HTTP请求:
public interface APODClient {
@GET("/planetary/apod")
@Headers("accept: application/json")
CompletableFuture<APOD> getApod(@Query("api_key") String apiKey);
}
返回类型CompletableFuture表明这个方法是个异步请求,Square提供更多 adapters或者也可以自定义。我很喜欢这种方式,使用这样的接口在测试时可方便mock客户端。
声明接口后,可使用Retrofit创建一个实现类发送请求,可更换基础URL方便集成测试。生成具体实现类,代码如下:
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.nasa.gov")
.addConverterFactory(JacksonConverterFactory.create())
.build();
APODClient apodClient = retrofit.create(APODClient.class);
CompletableFuture<APOD> response = apodClient.getApod("DEMO_KEY");
// do other stuff here while the request is in-flight
APOD apod = response.get();
System.out.println(apod.title);
API鉴权
如果有多个接口都需增加一个API key, 这可通过在OkHttpClient基础上增加HttpInterceptor来实现,自定义客户端也可基于Retrofit.Builder。下面是自定义客户端代码:
private OkHttpClient clientWithApiKey(String apiKey) {
return new OkHttpClient.Builder()
.addInterceptor(chain -> {
Request originalRequest = chain.request();
HttpUrl newUrl = originalRequest.url().newBuilder()
.addQueryParameter("api_key", apiKey).build();
Request request = originalRequest.newBuilder().url(newUrl).build();
return chain.proceed(request);
}).build();
}
除了简单场景,我喜欢这种API,通过依赖注入生成类来进行远程API调用,并且可通过Retrofit生成自定义OkHttp客户端,这种方式很好。
Java中其他HTTP客户端
自从在Twitter 上发表此篇文章后,我很高兴地看到人们讨论他们使用的HTTP客户端。如果上面那些并不是你想要的,可看看下面这些建议:
- REST Assured - 用于测试REST 服务的一个HTTP客户端,流式风格构建请求, 并且可设置响应断言.
- cvurl - 包装Java 11 HttpClient,可避免特殊场景下构建复杂请求.
- Feign - 类似Retrofit, Feign可基于注解接口生成类. Feign可灵活发送请求和读取响应,度量性能,重试等等。
- Spring RestTemplate (同步) and WebClient (异步) 客户端 - 如果项目中使用了Spring,使用它能很好整合到系统中. Baeldung 有一篇比较它们的文章.
- MicroProfile Rest Client - 另外一个从注解接口生成类的客户端,它很有趣因为可使用相同接口生成服务端,并保证客户端和服务端兼容。如果同时构建客户端和服务端,可以选择它。
Java中有很多HTTP客户端库可供选择 - 简单场景推荐使用内置的 java.net.http.HttpClient
. 复杂场景或在大型应用中想使用Java类抽象HTTP API, 可考虑Retrofit 或Feign. 也可魔改, 我很期待看到你的创新!