文章目录
概述
作为一个java开发,自己肯定写过或者用过HttpUtils用来发送http请求吧,而且肯定也见过各种五花八门的工具类吧,而且每个都不一样,内心有没有写一个相对标准的工具类的想法呢?反正我自己是有这种想法的,毕竟Http是有标准的,刚好机会来了,就按照自己理解的标准去写了,分享一下,当然也会提供一些比较容易扩展的方式,毕竟每个人的需求都是不同的。
前面会用一定的篇幅讲述一下我所依赖的“标准”到底是什么样的,后面再贴代码。
注:我依赖的httpclient版本是org.apache.httpcomponents:httpclient:4.4.1。
关于Http的基本标准
这边我对Http的基本知识不做太多普及,仅讲述和我设计有关的相关知识。
Http的四要素
众所周知,Http是基于TCP,而TCP是用来传输数据的,说得再通俗一些,就是用来传递字符串的。那么这个字符串到底要如何传递以及如何解析,这就是应用层协议需要设计的,我们平时见到的应用层协议都是围绕“如何来传递字符串”这个目标然后实现的(如何传递也意味着如何解析),Http也是,它的标准就是4要素,或者是4块内容。
- 请求行
请求行占了一行,格式是:方法 请求地址 HTTP/版本号
后面中间分别有1个空格 - 请求头
这个是多行,每一行都是key:value的形式 - 空行
就是一个空行,用于区分请求头和请求体 - 请求体
请求体就是我们常说的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�1JǙ?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�2�A(�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要素,顺便也把所有的参数传递方式讲了一遍,我这边总结一下,下面的几种参数,只有请求体里面只能允许一种情况存在,其它的都可以存在。
- 请求方法,这也算参数吧,因为在用的时候需要用。
- 请求地址。
- 路径参数。
- 请求头参数。
- 请求体参数。
关于请求体的话,有这么几种情况。
普通字符串。这个就包含我们常见的字符串,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要素都是由的。因此我借鉴了这个思路,但是并没有按照它们的实现去做,感觉有点儿过度设计,实在没有必要。
重复造轮子肯定是没有必要的,但是前面的轮子觉得它不好,那我们就得站出来给它修一修。