三年多以前写过一个HTTP 请求类,然后又将其改进为链式风格的调用方式。虽然可以实现需求,基本上也没用重复的逻辑,但是编码上总是觉得怪怪的,当时也说不上哪里不对劲,尽管逻辑没错能实现,然而就是感觉谈不上“优雅”。那时水平有限,想不出办法也就没去专研了。
应该说,现在的 Java 8 的函数式风格给予了我完全不一样的灵感。使用 lambda(匿名函数),与使用普通 Java 函数(方法),首先它更轻量级的,更灵活,于是能够易于表达“我想做什么”,而至于“我怎么做”那部分,能省则省,不要我重复写,也不要然让我去调用(当然前提你要封装好,使用 FP 这“武器”来封装),好比简单的 for 语句,当使用函数式风格之后,封装了 for 逻辑,允许 for 中间部分的逻辑形成于 lambda,这一部分的 lambda 即是属于“我想做什么”,而代表“我怎么做”的那个 for 部分,却被封装起来,外界不会容易看到,而且 lambda 本身语句精简,不会造成 Java 语句冗长啰嗦。
理论上,即使在 Java 8 之前,上述目的都可以通过写就一个个 interface,然后传入一个个回调函数来完成,好比 Swing/Android 的事件处理,乃典型 interface 应用。但那实在太啰嗦,敲代码的成本太高,没人会如此干的。Java 需要一个更简练的语法去做 interface 的事情,于是 FP 的 lambda 被提出并加入到 Java 8 了,同时那也是大趋势使然。实事求是地说,与其说 lambda 是代替品,不如说是新思想的落地实践(当然 FP 思想 N 久之前在学术上已经被提出来了)。而且 Java 8 的函数接口,是类型系统与 FP 一次不错的“联婚”,能较好地对 lambda 进行类型约束,加之泛型的使用,虽有约束但也不失灵活——“一柔一刚”——这是在弱类型的 FP 语言(如 JavaScript)所不能体验的。
总之,FP 带来的好处多多,令 Java 语言更精炼而不是“啰嗦”,而且,我个人收获的价值,某个程度来说也能消灭代码重复。
上面说了那么多,现在才进入“实战环节”。发起 HTTP 请求,是 HttpURLConnection 干的事情,至于底层 Socket 怎么干,我们就不管啦。
/** * HttpURLConnection 工厂函数 * * @param url 请求目的地址 * @return HttpURLConnection 对象 */ public static HttpURLConnection initHttpConnection(String url) { URL httpUrl = null; try { httpUrl = new URL(url); } catch (MalformedURLException e) { LOGGER.warning(e, "初始化连接出错!URL {0} 格式不对!", url); } try { return (HttpURLConnection) httpUrl.openConnection(); } catch (IOException e) { LOGGER.warning(e, "初始化连接出错!URL {0}。", url); } return null; }
拿到 HttpURLConnection,我们可以对其施加配置,例如下面一堆 lambda,
/** * 设置请求方法 */ public final static BiConsumer<HttpURLConnection, String> setMedthod = (conn, method) -> { try { conn.setRequestMethod(method); } catch (ProtocolException e) { LOGGER.warning(e); } }; /** * 设置 cookies */ public final static BiConsumer<HttpURLConnection, Map<String, String>> setCookies = (conn, map) -> conn.addRequestProperty("Cookie", MapTool.join(map, ";")); /** * 请求来源 */ public final static BiConsumer<HttpURLConnection, String> setReferer = (conn, url) -> conn.addRequestProperty("Referer", url); // httpUrl.getHost()? /** * 设置超时 (单位:秒) */ public final static BiConsumer<HttpURLConnection, Integer> setTimeout = (conn, timeout) -> conn.setConnectTimeout(timeout * 1000); /** * 客户端识别 */ public final static BiConsumer<HttpURLConnection, String> setUserAgent = (conn, url) -> conn.addRequestProperty("User-Agent", url); /** * 默认的客户端识别 */ public final static Consumer<HttpURLConnection> setUserAgentDefault = conn -> setUserAgent.accept(conn, "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.4; en-US; rv:1.9.2.2) Gecko/20100316 Firefox/3.6.2"); /** * HTTP Basic 用户认证 */ public final static BiConsumer<HttpURLConnection, String[]> setBasicAuth = (conn, auth) -> { String username = auth[0], password = auth[1]; String encoding = Encode.base64Encode(username + ":" + password); conn.setRequestProperty("Authorization", "Basic " + encoding); }; /** * 设置启动 GZip 请求 */ public final static Consumer<HttpURLConnection> setGizpRequest = conn -> conn.addRequestProperty("Accept-Encoding", "gzip, deflate");
这些正是“函数接口”的实现,可把一个个函数视作为一个个变量,作为参数参与到方法中,或者立刻执行。当然写作普通 Java 方法也行,可以通过 ClassFoo::Method 视作变量传递,只是代码行数会多一点,——样样都多一点,加起来就很多的啦。
配置好连接对象之后,就可以发送请求了。发送的时机是执行 conn.getInputStream(); 的时候。
/** * 发送请求,返回响应信息 * * @param conn 链接对象 * @param isEnableGzip 是否需要 GZip 解码 * @param callback 回调里面请记得关闭 InputStream * @return */ public static <T> T getResponse(HttpURLConnection conn, Boolean isEnableGzip, Function<InputStream, T> callback) { try { InputStream in = conn.getInputStream();// 发起请求,接收响应 // 是否启动 GZip 请求 // 有些网站强制加入 Content-Encoding:gzip,而不管之前的是否有 GZip 的请求 boolean isGzip = isEnableGzip || "gzip".equals(conn.getHeaderField("Content-Encoding")); if (isGzip) in = new GZIPInputStream(in); int responseCode = conn.getResponseCode(); if (responseCode >= 400) {// 如果返回的结果是400以上,那么就说明出问题了 RuntimeException e = new RuntimeException(responseCode < 500 ? responseCode + ":客户端请求参数错误!" : responseCode + ":抱歉!我们服务端出错了!"); LOGGER.warning(e); } if (callback == null) { in.close(); } else return callback.apply(in); } catch (IOException e) { LOGGER.warning(e); } return null; }
基本上要对响应的 HTTP code 检查一下,告知基本的响应情况,是 4xx 客户端错误还是 5XX 服务端的责任。有时候无须获取内容的,只要获取响应头(Response Head)即可,例如 HEAD 请求。
得到响应后至于要干什么,具体是 Function<InputStream, T> callback
干的事情,表示这个函数输入的参数是 InputStream 类型,返回的是 T 类型,也就是说,这个 lambda 返回什么,getResponse 就返回什么。我们必不限定必须返回 String,甚至一个特定的 JSON/XML 类型也可以,——显然,这是灵活性的一个体现。
下面 方法整合了上述 initHttpConnection() 和 getResponse(),
/** * GET 请求,返回文本内容 * * @param url * @return */ public static String get(String url, boolean isGzip) { HttpURLConnection conn = initHttpConnection(url); if (isGzip) setGizpRequest.accept(conn); return getResponse(conn, isGzip, NetUtil::byteStream2stringStream); }
NetUtil::byteStream2stringStream 是一个方法引用,此刻最能体现“函数作为变量传来传去”之意味——它只是引用却没用马上执行,与 NetUtil.byteStream2stringStream(xx) 明显不同的。有括号的表示立刻执行。虽然没用显示参数,但实际上是有“函数接口”作类型约束的,不是什么函数都可以传入给 getResponse()。
byteStream2stringStream 原型是 public static String byteStream2stringStream(InputStream in)
,读输入的字节流转换到字符流,将其转换为文本(多行)的字节流转换为字符串。注意 HTTP 请求的原始数据多为流(Stream)。
get() 方法是返回文本 String,如果想将响应的内容保存文件,那就不是 byteStream2stringStream,且看下载文件方法:
public static String download(String url, String saveDir, String newFileName) { HttpURLConnection conn = initHttpConnection(url); setUserAgentDefault.accept(conn); conn.setDoInput(true); conn.setDoOutput(true); String fileName = newFileName == null ? IoHelper.getFileNameFromUrl(url) : newFileName; String newlyFilePath = getResponse(conn, false, in -> { File file = IoHelper.createFile(saveDir, fileName); try (OutputStream out = new FileOutputStream(file);) { IoHelper.write(in, out, true); return file.toString(); } catch (IOException e) { LOGGER.warning(e); } finally { try { in.close(); } catch (IOException e) { LOGGER.warning(e); } } return null; }); return newlyFilePath; }
上述 download 方法写死了一个最简单的方案,如果有新的需求,例如要 HTTP Basic Auth 认证的,就要在发起请求之前对 conn 进行配置,对此我们不妨加入一个符合 Consumer<HttpURLConnection> fn)
接口的函数对象,其实现就是进行 Basic 认证。甚至地,不止一个 Consumer<HttpURLConnection>
,可以多个对 conn 进行配置,那么就改为可变长的参数 Consumer<HttpURLConnection>... fn
,遍历一下执行 fn 即可。这思路的代码没有在库里面实现,读者可以自己尝试写一下。
基本上文给了一个完整思路,而围绕一个 HTTP 库还有其它的如 POST 的请求,我就不逐一分析了,基本都是对 HTTP Connection 进行配置,当然得对 HTTP 协议有一定了解才行。本文主要讲的是编码风格的一种提倡,即 Java 8 的 lambda,——它好处不少,也与之前编码风格不太一样,如果你在学习 Java FP,那么欢迎你结合本文来探讨。笔者说的不一定对,有错的地方请多多包涵并祈望告之,谢谢喔。
库源码:
https://gitee.com/sp42_admin/ajaxjs/blob/master/ajaxjs-base/src/main/java/com/ajaxjs/net/http/NetUtil.java