还在为HttpUtils怎么写而烦恼吗?看这一篇就足够了

概述

作为一个java开发,自己肯定写过或者用过HttpUtils用来发送http请求吧,而且肯定也见过各种五花八门的工具类吧,而且每个都不一样,内心有没有写一个相对标准的工具类的想法呢?反正我自己是有这种想法的,毕竟Http是有标准的,刚好机会来了,就按照自己理解的标准去写了,分享一下,当然也会提供一些比较容易扩展的方式,毕竟每个人的需求都是不同的。
前面会用一定的篇幅讲述一下我所依赖的“标准”到底是什么样的,后面再贴代码。

注:我依赖的httpclient版本是org.apache.httpcomponents:httpclient:4.4.1。

关于Http的基本标准

这边我对Http的基本知识不做太多普及,仅讲述和我设计有关的相关知识。

Http的四要素

众所周知,Http是基于TCP,而TCP是用来传输数据的,说得再通俗一些,就是用来传递字符串的。那么这个字符串到底要如何传递以及如何解析,这就是应用层协议需要设计的,我们平时见到的应用层协议都是围绕“如何来传递字符串”这个目标然后实现的(如何传递也意味着如何解析),Http也是,它的标准就是4要素,或者是4块内容。

  1. 请求行
    请求行占了一行,格式是:方法 请求地址 HTTP/版本号
    后面中间分别有1个空格
  2. 请求头
    这个是多行,每一行都是key:value的形式
  3. 空行
    就是一个空行,用于区分请求头和请求体
  4. 请求体
    请求体就是我们常说的body或者form部分了。这个行数不定,那如何知道结尾了呢?就要用请求头里面所标记的Content-Length来判断了,用一些框架在解析的时候,如何发现Length的长度和请求体的长度不一致的时候,就会出现异常。包括接收Http请求或者请求http的时候。

对于Http的响应也是这4要素。

四要素举例

光看上面的解释,其实还是比较抽象的,我举几个平时最常见的例子,一目了然。对了,我都是用postman做的测试。

最常见的get请求

GET /http/get?arg=123 HTTP/1.1
name: xiaopang
Host: localhost:8080


get请求,没有请求体。

普通字符串的post请求

POST /http/post/string?arg=123 HTTP/1.1
name: xiaopang
Content-Type: text/plain
Host: localhost:8080
content-length: 4

test

这个相对与之前的增加了3个地方。
请求头增加了Content-Type:text/plain,这个就是告诉接收方我发送的就是普通的字符串,不用特殊处理。
关于这个参数多说一点点,这个看你使用的web框架,会不会用这个,有的根本不会用。
增加了请求体,因此也有了conten-length,这个长度它指的是字节

普通form表单的post请求

我们经常也会用这种的,就是以key-value的方式去提交表单。都是普通的字符串,这种form我们都称之为“application/x-www-form-urlencoded”。

POST /http/post/form-simple?arg=123&arg1=456 HTTP/1.1
name: xiaopang
content-type: application/x-www-form-urlencoded
content-length: 15

name=ywg&age=25

这种的其实还是比较常见的,这边我传递了两个参数,name和age,但是你注意看第一行,我还传递了两个参数arg和arg1,你会发现和请求体里面的格式特别像。这也就解释了为什么叫做form-urlencoded了,form代表了它是表单形式,在请求体里面,同时我还标记了一下它的方式仍然是按照url参数来传递的。
在url后面跟着的key-value形式的参数,我们一般称之为url参数户或者查询参数(query),我比较喜欢称之为查询参数,叫做query。对于这些一般传递的时候都会encode一下,解析的时候需要decode一下。

multi-form(多表单)的post请求

我们这边以传递一个字符串和一个文件为例:

POST /http/post/form-multi?arg=123&arg1=456 HTTP/1.1
name: xiaopang
cache-control: no-cache
Host: localhost:8080
content-type: multipart/form-data; boundary=--------------------------400103441794568578082510
content-length: 350
Connection: keep-alive

----------------------------400103441794568578082510
Content-Disposition: form-data; name="name"

ywg
----------------------------400103441794568578082510
Content-Disposition: form-data; name="file"; filename="多表单测试文件.txt"
Content-Type: text/plain

多表单测试文件
----------------------------400103441794568578082510--

这个很明显复杂了很多,我挨个说一下里面的点。
content-type是multipart/form-data,代表是多表单,然后后面多了一个boundary,也就是边界,什么的边界呢?既然是多表单,那就是多个表单之间的边界。这个是随机生成的,后面的请求体的话以boundary开头,中间以boundary分割,最后以boundary+“–”结尾
然后我们再仔细看一下form里面的每个细节,你就会发现它完全就是http的3要素啊,请求头+空行+请求体。请求头里面全都是对里面内容的描述,比如它是个字符串,还是个普通文件,或者是图片视频之类的。不过它不需要content-length,因为有boundary。
再看一下传图片的吧,里面的我就不解释了,也不用纠结里面乱七八糟的符号是什么了。

POST /http/post/form-multi?arg=123&arg1=456 HTTP/1.1
name: xiaopang
Host: localhost:8080
content-type: multipart/form-data; boundary=--------------------------345861699398287183379366
content-length: 2324

----------------------------345861699398287183379366
Content-Disposition: form-data; name="name"

ywg
----------------------------345861699398287183379366
Content-Disposition: form-data; name="file"; filename="多表单测试.png"
Content-Type: image/png


�PNG

IHDR   �      "]�Q   sRGB ���   gAMA  ���a   	pHYs  �  ��o�d  gIDATx^!�KF�o�G`�80X�$(4�E�Y�D�` 
���I0�y��u�����[KOW3�$_z�fz;u��V�	S�C'߿�������(�V�2����@� �7Ba�1��E{�����0�X�PA�o�
�|c T���0����@� �7Ba�1�PA�o�
BF�P�1?W�*"	������R!�a+���2 Te@�|ʀP� ���A(B�P��g7B�=�[7	B-�ԦU�5���ҡv�R�Ku��ݡ�c�:mʀP� ���A(1�޾}K� ��P�߿'U�pL(��2 T�2 T�2 T�2 T�2 T�2 T�2 T����Dl���L�M�nk���C턞�,9Bw���i�v��3t���ӧO�ȶ �at�~��ux�����ÇW��� BF�۷oW?AWw��Ǐ�+ہ
P�х�r��$��߿?IJ�P���*]�Hb�Ʒ�#%�ԕ�2��w�ܹZ��#Uw���i��2*T�G_ԙw�-@(èB��]A˝��-o��0�P�f����{�T �aD��u!�Cmys�P���2 T��OM�5�P˔ڴ궔�:�:�N��&j:�A�ӂP�ʃP�ʃP�ʃP�ʃP�ʃP�ʃP�ʃP�ʃP�cB}���TA(1�Z{L�{�pL(��2 T����Dl���L�M�nk���C턞�,9Bw���i� ��� ��� ��� ��� ��� �ad��s�z��-4( �at��{�?T����*e�?T����*{Fm��!�aOB�3��f���Lܔ#�ci��*�|��J�Cm��ڴ�Fz?_:TEK��zj�T�j:9�X�r T�d�oʷ�I �ad�F��������������������g7B�=�[7	B-SjӪ���|�P;��6Ku��ݡ�c�5�j�1y�A(1�Z{L�{�pL(��2 T�2 T�2 T�2 T�2 T�2 T�2 T�2 T�2 T�2A(�Zx���z��S��t⩣���G�l�Xo�!���Iz�M�nKY��CU���c]MOm6�P�1��(�v��w���;��@(èBic1mݣ-|F��
�{��;�P�ʃP�Q�������
U�2��;�eU(��.UK5��Pj �D�T�oʷ�I �at�F��������g7B�=�[7	B-SjӪ���|�P;��6Ku��ݡ�c�:e@�e@�e@�e@�e8&YN�����YD�_.//�J�8{�"lu.�PA�o�|c T���0����꟯���PA�o�
�|c T���0����@� �7���/����w�W�    IEND�B`�
----------------------------3458616993982871

顺便说一句,这就是http的标准,但这个标准定的是在太大了,也就是太不规范了,因此有了我们常说的REST协议了。

Http传递参数的方式

其实HttpUtils要解决的问题就是http参数传递的问题,因此想写出好的工具类,那就必须知道所有的传参方式,哪怕自己不用,也可以不写,但得知道。上面花那么多篇幅讲4要素,顺便也把所有的参数传递方式讲了一遍,我这边总结一下,下面的几种参数,只有请求体里面只能允许一种情况存在,其它的都可以存在。

  1. 请求方法,这也算参数吧,因为在用的时候需要用。
  2. 请求地址。
  3. 路径参数。
  4. 请求头参数。
  5. 请求体参数。

关于请求体的话,有这么几种情况。
普通字符串。这个就包含我们常见的字符串,xml以及json,在我眼里。都是字符串。
普通form。
多表单形式。
我觉得就这3种,但是在日常工作中可能提及的不止这么几种,但本质上就是它们。
而且可能更多人用流这个词,这个也没啥问题,因为从Socket编程来讲,确实是这样,以字符串的形式说这些,我觉得更好理解吧。

不同的方法有不同的处理方式

对于Http来讲,整体上就分为两种,有没有请求体。没有的,就是代表性的get,有请求体的就是post(这个也可以没有)。
不要跟我纠结到底get能不能传递body,要严格来说的话,从Http角度来讲,它确实可以传。但是从应用角度来讲,千万不要这样做,这不是一种主流,也不符合REST规范,而且最关键的在于,你所用的框架支持不支持,这是最重要的。
但是对于没有body的情况,你可以认为body是空,因此,它还是4要素,都是一样的。因此我们工具类里面就是分别处理一下上面说的5种参数,然后把它们按照不同的情况进行组合一下,就可以支持很多种情况了。

HttpUtils的设计思想

这边可以把它分成3个层次。
最底下的层次就是刚才说的处理参数的地方,它们都是独立的,分别处理自己的参数。
中间的一层就是组合处理参数方法,把它们构成一个完整的http请求体。
最外层就是对外提供的API,就是我们的静态方法了,这边根据自己的实际情况随意的进行封装就行了。

特别是下面那两层,我见得最多的就是把各个混为一谈,完全没有章法,这也是形成不了标准的原因。

实现思路的话,既然我能够组合成多种情况,那么我希望有一个类能够处理所有的情况,而且就只有一个出口,通过里面的参数来标记你到底是什么请求,然后我再处理。

HttpUtils的代码

这边有很多个类,分块来看。

基础类

我比较希望只有一个出口,一个参数,这个参数类如下:

public class HttpArgument{
    private String url;
    private HttpMethod method;
    private String contentType="application/json";
    private Map<String,String> header;
    private Map<String,String> query;
    private Map<String,String> form;
    private String body;

    public HttpArgument(String url, HttpMethod method) {
        this.url = url;
        this.method = method;
    }
}

//省略get和set方法。

这个就是入口的参数,它基本包含了所有的参数,除过多表单文件的,这个我这边用的少,就没写。
通过method来标记是什么方法。

public enum HttpMethod {
    OPTIONS,
    GET,
    HEAD,
    POST,
    PUT,
    DELETE,
    TRACE,
    CONNECT,
    PATCH,
    OTHER
}

常见的方法。

public class HttpException extends Exception {
    public HttpException(String message, Throwable cause) {
        super(message, cause);
    }

    public HttpException(String message) {
        super(message);
    }
}

普通的异常封装类。

我把请求的结果做了一层封装,对于正常返回的,没有抛异常的,我都正常返回,包括400,404等,这个是我希望外界去处理这个,因此有了下面这个。

public class HttpResult {
    private int statusCode;
    private String response;

    public int getStatusCode() {
        return this.statusCode;
    }

    public void setStatusCode(int statusCode) {
        this.statusCode = statusCode;
    }

    public String getResponse() {
        return response;
    }

    public void setResponse(String response) {
        this.response = response;
    }

    public HttpResult() {
    }

    public HttpResult(int statusCode, String response) {
        this.statusCode = statusCode;
        this.response = response;
    }

    public boolean isOK() {
        return 200 == this.getStatusCode();
    }
}

这个返回值目前就是string。

真正的调用类

这个是真正的处理过程,入口是doAction()方法,细节我就不讲了。

public class HttpClient {
	private Logger logger = LoggerFactory.getLogger(this.getClass().getName());
    private int connectionRequestTimeout=10*1000;
    private int connectTimeout=2*1000;
    private int socketTimeout=60*1000;
    private int maxPoolSize=200;

	private RequestConfig requestConfig;
	private PoolingHttpClientConnectionManager connectPool;
	private CloseableHttpClient httpClient;
	private String charset="UTF-8";
	public HttpClient() {
	}

	public HttpResult doAction(HttpArgument argument) throws HttpException{
		HttpResult result;
		HttpMethod method=argument.getMethod();
		switch (method){
			case POST:
				result =postOrPut(argument,createUri(argument),true);
				break;
			case PUT:
				result =postOrPut(argument,createUri(argument),false);
				break;
			case GET:
				result =getOrDelete(argument,createUri(argument),true);
				break;
			case DELETE:
				result =getOrDelete(argument,createUri(argument),false);
				break;
			default:
				throw new HttpException("not support method:"+method.name());
		}
		return result;
	}

	private URI createUri(HttpArgument argument) throws HttpException {
		QueryStringEncoder encoder=new QueryStringEncoder(argument.getUrl());
		Optional.ofNullable(argument.getQuery()).ifPresent(res->res.forEach((key, value)->encoder.addParam(key,value)));
		URI requestURI=null;
		try {
			requestURI = encoder.toUri();
		} catch (Exception e) {
			String message="非法的url:"+argument.getUrl();
			logger.error(message);
			throw new HttpException(message,e);
		}
		return  requestURI;
	}

	private HttpResult postOrPut(HttpArgument argument,URI requestURI,boolean post) throws HttpException{
		HttpEntityEnclosingRequestBase method;
		if (post){
			method=new HttpPost(requestURI);
		}else{
			method=new HttpPut(requestURI);
		}
		HttpEntity entity=null;
		if (argument.getForm()!=null){
			//form表单传递方式,
			List<NameValuePair> form=new ArrayList<>();
			argument.getForm().forEach((key,value)->form.add(new BasicNameValuePair(key,value)));
			try {
				entity=new UrlEncodedFormEntity(form,charset);
			} catch (UnsupportedEncodingException e) {
				//ignore
			}
		}else{
			//请求体的情况
			entity = new StringEntity(argument.getBody(), charset);
			((StringEntity)entity).setContentEncoding(new BasicHeader(HTTP.CONTENT_TYPE,
					argument.getContentType()));
		}
		method.setEntity(entity);

		return request(method,argument.getHeader());
	}


	private HttpResult getOrDelete(HttpArgument argument,URI requestURI,boolean get) throws HttpException {
		HttpRequestBase httpRequestBase;
		if (get){
			httpRequestBase=new HttpGet(requestURI);
		}else{
			httpRequestBase=new HttpDelete(requestURI);
		}
		return request(httpRequestBase,argument.getHeader());
	}

	/**
	 * 增加header
	 * @param httpUriRequest
	 * @param header
	 * @return
	 * @throws HttpException
	 */
	private HttpResult request(HttpRequestBase httpUriRequest,Map<String,String> header) throws HttpException {
		Optional.ofNullable(header).ifPresent(res->res.forEach((key, value)->httpUriRequest.addHeader(new BasicHeader(key,value))));
		return request(httpUriRequest);
	}

	/**
	 * 最终请求
	 * @param httpRequestBase
	 * @return
	 * @throws HttpException
	 */
	private HttpResult request(HttpRequestBase httpRequestBase)throws HttpException{
		CloseableHttpClient httpClient = getHttpClient();
		CloseableHttpResponse httpResponse = null;
		try {
			httpResponse = httpClient.execute(httpRequestBase);
			int statusCode = httpResponse.getStatusLine().getStatusCode();
			return new HttpResult(statusCode,EntityUtils.toString(httpResponse.getEntity(),charset));
		} catch (IOException e) {
			logger.error("访问http失败,url:"+httpRequestBase.getURI().getPath(), e);
			String message=e.getMessage();
			if (message==null&&e.getCause()!=null){
				message=e.getCause().getMessage();
			}
			throw new HttpException("url:"+httpRequestBase.getURI().getPath()+";"+message);
		} finally {
			if (httpResponse != null) {
				IOUtils.closeQuietly(httpResponse);
			}
		}
	}

	private CloseableHttpClient getHttpClient() {
		if (httpClient==null) {
			return createHttpClient();
		}
		return httpClient;
	}

	public void setConnectionRequestTimeout(int connectionRequestTimeout) {
		this.connectionRequestTimeout = connectionRequestTimeout;
	}

	public void setConnectTimeout(int connectTimeout) {
		this.connectTimeout = connectTimeout;
	}

	public void setSocketTimeout(int socketTimeout) {
		this.socketTimeout = socketTimeout;
	}

	public void setMaxPoolSize(int maxPoolSize) {
		this.maxPoolSize = maxPoolSize;
	}


	private CloseableHttpClient createHttpClient() {
		httpClient=null;
		SSLContext ctx = null;

		X509TrustManager xtm = new X509TrustManager() {
			@Override
			public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
			}
			@Override
			public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
			}
			@Override
			public X509Certificate[] getAcceptedIssuers() {
				return null;
			}
		};

		try {
			ctx = SSLContext.getInstance("TLS");
			// 使用TrustManager来初始化该上下文,TrustManager只是被SSL的Socket所使用
			ctx.init(null, new TrustManager[]{xtm}, null);
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
		} catch (KeyManagementException e) {
			e.printStackTrace();
		}


		ConnectionSocketFactory plainsf = PlainConnectionSocketFactory.getSocketFactory();
		LayeredConnectionSocketFactory sslsf = SSLConnectionSocketFactory.getSocketFactory();
		Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
				.register("http", plainsf)
				.register("https", new SSLConnectionSocketFactory(ctx))
				.build();

		connectPool=new PoolingHttpClientConnectionManager(registry);
		connectPool.setMaxTotal(maxPoolSize);
		requestConfig=RequestConfig.custom()
				.setConnectionRequestTimeout(connectionRequestTimeout)
				.setConnectTimeout(connectTimeout)
				.setSocketTimeout(socketTimeout).build();
		httpClient=HttpClients.custom()
				.setConnectionManager(connectPool)
				.setDefaultRequestConfig(requestConfig)
				.setSslcontext(ctx)
				.setSSLHostnameVerifier((host, sslSession)->true)
				.build();
		return httpClient;
	}

	private  static  class QueryStringEncoder{
		private final String charsetName;
		private final StringBuilder uriBuilder;
		private boolean hasParams;

		public QueryStringEncoder(String uri) {
			this(uri, "UTF-8");
		}

		public QueryStringEncoder(String uri, String charsetName) {
			if (uri.contains("?")&&uri.contains("=")){
				this.hasParams=true;
			}
			this.uriBuilder = new StringBuilder(uri);
			this.charsetName = charsetName;
		}

		public void addParam(String name, String value) {
			Objects.requireNonNull(name, "name");
			if (hasParams) {
				uriBuilder.append('&');
			} else {
				uriBuilder.append('?');
				hasParams = true;
			}
			appendUrl(name, charsetName, uriBuilder);
			if (value != null) {
				uriBuilder.append('=');
				appendUrl(value, charsetName, uriBuilder);
			}
		}

		public URI toUri() throws URISyntaxException {
			return new URI(toString());
		}

		@Override
		public String toString() {
			return uriBuilder.toString();
		}

		private static void appendUrl(String s, String charset, StringBuilder sb) {
			try {
				s = URLEncoder.encode(s, charset);
			} catch (UnsupportedEncodingException ignored) {
				throw new UnsupportedCharsetException(charset);
			}
			sb.append(s);
		}
	}
}

后面的QueryStringEncoder是专门用来处理URLencode参数的。

工具类的封装

简单写了下,其它需求的都可以补充。这个是我现写的,我真正用的都直接调用HttpClient了。。。

public class HttpUtils {
    private static HttpClient httpClient=new HttpClient();

    public static HttpResult get(String url) throws HttpException {
        return get(url,null);
    };

    public static HttpResult get(String url,Map<String,String> header) throws HttpException {
        return get(url,header,null);
    };

    public static HttpResult get(String url, Map<String,String> header, Map<String,String> query) throws HttpException {
        HttpArgument argument=new HttpArgument(url,HttpMethod.GET);
        argument.setHeader(header);
        argument.setQuery(query);
        return httpClient.doAction(argument);
    }
}

总结

其实这个表面上看起来和别人的都差不多,但是我觉得区别很大。
我是按照Http协议本身的特点去思考如何更加合理的处理这些参数,这个思路我是看了apache-http的接口设计上面才有的,有兴趣的可以看看,它们的设计上完全是按照4要素去设计的,针对不同的请求体的类型,它们就做了不同的实现,但是接口定义就只有一个HttpEntity,它就代表的是请求体,不同的情况实现类去处理就行了,当然设计上还不止这一个,4要素都是由的。因此我借鉴了这个思路,但是并没有按照它们的实现去做,感觉有点儿过度设计,实在没有必要。
重复造轮子肯定是没有必要的,但是前面的轮子觉得它不好,那我们就得站出来给它修一修。

猜你喜欢

转载自blog.csdn.net/ywg_1994/article/details/104487216