运用 Java 8 写一个 HTTP 请求工具类

三年多以前写过一个 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;
}

可见输入流导入到 输出流 FileOutputStream 中,不再是转换为文本,而是把得到的字节流保存成为文件。

当前而言,上述 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

本文原文地址:https://blog.csdn.net/zhangxin09/article/details/86668854

猜你喜欢

转载自blog.csdn.net/Roger_CoderLife/article/details/87155982