Spring Boot 2系列(二十一):RestTemplate 远程调用 REST 服务

  互联网项目经常存在远程调用的情况,如果调用的是 REST 远程服务,可以使用 Spring Web 提供的RestTemplate.

  RestTemplate 是原始的 Spring REST 同步请求客户端,通过 HTTP 客户端提供更高级别的 API,使得调用 REST 端点变更更容易。

  Spring Boot 没有自动配置 RestTemplate,但自动注册了 RestTemplateBuilder Bean,用于构建 RestTemplate,并且 HttpMessageConverters 会自动应用到 RestTemplate 实例中。

  如果是 WebFlux 项目,可以使用 WebClient 来远程调用 REST 服务,相比 RestTemplate,WebClient 拥有更多的功能,并且是完全响应式, 后续再对 WebClient 进行详解。

  Spring Boot > Calling REST Services with RestTemplateSpring Framework > Using RestTemplate

RestTemplate

初始化

RestTemplate 默认使用的是 java.net.HttpURLConnection 来执行请求,可以切换成不同的实现了ClientHttpRequestFactory 接口的 HTTP 库,如:Apache HttpComponents,Netty,OkHttp。

示例:使用 Apache HttpComponents

1
RestTemplate template = new RestTemplate(new HttpComponentsClientHttpRequestFactory());

每个 ClientHttpRequestFactory 公开的配置项都是基于 HTTP Client 底层库的支持,如:凭据,连接池等。

注意: java.net 实现的 HTTP 请求可能在访问表示错误的响应状态时会引发异常,如:401。如果是这个问题,切换到其它的 HTTP Client 库。

URIs

RestTemplate 方法可以接收 URI 模板和 URI 模板变量,可以是 String 变量,也可以是 Map<String, String>。

1
2
3
4
5
// uri 路径传参
String result = restTemplate.getForObject("http://example.com/hotels/{hotel}/bookings/{booking}", String.class, "42", "21");
// map 传参
Map<String, String> vars = Collections.singletonMap("hotel", "42");
String result = restTemplate.getForObject("http://example.com/hotels/{hotel}/rooms/{hotel}", String.class, vars);

注意:URI templates are automatically encoded(自动编码),如下:

1
2
restTemplate.getForObject("http://example.com/hotel list", String.class);
Results in request to "http://example.com/hotel%20list" (实际请求连接)

也可以使用 RestTemplate 的 uriTemplateHandler 属性来自定义 URI 的编码方式,或创建一个 java.net.URI 传给接收 URI 的 RestTemplate方法。如下,更多 URI编码请详见

Headers

可以使用 exchange() 方法来指定请求头信息,如下:

1
2
3
4
5
6
7
8
9
10
11
String uriTemplate = "http://example.com/hotels/{hotel}";
URI uri = UriComponentsBuilder.fromUriString(uriTemplate).build(42);

RequestEntity<Void> requestEntity = RequestEntity.get(uri)
.header(("MyRequestHeader", "MyValue")
.build();

ResponseEntity<String> response = template.exchange(requestEntity, String.class);

String responseHeader = response.getHeaders().getFirst("MyResponseHeader");
String body = response.getBody();

通过 RestTemplate 灵活的方法,可以让返回的 ResponseEntity 会包含响应头。

Body

传入并从 RestTemplate 方法返回的对象,是通过 HttpMessageConverter 使原始内容和对象之间实现相互转换。

  1. 在一个 POST 请求中,一个输入对象被序列化到请求体中:

    1
    URI location = template.postForLocation("http://example.com/people", person);

    默认情况下,可以不显式设置请求的 Content-Type, 底层会根据 源对象类型 找到合适的消息转换器来设置 Content-Type,若有特殊需求,可以使用 exchange() 方法来显式设置 Content-Type,相当于显式指定了消息转换器。

  2. 在一个 GET 请求中,响应体被反序列化为输出对象。

    1
    Person person = restTemplate.getForObject("http://example.com/people/{id}", Person.class, 42);

    不需要显式设置请求的 Accept 头。大多数情况下,可以根据预期的响应类型找到兼容的消息转换器,然后帮助填充 Accept 头。如果有特殊需求,可使用 exchange() 方法显式提供 Accept 头。

    默认情况下,RestTemplate 会注册所有内置消息转换器,具体取决于在类路径下能检查到的可选转换库。还可以显式设置消息转换器。

Message Conversion

参考 Spring Framework > Message Conversion

Jackson JSON Views

可以指定 Jackson JSON View 来只序列化对象属性的子集,如以下示例所示:

1
2
3
4
5
6
7
MappingJacksonValue value = new MappingJacksonValue(new User("eric", "7!jd#h23"));
value.setSerializationView(User.WithoutPasswordView.class);

RequestEntity<MappingJacksonValue> requestEntity =
RequestEntity.post(new URI("https://example.com/user")).body(value);

ResponseEntity<String> response = template.exchange(requestEntity, String.class);

Multipart

要发送 Multipart 数据,需要提供 MultiValueMap<String, ?>,其值为部分内容的 Object 实例,或表示部分内容和头的 HttpEntity 实例。

MultipartBodyBuilder 提供了一个方便的 API 来准备 Multipart 请求,如下示例所示:

1
2
3
4
5
6
MultipartBodyBuilder builder = new MultipartBodyBuilder();
builder.part("fieldPart", "fieldValue");
builder.part("filePart", new FileSystemResource("...logo.png"));
builder.part("jsonPart", new Person("Jason"));

MultiValueMap<String, HttpEntity<?>> parts = builder.build();

在大多数情况下,不必为每个部件(part)指定 Content-Type。内容类型是根据选择用于序列化它的 HttpMessageConverter 自动确定的,在有资源文件的情况下,是基于文件扩展名的。若有必要,可以通过重载构建部分方法为每个部件(part)显式提供 MediaType

一旦 MultiValueMap 准备好后,可以将其传递给 RestTemplate ,如下示例:

1
2
MultipartBodyBuilder builder = ...;
template.postForObject("https://example.com/upload", builder.build(), Void.class);

如果 MultiValueMap 包含至少一个非 String 值,该值也可以表示常规表单数据(即 application / x-www-form-urlencoded ),则无需将 Content-Type 设置为 multipart / form-data 。当使用 MultipartBodyBuilder 确保 HttpEntity 包装器时,情况总是如此。

RestTemplateBuilder

  1. Build

    RestTemplateBuilder 包含多个有用的方法来快速的配置 RestTemplate ,如添加 BASIC auth 支持,可以使用 builder.basicAuthorization(“user”, “password”).build();

    RestTemplate 默认会注册所有内置的消息转换器,具体的选择取决于类路径下有那些转换库可被选择使用。可以人为显式设置消息转换器。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @Service
    public class MyService {

    private final RestTemplate restTemplate;

    public MyService(RestTemplateBuilder restTemplateBuilder) {
    this.restTemplate = restTemplateBuilder.build();
    }

    public Details someRestCall(String name) {
    return this.restTemplate.getForObject("/{name}/details", Details.class, name);
    }
    }
  2. 自定义

    Spring Boot 提供了 RestTemplateBuilder 的自动配置, 几乎不需要通过自定义来创建 RestTemplateBuilder, 如果自定义则会关闭 RestTemplateBuilder 的自动配置。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    import org.springframework.boot.web.client.RestTemplateBuilder;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.client.RestTemplate;

    @Configuration
    public class RestTemplateConfig {

    @Bean
    public RestTemplate customRestTemplate(){
    //设置连接和读超时时间,毫秒
    // 2.1.0 版本方式
    return new RestTemplateBuilder().setConnectTimeout(Duration.ofMillis(1000)).setReadTimeout(Duration.ofMillis(1000)).build();
    // 2.0.x 版本方式
    //return new RestTemplateBuilder().setConnectTimeout(1000).setReadTimeout(1000).build();
    }
    }
  3. 配置代理

    示例:为除 192.168.0.5 之外的所有主机配置代理请求。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    static class ProxyCustomizer implements RestTemplateCustomizer {
    @Override
    public void customize(RestTemplate restTemplate) {
    HttpHost proxy = new HttpHost("proxy.example.com");
    HttpClient httpClient = HttpClientBuilder.create()
    .setRoutePlanner(new DefaultProxyRoutePlanner(proxy) {

    @Override
    public HttpHost determineProxy(HttpHost target,
    HttpRequest request, HttpContext context)
    throws HttpException {
    if (target.getHostName().equals("192.168.0.5")) {
    return null;
    }
    return super.determineProxy(target, request, context);
    }

    }).build();
    restTemplate.setRequestFactory(
    new HttpComponentsClientHttpRequestFactory(httpClient));
    }
    }

使用 RestTemplate

RestTemplate 请求方法

RestTemplate 提供了多个内部重载的方法组,其中 xxxForObject 和 xxxForEntity 两组是用的比较多的,两者在使用上基本没有区别,区别是在返回的内容上:

  • xxxForObject:返回的消息只有响应体,没有响应头。
  • xxForEntity:返回的消息中有包含响应头和响应体; 返回值是一个 ResponseEntit,ResponseEntity是Spring对HTTP 请求响应的封装,包括了几个重要的元素,如响应码、contentType、contentLength、响应消息体等。
方法组 描述
getForObject 使用 Get 请求,返回数据的类型是 Object,只有实体数据,不包含响应头数据
getForEntity 使用 Get 请求,返回数据的类型是 ResponseEntity,包含状态码,响应头,响应体数据
headForHeaders 使用 HEAD 请求,获取资源的所有头信息
postForLocation 使用 POST 创建新资源并返回新资源的 URI
postForObject 使用 POST 创建新资源并返回响应体中的数据
postForEntity 使用 POST 创建新资源并返回 ResponseEntity<T>
put 使用 PUT 创建或更新资源
patchForObject 使用 PATCH 更新资源并返回响应体
JDK自带的 HttpURLConnection 不支持,但Apache HttpComponents和其它支持
delete 使用 DELETE 删除指定 RUI 的资源
optionsForAllow 检查资源允许 HTTP 访问的请求方式
exchange 提供额外的灵活的操作,可接收 RequestEntity(包括 HTTP 方法,URL,Header,输入的Body),返回 ResponseEntity
execute 执行请求最常用的方式,通过回调接口完全控制请求前的准备工作和取出响应消息

RestTemplate 请求传参

  1. 没有请求参数

    1
    Object object = restTemplate.getForObject("https://example.com/hotels", Object.class);
  2. 路径变量传参

    1
    2
    3
    //按顺序取值填充到 URI 中
    String result = restTemplate.getForObject(
    "https://example.com/hotels/{hotel}/bookings/{booking}", String.class, "42", "21");
  3. Map 传参

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    //Map 传参,map 的 Key 与路径变量名相同
    Map<String, Long> varMap = new HashMap<>(1);
    varMap.put("cityId", cityId);
    //getForObject方法
    City city3 = restTemplate.getForObject("http://localhost:8080/city/{cityId}", City.class, varMap);
    //getForEntity方法
    ResponseEntity<City> responseEntity4 = restTemplate.getForEntity("http://localhost:8080/city/{cityId}", City.class, varMap);

    //POST 发送 Map 请求,Map 类型只能是 MultiValueMap
    MultiValueMap<String, String> paramMap = new LinkedMultiValueMap<String, String>();
    paramMap.add("userName","admin");
    restTemplate.postForEntity(url, map, String.class);
  4. 传实体类参数

    传实体类作为请求参数,只能使用 POST 方法。

    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
    public String postMethod(City city) {

    String url = "http://localhost:8080/city";
    String uriStr = null;

    //自定义请求头
    HttpHeaders headers = new HttpHeaders();
    MediaType type = MediaType.parseMediaType("application/json; charset=UTF-8");
    headers.setContentType(type);
    headers.add("Accept", MediaType.APPLICATION_JSON.toString());

    HttpEntity<String> formEntity = new HttpEntity<String>(JSON.toJSONString(city), headers);

    //rest服务中 Controller 方法使用对象无法接收参数
    //URI uri = restTemplate.postForLocation(url, city);
    //rest 服务 Controller 方法中 @RequestBody 注解不支持默认的 text/plain;charset=ISO-8859-1
    //URI uri = restTemplate.postForLocation(url, JSON.toJSONString(city));

    //1.只能发送对象字符串,否则Rest服务接收不到参数
    //2.字符串默认数据类型和编码是text/plain;charset=ISO-8859-1,
    // 调用的Rest服务的Controller方法中使用 @RequestBody 解析参数封装到对象中,
    // 而@RequestBody注解解析的是JSON数据,所以需要设置消息头告诉数据类型和编码

    //postForLocation 方法
    URI uri = restTemplate.postForLocation(url, formEntity);
    if (uri != null) {
    uriStr = JSON.toJSONString(uri);
    }
    //return uriStr;

    //postForEntity 方法, 发送的是 JSON 字符串, 接收端使用 @RequestBody 注解解析
    ResponseEntity<City> cityResponseEntity = restTemplate.postForEntity(url, city, City.class);

    //使用 postForEntity 请求接口
    HttpHeaders headers = new HttpHeaders();
    HttpEntity<MultiValueMap<String, Object>> httpEntity = new HttpEntity<MultiValueMap<String, Object>>(paramMap, headers);
    ResponseEntity<String> respon = template.postForEntity(url, httpEntity, String.class);

    //postForObject方法, 发送的是 JSON 字符串, 接收端使用 @RequestBody 注解解析
    City city1 = restTemplate.postForObject(url, city, City.class);
    }

RestTemplate 请求示例

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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
@Service
public class CallRemoteServiceImpl implements CallRemoteService {

private static final Logger logger = LogManager.getLogger(CallRemoteServiceImpl.class);

@Autowired
private RestTemplate restTemplate;

/**
* getForEntity()
* 获取数据-所有
* 包含响应头,响应体数据
*/
@Override
public String getForEntityForAll() {
String uri = "http://localhost:8080/city";
ResponseEntity<Object> actorResponseEntity = null;
try {
actorResponseEntity = restTemplate.getForEntity(uri, Object.class);
} catch (RestClientException e) {
e.printStackTrace();
}
String cityListStr = JSON.toJSONString(actorResponseEntity);
return cityListStr;
}

/**
* getForObject()
* 返回的只有实体数据,没有响应头的数据
*/
@Override
public String getForObjectForAll() {
String uri = "http://localhost:8080/city";
Object object = restTemplate.getForObject(uri, Object.class);
String cityListStr = JSON.toJSONString(object);
logger.info("cityListStr:{}" + cityListStr);
return cityListStr;
}


/**
* getForEntity()
* 获取数据-参数
*/
public City getForEntityByCityId(Long cityId) {
String uri = "http://localhost:8080/city/" + cityId;
ResponseEntity<Object> responseEntity = restTemplate.getForEntity(uri, Object.class);

//Body是LinkHashMap类型
/*Object body = responseEntity.getBody();
String botyStr = JSON.toJSONString(body);
City city = JSON.parseObject(botyStr, City.class);*/

//从响应体中拿取实体类数据
City city = JSON.parseObject(JSON.toJSONString(responseEntity.getBody()), City.class);

return city;
}

/**
* postForLocation()
* 添加数据
*/
public String postForLocationForCity(City city) {
String uriStr = null;
HttpHeaders headers = new HttpHeaders();
MediaType type = MediaType.parseMediaType("application/json; charset=UTF-8");
headers.setContentType(type);
headers.add("Accept", MediaType.APPLICATION_JSON.toString());

HttpEntity<String> formEntity = new HttpEntity<String>(JSON.toJSONString(city), headers);

String url = restServerProperties.getUrl() + "/city";

//rest服务中Controller方法使用对象无法接收参数
// URI uri = restTemplate.postForLocation(url, city);
//rest服务中Controller方法中 @RequestBody 注解不支持默认的text/plain;charset=ISO-8859-1
// URI uri = restTemplate.postForLocation(url, JSON.toJSONString(city));

//1.只能发送对象字符串,否则Rest服务接收不到参数
//2.字符串默认数据类型和编码是text/plain;charset=ISO-8859-1,
// 调用的Rest服务的Controller方法中使用 @RequestBody 解析参数封装到对象中,
// 而@RequestBody注解解析的是JSON数据,所以需要设置消息头告诉数据类型和编码
URI uri = restTemplate.postForLocation(url, formEntity);
if(uri != null){
uriStr = JSON.toJSONString(uri);
}
return uriStr;
}

/**
* postForEntity()
* 添加数据
*/
public String postForEntityForCity(City city) {
String url = restServerProperties.getUrl() + "/city";
ResponseEntity<City> cityResponseEntity = restTemplate.postForEntity(url, city, City.class);
String str = JSON.toJSONString(cityResponseEntity);
return str;
}

/**
* postForObject()
* 添加数据
*/
public String postForObjectForCity(City city) {
String url = restServerProperties.getUrl() + "/city";
City city1 = restTemplate.postForObject(url, city, City.class);
String str = JSON.toJSONString(city1);
return str;
}

/**
* put()
* 更新数据
*/
public void putForCity(Long cityId, String cityName) {
String url = restServerProperties.getUrl() + "/city" + "/" + cityId + "/" + cityName;
HttpHeaders headers = new HttpHeaders();
MediaType type = MediaType.parseMediaType("application/json; charset=UTF-8");
headers.setContentType(type);
headers.add("Accept", MediaType.APPLICATION_JSON.toString());
HttpEntity<String> formEntity = new HttpEntity<String>(JSON.toJSONString(
new City().setCityId(cityId).setCityName(cityName)), headers);
restTemplate.put(url,null);
}

/**
* delete删除
* @param cityId
*/
@Override
public void deleteById(Long cityId) {
String url = restServerProperties.getUrl() + "/city" + "/" + cityId;
restTemplate.delete(url);
}
}

示例源码 -> GitHub

Spring Boot 2系列(二十一):RestTemplate 远程调用 REST 服务

http://blog.gxitsky.com/2018/06/26/SpringBoot-21-RestTemplate/

作者

光星

发布于

2018-06-26

更新于

2022-06-17

许可协议

评论