文章目錄
概述
作爲一個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要素都是由的。因此我借鑑了這個思路,但是並沒有按照它們的實現去做,感覺有點兒過度設計,實在沒有必要。
重複造輪子肯定是沒有必要的,但是前面的輪子覺得它不好,那我們就得站出來給它修一修。