Spring Boot使用RestTemplate消费REST服务的几个问题记录

2025-05-29 0 45

我们可以通过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/21854369/no-suitable-httpmessageconverter-found-for-response-type

https://stackoverflow.com/questions/40726145/rest-templatecould-not-extract-response-no-suitable-httpmessageconverter-found

https://stackoverflow.com/questions/10579122/resttemplate-no-suitable-httpmessageconverter

http://forum.spring.io/forum/spring-projects/android/126794-no-suitable-httpmessageconverter-found

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持快网idc。

原文链接:http://www.cnblogs.com/jeffwongishandsome/p/spring-boot-consume-rest-api-by-resttemplate.html

收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。

快网idc优惠网 建站教程 Spring Boot使用RestTemplate消费REST服务的几个问题记录 https://www.kuaiidc.com/111876.html

相关文章

发表评论
暂无评论