内容简介:三年多以前写过一个HTTP 请求类,然后又将其改进为链式风格的调用方式。虽然可以实现需求,基本上也没用重复的逻辑,但是编码上总是觉得怪怪的,当时也说不上哪里不对劲,尽管逻辑没错能实现,然而就是感觉谈不上“优雅”。那时水平有限,想不出办法也就没去专研了。应该说,现在的 Java 8 的函数式风格给予了我完全不一样的灵感。使用 lambda(匿名函数),与使用普通 Java 函数(方法),首先它更轻量级的,更灵活,于是能够易于表达“我想做什么”,而至于“我怎么做”那部分,能省则省,不要我重复写,也不要然让我去调
三年多以前写过一个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,那么欢迎你结合本文来探讨。笔者说的不一定对,有错的地方请多多包涵并祈望告之,谢谢喔。
以上所述就是小编给大家介绍的《运用 Java 8 写一个 HTTP请求工具类》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Agile Web Development with Rails, Third Edition
Sam Ruby、Dave Thomas、David Heinemeier Hansson / Pragmatic Bookshelf / 2009-03-17 / USD 43.95
Rails just keeps on changing. Rails 2, released in 2008, brings hundreds of improvements, including new support for RESTful applications, new generator options, and so on. And, as importantly, we’ve a......一起来看看 《Agile Web Development with Rails, Third Edition》 这本书的介绍吧!