我們可以通過spring boot快速開發rest接口,同時也可能需要在實現接口的過程中,通過spring boot調用內外部rest接口完成業務邏輯。
在spring boot中,調用rest api常見的一般主要有兩種方式,通過自帶的resttemplate或者自己開發http客戶端工具實現服務調用。
resttemplate基本功能非常強大,不過某些特殊場景,我們可能還是更習慣用自己封裝的工具類,比如上傳文件至分布式文件系統、處理帶證書的https請求等。
本文以resttemplate來舉例,記錄幾個使用resttemplate調用接口過程中發現的問題和解決方案。
一、resttemplate簡介
1、什么是resttemplate
我們自己封裝的httpclient,通常都會有一些模板代碼,比如建立連接,構造請求頭和請求體,然后根據響應,解析響應信息,最后關閉連接。
resttemplate是spring中對httpclient的再次封裝,簡化了發起http請求以及處理響應的過程,抽象層級更高,減少消費者的模板代碼,使冗余代碼更少。
其實仔細想想spring boot下的很多xxxtemplate類,它們也提供各種模板方法,只不過抽象的層次更高,隱藏了更多細節而已。
順便提一下,spring cloud有一個聲明式服務調用feign,是基于netflix feign實現的,整合了spring cloud ribbon與 spring cloud hystrix,并且實現了聲明式的web服務客戶端定義方式。
本質上feign是在resttemplate的基礎上對其再次封裝,由它來幫助我們定義和實現依賴服務接口的定義。
2、resttemplate常見方法
常見的rest服務有很多種請求方式,如get,post,put,delete,head,options等。resttemplate實現了最常見的方式,用的最多的就是get和post了,調用api可參考源碼,這里列舉幾個方法定義(get、post、delete):
methods
1
2
3
4
5
6
7
8
9
10
11
|
public <t> t getforobject(string url, class <t> responsetype, object... urivariables) public <t> responseentity<t> getforentity(string url, class <t> responsetype, object... urivariables) public <t> t postforobject(string url, @nullable object request, class <t> responsetype,object... urivariables) public <t> responseentity<t> postforentity(string url, @nullable object request, class <t> responsetype, object... urivariables) public void delete(string url, object... urivariables) public void delete(uri url) |
同時要注意兩個較為“靈活”的方法 exchange 和 execute 。
resttemplate暴露的exchange與其它接口的不同:
(1)允許調用者指定http請求的方法(get,post,delete等)
(2)可以在請求中增加body以及頭信息,其內容通過參數‘httpentity<?>requestentity'描述
(3)exchange支持‘含參數的類型'(即泛型類)作為返回類型,該特性通過‘parameterizedtypereference<t>responsetype'描述。
resttemplate所有的get,post等等方法,最終調用的都是execute方法。excute方法的內部實現是將string格式的uri轉成了java.net.uri,之后調用了doexecute方法,doexecute方法的實現如下:
doexecute
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
/** * execute the given method on the provided uri. * <p>the {@link clienthttprequest} is processed using the {@link requestcallback}; * the response with the {@link responseextractor}. * @param url the fully-expanded url to connect to * @param method the http method to execute (get, post, etc.) * @param requestcallback object that prepares the request (can be {@code null}) * @param responseextractor object that extracts the return value from the response (can be {@code null}) * @return an arbitrary object, as returned by the {@link responseextractor} */ @nullable protected <t> t doexecute(uri url, @nullable httpmethod method, @nullable requestcallback requestcallback, @nullable responseextractor<t> responseextractor) throws restclientexception { assert .notnull(url, "'url' must not be null" ); assert .notnull(method, "'method' must not be null" ); clienthttpresponse response = null ; try { clienthttprequest request = createrequest(url, method); if (requestcallback != null ) { requestcallback.dowithrequest(request); } response = request.execute(); handleresponse(url, method, response); if (responseextractor != null ) { return responseextractor.extractdata(response); } else { return null ; } } catch (ioexception ex) { string resource = url.tostring(); string query = url.getrawquery(); resource = (query != null ? resource.substring( 0 , resource.indexof( '?' )) : resource); throw new resourceaccessexception( "i/o error on " + method.name() + " request for \"" + resource + "\": " + ex.getmessage(), ex); } finally { if (response != null ) { response.close(); } } } |
doexecute方法封裝了模板方法,比如創建連接、處理請求和應答,關閉連接等。
多數人看到這里,估計都會覺得封裝一個restclient不過如此吧?
3、簡單調用
以一個post調用為例:
goodsserviceclient
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
|
package com.power.demo.restclient; import com.power.demo.common.appconst; import com.power.demo.restclient.clientrequest.clientgetgoodsbygoodsidrequest; import com.power.demo.restclient.clientresponse.clientgetgoodsbygoodsidresponse; import org.springframework.beans.factory.annotation.autowired; import org.springframework.beans.factory.annotation.value; import org.springframework.stereotype.component; import org.springframework.web.client.resttemplate; /** * 商品rest接口客戶端 (demo測試用) **/ @component public class goodsserviceclient { //服務消費者調用的接口url 形如:http://localhost:9090 @value ( "${spring.power.serviceurl}" ) private string _serviceurl; @autowired private resttemplate resttemplate; public clientgetgoodsbygoodsidresponse getgoodsbygoodsid(clientgetgoodsbygoodsidrequest request) { string svcurl = getgoodssvcurl() + "/getinfobyid" ; clientgetgoodsbygoodsidresponse response = null ; try { response = resttemplate.postforobject(svcurl, request, clientgetgoodsbygoodsidresponse. class ); } catch (exception e) { e.printstacktrace(); response = new clientgetgoodsbygoodsidresponse(); response.setcode(appconst.fail); response.setmessage(e.tostring()); } return response; } private string getgoodssvcurl() { string url = "" ; if (_serviceurl == null ) { _serviceurl = "" ; } if (_serviceurl.length() == 0 ) { return url; } if (_serviceurl.substring(_serviceurl.length() - 1 , _serviceurl.length()) == "/" ) { url = string.format( "%sapi/v1/goods" , _serviceurl); } else { url = string.format( "%s/api/v1/goods" , _serviceurl); } return url; } } |
demo里直接resttemplate.postforobject方法調用,反序列化實體轉換這些resttemplate內部封裝搞定。
二、問題匯總
1、no suitable httpmessageconverter found for request type異常
這個問題通常會出現在postforobject中傳入對象進行調用的時候。
分析resttemplate源碼,在httpentityrequestcallback類的dowithrequest方法中,如果 messageconverters (這個字段后面會繼續提及)列表字段循環處理的過程中沒有滿足return跳出的邏輯(也就是沒有匹配的httpmessageconverter),則拋出上述異常:
httpentityrequestcallback.dowithrequest
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
|
@override @suppresswarnings ( "unchecked" ) public void dowithrequest(clienthttprequest httprequest) throws ioexception { super .dowithrequest(httprequest); object requestbody = this .requestentity.getbody(); if (requestbody == null ) { httpheaders httpheaders = httprequest.getheaders(); httpheaders requestheaders = this .requestentity.getheaders(); if (!requestheaders.isempty()) { for (map.entry<string, list<string>> entry : requestheaders.entryset()) { httpheaders.put(entry.getkey(), new linkedlist<>(entry.getvalue())); } } if (httpheaders.getcontentlength() < 0 ) { httpheaders.setcontentlength(0l); } } else { class <?> requestbodyclass = requestbody.getclass(); type requestbodytype = ( this .requestentity instanceof requestentity ? ((requestentity<?>) this .requestentity).gettype() : requestbodyclass); httpheaders httpheaders = httprequest.getheaders(); httpheaders requestheaders = this .requestentity.getheaders(); mediatype requestcontenttype = requestheaders.getcontenttype(); for (httpmessageconverter<?> messageconverter : getmessageconverters()) { if (messageconverter instanceof generichttpmessageconverter) { generichttpmessageconverter<object> genericconverter = (generichttpmessageconverter<object>) messageconverter; if (genericconverter.canwrite(requestbodytype, requestbodyclass, requestcontenttype)) { if (!requestheaders.isempty()) { for (map.entry<string, list<string>> entry : requestheaders.entryset()) { httpheaders.put(entry.getkey(), new linkedlist<>(entry.getvalue())); } } if (logger.isdebugenabled()) { if (requestcontenttype != null ) { logger.debug( "writing [" + requestbody + "] as \"" + requestcontenttype + "\" using [" + messageconverter + "]" ); } else { logger.debug( "writing [" + requestbody + "] using [" + messageconverter + "]" ); } } genericconverter.write(requestbody, requestbodytype, requestcontenttype, httprequest); return ; } } else if (messageconverter.canwrite(requestbodyclass, requestcontenttype)) { if (!requestheaders.isempty()) { for (map.entry<string, list<string>> entry : requestheaders.entryset()) { httpheaders.put(entry.getkey(), new linkedlist<>(entry.getvalue())); } } if (logger.isdebugenabled()) { if (requestcontenttype != null ) { logger.debug( "writing [" + requestbody + "] as \"" + requestcontenttype + "\" using [" + messageconverter + "]" ); } else { logger.debug( "writing [" + requestbody + "] using [" + messageconverter + "]" ); } } ((httpmessageconverter<object>) messageconverter).write( requestbody, requestcontenttype, httprequest); return ; } } string message = "could not write request: no suitable httpmessageconverter found for request type [" + requestbodyclass.getname() + "]" ; if (requestcontenttype != null ) { message += " and content type [" + requestcontenttype + "]" ; } throw new restclientexception(message); } } |
最簡單的解決方案是,可以通過包裝http請求頭,并將請求對象序列化成字符串的形式傳參,參考示例代碼如下:
postforobject
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
/* * post請求調用 * */ public static string postforobject(resttemplate resttemplate, string url, object params) { httpheaders headers = new httpheaders(); mediatype type = mediatype.parsemediatype( "application/json; charset=utf-8" ); headers.setcontenttype(type); headers.add( "accept" , mediatype.application_json.tostring()); string json = serializeutil.serialize(params); httpentity<string> formentity = new httpentity<string>(json, headers); string result = resttemplate.postforobject(url, formentity, string. class ); return result; } |
如果我們還想直接返回對象,直接反序列化返回的字符串即可:
postforobject
1
2
3
4
5
6
7
8
9
10
11
12
|
/* * post請求調用 * */ public static <t> t postforobject(resttemplate resttemplate, string url, object params, class <t> clazz) { t response = null ; string respstr = postforobject(resttemplate, url, params); response = serializeutil.deserialize(respstr, clazz); return response; } |
其中,序列化和反序列化工具比較多,常用的比如fastjson、jackson和gson。
2、no suitable httpmessageconverter found for response type異常
和發起請求發生異常一樣,處理應答的時候也會有問題。
stackoverflow上有人問過相同的問題,根本原因是http消息轉換器httpmessageconverter缺少 mime type ,也就是說http在把輸出結果傳送到客戶端的時候,客戶端必須啟動適當的應用程序來處理這個輸出文檔,這可以通過多種mime(多功能網際郵件擴充協議)type來完成。
對于服務端應答,很多httpmessageconverter默認支持的媒體類型(mimetype)都不同。stringhttpmessageconverter默認支持的則是mediatype.text_plain,sourcehttpmessageconverter默認支持的則是mediatype.text_xml,formhttpmessageconverter默認支持的是mediatype.application_form_urlencoded和mediatype.multipart_form_data,在rest服務中,我們用到的最多的還是 mappingjackson2httpmessageconverter ,這是一個比較通用的轉化器(繼承自generichttpmessageconverter接口),根據分析,它默認支持的mimetype為mediatype.application_json:
mappingjackson2httpmessageconverter
1
2
3
4
5
6
7
8
|
/** * construct a new {@link mappingjackson2httpmessageconverter} with a custom {@link objectmapper}. * you can use {@link jackson2objectmapperbuilder} to build it easily. * @see jackson2objectmapperbuilder#json() */ public mappingjackson2httpmessageconverter(objectmapper objectmapper) { super (objectmapper, mediatype.application_json, new mediatype( "application" , "*+json" )); } |
但是有些應用接口默認的應答mimetype不是application/json,比如我們調用一個外部天氣預報接口,如果使用resttemplate的默認配置,直接返回一個字符串應答是沒有問題的:
1
2
3
|
string url = "http://wthrcdn.etouch.cn/weather_mini?city=上海" ; string result = resttemplate.getforobject(url, string. class ); clientweatherresultvo vo = serializeutil.deserialize(result, clientweatherresultvo. class ); |
但是,如果我們想直接返回一個實體對象:
1
2
3
|
string url = "http://wthrcdn.etouch.cn/weather_mini?city=上海" ; clientweatherresultvo weatherresultvo = resttemplate.getforobject(url, clientweatherresultvo. class ); |
則直接報異常:
could not extract response: no suitable httpmessageconverter found for response type [class ]
and content type [application/octet-stream]
很多人碰到過這個問題,首次碰到估計大多都比較懵吧,很多接口都是json或者xml或者plain text格式返回的,什么是application/octet-stream?
查看resttemplate源代碼,一路跟蹤下去會發現 httpmessageconverterextractor 類的extractdata方法有個解析應答及反序列化邏輯,如果不成功,拋出的異常信息和上述一致:
httpmessageconverterextractor.extractdata
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
@override @suppresswarnings ({ "unchecked" , "rawtypes" , "resource" }) public t extractdata(clienthttpresponse response) throws ioexception { messagebodyclienthttpresponsewrapper responsewrapper = new messagebodyclienthttpresponsewrapper(response); if (!responsewrapper.hasmessagebody() || responsewrapper.hasemptymessagebody()) { return null ; } mediatype contenttype = getcontenttype(responsewrapper); try { for (httpmessageconverter<?> messageconverter : this .messageconverters) { if (messageconverter instanceof generichttpmessageconverter) { generichttpmessageconverter<?> genericmessageconverter = (generichttpmessageconverter<?>) messageconverter; if (genericmessageconverter.canread( this .responsetype, null , contenttype)) { if (logger.isdebugenabled()) { logger.debug( "reading [" + this .responsetype + "] as \"" + contenttype + "\" using [" + messageconverter + "]" ); } return (t) genericmessageconverter.read( this .responsetype, null , responsewrapper); } } if ( this .responseclass != null ) { if (messageconverter.canread( this .responseclass, contenttype)) { if (logger.isdebugenabled()) { logger.debug( "reading [" + this .responseclass.getname() + "] as \"" + contenttype + "\" using [" + messageconverter + "]" ); } return (t) messageconverter.read(( class ) this .responseclass, responsewrapper); } } } } catch (ioexception | httpmessagenotreadableexception ex) { throw new restclientexception( "error while extracting response for type [" + this .responsetype + "] and content type [" + contenttype + "]" , ex); } throw new restclientexception( "could not extract response: no suitable httpmessageconverter found " + "for response type [" + this .responsetype + "] and content type [" + contenttype + "]" ); } |
stackoverflow上的解決的示例代碼可以接受,但是并不準確,常見的mimetype都應該加進去,貼一下我認為正確的代碼:
resttemplateconfig
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
|
package com.power.demo.restclient.config; import com.fasterxml.jackson.databind.objectmapper; import com.google.common.collect.lists; import org.springframework.beans.factory.annotation.autowired; import org.springframework.boot.web.client.resttemplatebuilder; import org.springframework.context.annotation.bean; import org.springframework.http.mediatype; import org.springframework.http.converter.*; import org.springframework.http.converter.cbor.mappingjackson2cborhttpmessageconverter; import org.springframework.http.converter.feed.atomfeedhttpmessageconverter; import org.springframework.http.converter.feed.rsschannelhttpmessageconverter; import org.springframework.http.converter.json.gsonhttpmessageconverter; import org.springframework.http.converter.json.jsonbhttpmessageconverter; import org.springframework.http.converter.json.mappingjackson2httpmessageconverter; import org.springframework.http.converter.smile.mappingjackson2smilehttpmessageconverter; import org.springframework.http.converter.support.allencompassingformhttpmessageconverter; import org.springframework.http.converter.xml.jaxb2rootelementhttpmessageconverter; import org.springframework.http.converter.xml.mappingjackson2xmlhttpmessageconverter; import org.springframework.http.converter.xml.sourcehttpmessageconverter; import org.springframework.stereotype.component; import org.springframework.util.classutils; import org.springframework.web.client.resttemplate; import java.util.arrays; import java.util.list; @component public class resttemplateconfig { private static final boolean romepresent = classutils.ispresent( "com.rometools.rome.feed.wirefeed" , resttemplate . class .getclassloader()); private static final boolean jaxb2present = classutils.ispresent( "javax.xml.bind.binder" , resttemplate. class .getclassloader()); private static final boolean jackson2present = classutils.ispresent( "com.fasterxml.jackson.databind.objectmapper" , resttemplate. class .getclassloader()) && classutils.ispresent( "com.fasterxml.jackson.core.jsongenerator" , resttemplate. class .getclassloader()); private static final boolean jackson2xmlpresent = classutils.ispresent( "com.fasterxml.jackson.dataformat.xml.xmlmapper" , resttemplate. class .getclassloader()); private static final boolean jackson2smilepresent = classutils.ispresent( "com.fasterxml.jackson.dataformat.smile.smilefactory" , resttemplate. class .getclassloader()); private static final boolean jackson2cborpresent = classutils.ispresent( "com.fasterxml.jackson.dataformat.cbor.cborfactory" , resttemplate. class .getclassloader()); private static final boolean gsonpresent = classutils.ispresent( "com.google.gson.gson" , resttemplate. class .getclassloader()); private static final boolean jsonbpresent = classutils.ispresent( "javax.json.bind.jsonb" , resttemplate. class .getclassloader()); // 啟動的時候要注意,由于我們在服務中注入了resttemplate,所以啟動的時候需要實例化該類的一個實例 @autowired private resttemplatebuilder builder; @autowired private objectmapper objectmapper; // 使用resttemplatebuilder來實例化resttemplate對象,spring默認已經注入了resttemplatebuilder實例 @bean public resttemplate resttemplate() { resttemplate resttemplate = builder.build(); list<httpmessageconverter<?>> messageconverters = lists.newarraylist(); mappingjackson2httpmessageconverter converter = new mappingjackson2httpmessageconverter(); converter.setobjectmapper(objectmapper); //不加會出現異常 //could not extract response: no suitable httpmessageconverter found for response type [class ] mediatype[] mediatypes = new mediatype[]{ mediatype.application_json, mediatype.application_octet_stream, mediatype.application_json_utf8, mediatype.text_html, mediatype.text_plain, mediatype.text_xml, mediatype.application_stream_json, mediatype.application_atom_xml, mediatype.application_form_urlencoded, mediatype.application_pdf, }; converter.setsupportedmediatypes(arrays.aslist(mediatypes)); //messageconverters.add(converter); if (jackson2present) { messageconverters.add(converter); } else if (gsonpresent) { messageconverters.add( new gsonhttpmessageconverter()); } else if (jsonbpresent) { messageconverters.add( new jsonbhttpmessageconverter()); } messageconverters.add( new formhttpmessageconverter()); messageconverters.add( new bytearrayhttpmessageconverter()); messageconverters.add( new stringhttpmessageconverter()); messageconverters.add( new resourcehttpmessageconverter( false )); messageconverters.add( new sourcehttpmessageconverter()); messageconverters.add( new allencompassingformhttpmessageconverter()); if (romepresent) { messageconverters.add( new atomfeedhttpmessageconverter()); messageconverters.add( new rsschannelhttpmessageconverter()); } if (jackson2xmlpresent) { messageconverters.add( new mappingjackson2xmlhttpmessageconverter()); } else if (jaxb2present) { messageconverters.add( new jaxb2rootelementhttpmessageconverter()); } if (jackson2smilepresent) { messageconverters.add( new mappingjackson2smilehttpmessageconverter()); } if (jackson2cborpresent) { messageconverters.add( new mappingjackson2cborhttpmessageconverter()); } resttemplate.setmessageconverters(messageconverters); return resttemplate; } } |
看到上面的代碼,再對比一下resttemplate內部實現,就知道我參考了resttemplate的源碼,有潔癖的人可能會說這一坨代碼有點啰嗦,上面那一堆static final的變量和messageconverters填充數據方法,暴露了resttemplate的實現,如果resttemplate修改了,這里也要改,非常不友好,而且看上去一點也不oo。
經過分析,resttemplatebuilder.build()構造了resttemplate對象,只要將內部mappingjackson2httpmessageconverter修改一下支持的mediatype即可,resttemplate的messageconverters字段雖然是private final的,我們依然可以通過反射修改之,改進后的代碼如下:
resttemplateconfig
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
|
package com.power.demo.restclient.config; import com.fasterxml.jackson.databind.objectmapper; import com.google.common.collect.lists; import org.springframework.beans.factory.annotation.autowired; import org.springframework.boot.web.client.resttemplatebuilder; import org.springframework.context.annotation.bean; import org.springframework.http.mediatype; import org.springframework.http.converter.httpmessageconverter; import org.springframework.http.converter.json.mappingjackson2httpmessageconverter; import org.springframework.stereotype.component; import org.springframework.web.client.resttemplate; import java.lang.reflect.field; import java.util.arrays; import java.util.list; import java.util.optional; import java.util.stream.collectors; @component public class resttemplateconfig { // 啟動的時候要注意,由于我們在服務中注入了resttemplate,所以啟動的時候需要實例化該類的一個實例 @autowired private resttemplatebuilder builder; @autowired private objectmapper objectmapper; // 使用resttemplatebuilder來實例化resttemplate對象,spring默認已經注入了resttemplatebuilder實例 @bean public resttemplate resttemplate() { resttemplate resttemplate = builder.build(); list<httpmessageconverter<?>> messageconverters = lists.newarraylist(); mappingjackson2httpmessageconverter converter = new mappingjackson2httpmessageconverter(); converter.setobjectmapper(objectmapper); //不加可能會出現異常 //could not extract response: no suitable httpmessageconverter found for response type [class ] mediatype[] mediatypes = new mediatype[]{ mediatype.application_json, mediatype.application_octet_stream, mediatype.text_html, mediatype.text_plain, mediatype.text_xml, mediatype.application_stream_json, mediatype.application_atom_xml, mediatype.application_form_urlencoded, mediatype.application_json_utf8, mediatype.application_pdf, }; converter.setsupportedmediatypes(arrays.aslist(mediatypes)); try { //通過反射設置messageconverters field field = resttemplate.getclass().getdeclaredfield( "messageconverters" ); field.setaccessible( true ); list<httpmessageconverter<?>> orgconverterlist = (list<httpmessageconverter<?>>) field.get(resttemplate); optional<httpmessageconverter<?>> opconverter = orgconverterlist.stream() .filter(x -> x.getclass().getname().equalsignorecase(mappingjackson2httpmessageconverter. class .getname())) .findfirst(); if (opconverter.ispresent() == false ) { return resttemplate; } messageconverters.add(converter); //添加mappingjackson2httpmessageconverter //添加原有的剩余的httpmessageconverter list<httpmessageconverter<?>> leftconverters = orgconverterlist.stream() .filter(x -> x.getclass().getname().equalsignorecase(mappingjackson2httpmessageconverter. class .getname()) == false ) .collect(collectors.tolist()); messageconverters.addall(leftconverters); system.out.println(string.format( "【httpmessageconverter】原有數量:%s,重新構造后數量:%s" , orgconverterlist.size(), messageconverters.size())); } catch (exception e) { e.printstacktrace(); } resttemplate.setmessageconverters(messageconverters); return resttemplate; } } |
除了一個messageconverters字段,看上去我們不再關心resttemplate那些外部依賴包和內部構造過程,果然干凈簡潔好維護了很多。
3、亂碼問題
這個也是一個非常經典的問題。解決方案非常簡單,找到httpmessageconverter,看看默認支持的charset。abstractjackson2httpmessageconverter是很多httpmessageconverter的基類,默認編碼為utf-8:
abstractjackson2httpmessageconverter
1
2
3
4
5
|
public abstract class abstractjackson2httpmessageconverter extends abstractgenerichttpmessageconverter<object> { public static final charset default_charset = standardcharsets.utf_8; } |
而stringhttpmessageconverter比較特殊,有人反饋過發生亂碼問題由它默認支持的編碼 iso-8859-1 引起:
stringhttpmessageconverter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
/** * implementation of {@link httpmessageconverter} that can read and write strings. * * <p>by default, this converter supports all media types ({@code }), * and writes with a {@code content-type} of {@code text/plain}. this can be overridden * by setting the {@link #setsupportedmediatypes supportedmediatypes} property. * * @author arjen poutsma * @author juergen hoeller * @since 3.0 */ public class stringhttpmessageconverter extends abstracthttpmessageconverter<string> { public static final charset default_charset = standardcharsets.iso_8859_1; /** * a default constructor that uses {@code "iso-8859-1"} as the default charset. * @see #stringhttpmessageconverter(charset) */ public stringhttpmessageconverter() { this (default_charset); } } |
如果在使用過程中發生亂碼,我們可以通過方法設置httpmessageconverter支持的編碼,常用的有utf-8、gbk等。
4、反序列化異常
這是開發過程中容易碰到的又一個問題。因為java的開源框架和工具類非常之多,而且版本更迭頻繁,所以經常發生一些意想不到的坑。
以joda time為例,joda time是流行的java時間和日期框架,但是如果你的接口對外暴露joda time的類型,比如datetime,那么接口調用方(同構和異構系統)可能會碰到序列化難題,反序列化時甚至直接拋出如下異常:
org.springframework.http.converter.httpmessageconversionexception: type definition error: [simple type, class org.joda.time.chronology]; nested exception is com.fasterxml.jackson.databind.exc.invaliddefinitionexception: cannot construct instance of `org.joda.time.chronology` (no creators, like default construct, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information
at [source: (pushbackinputstream);
我在前廠就碰到過,后來為了調用方便,改回直接暴露java的date類型。
當然解決的方案不止這一種,可以使用jackson支持自定義類的序列化和反序列化的方式。在精度要求不是很高的系統里,實現簡單的datetime自定義序列化:
datetimeserializer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
package com.power.demo.util; import com.fasterxml.jackson.core.jsongenerator; import com.fasterxml.jackson.core.jsonprocessingexception; import com.fasterxml.jackson.databind.jsonserializer; import com.fasterxml.jackson.databind.serializerprovider; import org.joda.time.datetime; import org.joda.time.format.datetimeformat; import org.joda.time.format.datetimeformatter; import java.io.ioexception; /** * 在默認情況下,jackson會將joda time序列化為較為復雜的形式,不利于閱讀,并且對象較大。 * <p> * jodatime 序列化的時候可以將datetime序列化為字符串,更容易讀 **/ public class datetimeserializer extends jsonserializer<datetime> { private static datetimeformatter dateformatter = datetimeformat.forpattern( "yyyy-mm-dd hh:mm:ss" ); @override public void serialize(datetime value, jsongenerator jgen, serializerprovider provider) throws ioexception, jsonprocessingexception { jgen.writestring(value.tostring(dateformatter)); } } |
以及datetime反序列化:
datetimedeserializer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
package com.power.demo.util; import com.fasterxml.jackson.core.jsonparser; import com.fasterxml.jackson.core.jsonprocessingexception; import com.fasterxml.jackson.databind.deserializationcontext; import com.fasterxml.jackson.databind.jsondeserializer; import com.fasterxml.jackson.databind.jsonnode; import org.joda.time.datetime; import org.joda.time.format.datetimeformat; import org.joda.time.format.datetimeformatter; import java.io.ioexception; /** * jodatime 反序列化將字符串轉化為datetime **/ public class datetimedeserializer extends jsondeserializer<datetime> { private static datetimeformatter dateformatter = datetimeformat.forpattern( "yyyy-mm-dd hh:mm:ss" ); @override public datetime deserialize(jsonparser jp, deserializationcontext context) throws ioexception, jsonprocessingexception { jsonnode node = jp.getcodec().readtree(jp); string s = node.astext(); datetime parse = datetime.parse(s, dateformatter); return parse; } } |
最后可以在resttemplateconfig類中對常見調用問題進行匯總處理,可以參考如下:
resttemplateconfig
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
|
package com.power.demo.restclient.config; import com.fasterxml.jackson.databind.objectmapper; import com.fasterxml.jackson.databind.module.simplemodule; import com.google.common.collect.lists; import com.power.demo.util.datetimeserializer; import com.power.demo.util.datetimedeserializer; import org.joda.time.datetime; import org.springframework.beans.factory.annotation.autowired; import org.springframework.boot.web.client.resttemplatebuilder; import org.springframework.context.annotation.bean; import org.springframework.http.mediatype; import org.springframework.http.converter.httpmessageconverter; import org.springframework.http.converter.json.mappingjackson2httpmessageconverter; import org.springframework.stereotype.component; import org.springframework.web.client.resttemplate; import java.lang.reflect.field; import java.util.arrays; import java.util.list; import java.util.optional; import java.util.stream.collectors; @component public class resttemplateconfig { // 啟動的時候要注意,由于我們在服務中注入了resttemplate,所以啟動的時候需要實例化該類的一個實例 @autowired private resttemplatebuilder builder; @autowired private objectmapper objectmapper; // 使用resttemplatebuilder來實例化resttemplate對象,spring默認已經注入了resttemplatebuilder實例 @bean public resttemplate resttemplate() { resttemplate resttemplate = builder.build(); //注冊model,用于實現jackson joda time序列化和反序列化 simplemodule module = new simplemodule(); module.addserializer(datetime. class , new datetimeserializer()); module.adddeserializer(datetime. class , new datetimedeserializer()); objectmapper.registermodule(module); list<httpmessageconverter<?>> messageconverters = lists.newarraylist(); mappingjackson2httpmessageconverter converter = new mappingjackson2httpmessageconverter(); converter.setobjectmapper(objectmapper); //不加會出現異常 //could not extract response: no suitable httpmessageconverter found for response type [class ] mediatype[] mediatypes = new mediatype[]{ mediatype.application_json, mediatype.application_octet_stream, mediatype.text_html, mediatype.text_plain, mediatype.text_xml, mediatype.application_stream_json, mediatype.application_atom_xml, mediatype.application_form_urlencoded, mediatype.application_json_utf8, mediatype.application_pdf, }; converter.setsupportedmediatypes(arrays.aslist(mediatypes)); try { //通過反射設置messageconverters field field = resttemplate.getclass().getdeclaredfield( "messageconverters" ); field.setaccessible( true ); list<httpmessageconverter<?>> orgconverterlist = (list<httpmessageconverter<?>>) field.get(resttemplate); optional<httpmessageconverter<?>> opconverter = orgconverterlist.stream() .filter(x -> x.getclass().getname().equalsignorecase(mappingjackson2httpmessageconverter. class .getname())) .findfirst(); if (opconverter.ispresent() == false ) { return resttemplate; } messageconverters.add(converter); //添加mappingjackson2httpmessageconverter //添加原有的剩余的httpmessageconverter list<httpmessageconverter<?>> leftconverters = orgconverterlist.stream() .filter(x -> x.getclass().getname().equalsignorecase(mappingjackson2httpmessageconverter. class .getname()) == false ) .collect(collectors.tolist()); messageconverters.addall(leftconverters); system.out.println(string.format( "【httpmessageconverter】原有數量:%s,重新構造后數量:%s" , orgconverterlist.size(), messageconverters.size())); } catch (exception e) { e.printstacktrace(); } resttemplate.setmessageconverters(messageconverters); return resttemplate; } } |
目前良好地解決了resttemplate常用調用問題,而且不需要你寫resttemplate幫助工具類了。
上面列舉的這些常見問題,其實.net下面也有,有興趣大家可以搜索一下微軟的httpclient常見使用問題,用過的人都深有體會。更不用提 restsharp 這個開源類庫,幾年前用的過程中發現了非常多的bug,到現在還有一個反序列化數組的問題困擾著我們,我只好自己造個簡單輪子特殊處理,給我最深刻的經驗就是,很多看上去簡單的功能,真的碰到了依然會花掉不少的時間去排查和解決,甚至要翻看源碼。所以,我們寫代碼要認識到,越是通用的工具,越需要考慮到特例,可能你需要花80%以上的精力去處理20%的特殊情況,這估計也是滿足常見的二八定律吧。
參考:
https://stackoverflow.com/questions/10579122/resttemplate-no-suitable-httpmessageconverter
http://forum.spring.io/forum/spring-projects/android/126794-no-suitable-httpmessageconverter-found
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持服務器之家。
原文鏈接:http://www.cnblogs.com/jeffwongishandsome/p/spring-boot-consume-rest-api-by-resttemplate.html