前面介绍了基于HttpURLConnection的网络访问请求,包括GET方式调用接口、POST方式调用接口、下载网络文件、上传本地文件这四种HTTP操作。虽然通过HttpURLConnection能够实现相应的业务功能,但是它的编码过程却有些繁琐,需要时时刻刻注意有关细节,一不留神便会掉到坑里。比如下列编码细节就经常令初学者头痛不已:
1、HttpURLConnection工具独自一人承担了所有的方法实现,分不清哪些方法与请求有关,哪些方法与应答有关;
2、HTTP调用的步骤太多,诸如参数设置、开启连接、写入请求报文、读取应答报文、断开连接这些操作的次序得牢牢记住,一旦弄错顺序就无法正常调用;
3、对于请求报文与应答报文,HttpURLConnection只笼统提供了输出流和输入流,剩下的事全凭开发者自由发挥,害得开发者忙于I/O流与字符串/文件之间的转换工作;
4、服务器返回的应答报文,其数据有可能采用gzip压缩,还可能采取GBK字符编码,然而HttpURLConnection默认情况下却袖手旁观,必须由开发者对数据手工解压和重新编码;
总而言之,HttpURLConnection要求开发者掌握太多的技术细节,容易造成初学者对其望而却步。为此第三方的HTTP框架层出不穷,意图通过简单明了的方法调用来简化HTTP通信编程。Apache旗下的HttpClient便是其中一个佼佼者,它封装了大部分的编码细节,开发者只需书写寥寥数行代码,即可完成常见的HTTP访问操作。当然,Apache的HttpClient毕竟是个外来者,它运用得越广泛,Java的老板Oracle越是觉得不爽,老财主Oracle心想:咱卧榻之侧,岂容他人鼾睡?与其依赖Apache,不如自己动手丰衣足食,于是从Java11开始,JDK新增了自己的HttpClient框架,总算在自力更生的道路上迈开了小小的一步。
Java11的HttpClient体系由三部分组成,分别是表示HTTP客户端的HttpClient、表示HTTP请求过程的HttpRequest、表示HTTP应答过程的HttpResponse。其中HttpClient用于描述通用的客户端连接信息,包括HTTP协议的版本号、HTTP代理、重定向方式、连接超时时间、身份认证、SSL证书等等。下面是创建HTTP客户端对象的代码例子:
// 创建一个自定义的HTTP客户端对象 HttpClient client = HttpClient.newBuilder() .version(Version.HTTP_1_1) // 遵循HTTP协议的1.1版本 .followRedirects(Redirect.NORMAL) // 正常的重定向 .connectTimeout(Duration.ofMillis(5000)) // 连接的超时时间为5秒 .authenticator(Authenticator.getDefault()) // 默认的身份认证 .build();
显然以上的代码例子很啰嗦,对于普通的HTTP连接,一律按照默认的参数就行。于是HTTP客户端对象的创建代码可缩短到如下一行:
// 创建默认的HTTP客户端对象 HttpClient client = HttpClient.newHttpClient();
至于HttpRequest,则用于描述本次网络访问的请求信息,包括对方地址、接口的调用方式(GET还是POST)、请求的超时时间、请求的头部属性等等。下面是创建HTTP请求对象的代码例子:
// 创建一个自定义的HTTP请求对象 HttpRequest request = HttpRequest.newBuilder() .GET() // 调用方式为GET .uri(URI.create(url)) // 待调用的url地址 .header("Accept-Language", "zh-CN") // 设置头部参数,中文文本 .timeout(Duration.ofMillis(5000)) // 请求的超时时间为5秒 .build();
对于一般的GET调用而言,HTTP请求可以使用默认的参数,再把对方地址作为newBuilder方法的输入参数,如此一来HTTP请求对象的创建代码也可缩短到如下一行:
// 创建默认的HTTP请求对象(默认GET调用) HttpRequest request = HttpRequest.newBuilder(URI.create(url)).build();
接着调用HTTP客户端对象的send方法,第一个参数填HTTP请求对象,第二个参数填BodyHandlers.ofString()表示要求返回字符串形式的应答报文,而send方法的返回值便是HttpResponse对象。HttpResponse主要提供了下列三个方法,以便开发者处理应答数据:
statusCode:获取应答的状态码。
body:获取应答报文的内容。
headers:获取应答的所有头部属性。
接下来结合HttpClient、HttpRequest、HttpResponse,很容易写出GET方式的HTTP调用代码,具体代码如下所示:
// 对指定url发起GET调用 private static void testCallGet(String url) { // 创建默认的HTTP客户端对象 HttpClient client = HttpClient.newHttpClient(); // 创建默认的HTTP请求对象(默认GET调用) HttpRequest request = HttpRequest.newBuilder(URI.create(url)).build(); try { // 客户端传递请求信息,且返回字符串形式的应答报文 HttpResponse<String> response = client.send(request, BodyHandlers.ofString()); // 获取应答的所有头部属性 HttpHeaders headers = response.headers(); // 打印HTTP调用的应答内容长度、内容类型、压缩方式 System.out.println( String.format("应答内容长度=%s, 内容类型=%s, 压缩方式=%s", headers.firstValue("Content-Length").orElse(null), headers.firstValue("Content-Type").orElse(null), headers.firstValue("Content-Encoding").orElse(null)) ); // 打印HTTP调用的应答状态码和应答报文 System.out.println( String.format("应答状态码=%d, 应答报文=%s", response.statusCode(), response.body()) ); } catch (Exception e) { e.printStackTrace(); } }
然后在外部调用上面的testCallGet方法,以股指查询的接口地址为例,查询上证指数的调用代码如下:
testCallGet("https://hq.sinajs.cn/list=s_sh000001");
运行以上的股指查询代码,观察到以下的查询日志,可见HttpClient已经自动完成了中文字符的GBK编码。
应答内容长度=75, 内容类型=application/javascript; charset=GBK, 压缩方式=null 应答状态码=200, 应答报文=var hq_str_s_sh000001="上证指数,3244.8103,-1.7611,-0.05,5045184,50643124";
利用HttpClient发起POST方式的调用过程类似GET方式,唯一的区别在于:创建HTTP请求对象之时要调用POST方法并传入请求报文。下面是采取POST方式访问服务地址的HttpClient代码例子:
// 对指定url发起POST调用 private static void testCallPost(String url, String body) { System.out.println("请求报文="+body); // 创建默认的HTTP客户端对象 HttpClient client = HttpClient.newHttpClient(); // 创建一个自定义的HTTP请求对象 HttpRequest request = HttpRequest.newBuilder(URI.create(url)) // 待调用的url地址 .POST(BodyPublishers.ofString(body)) // 调用方式为POST,且请求报文为字符串 .header("Content-Type", "application/json") // 设置头部参数,内容类型为json .build(); try { // 客户端传递请求信息,且返回字符串形式的应答报文 HttpResponse<String> response = client.send(request, BodyHandlers.ofString()); // 打印HTTP调用的应答状态码和应答报文 System.out.println( String.format("应答状态码=%d, 应答报文=%s", response.statusCode(), response.body()) ); } catch (Exception e) { e.printStackTrace(); } }
接着由外部调用上面的testCallPost方法,这里访问的是本机的HTTP服务,交互报文为json格式,具体代码如下所示:
testCallPost("http://localhost:8080/NetServer/checkUpdate", "{\"package_list\":[{\"package_name\":\"com.qiyi.video\"}]}");
运行以上的服务访问代码,观察到以下的接口日志,可见HttpClient正确完成了POST方式的接口调用。
请求报文={"package_list":[{"package_name":"com.qiyi.video"}]} 应答状态码=200, 应答报文={"package_list":[{"package_name":"com.qiyi.video","download_url":"https://3g.lenovomm.com/w3g/yydownload/com.qiyi.video/60020","new_version":"10.2.0"}]}
更多Java技术文章参见《Java开发笔记(序)章节目录》