写在前面

在前面介绍服务发现与消费实现的时候,就使用到了RestTemplate,该对象会使用Ribbon的自动化配置,同时通过配置@LoadBalanced注解来开启客户端负载均衡功能。(前面也说过Ribbon实现了服务消费者的客户端负载均衡功能。)前面的例子演示了通过RestTemplate来发起一个GET或者POST请求,其实是实现对HELLO-SERVICE服务提供的/hello接口进行调用。

接下来介绍RestTemplate针对几种不同请求类型和参数类型的服务调用实现,同时为了加深印象,笔者会通过一个例子来进行验证。

GET请求

首当其冲是Get请求,对于Get请求我们太司空见惯了,在RestTemplate中,我们可以通过以下两种方式来发送一个GET请求。

getForEntity方法使用

getForEntity方法返回了一个ResponseEntity<T>对象,该对象是Spring对HTTP请求响应的封装,其中主要存储了HTTP的几个重要元素,如HTTP请求状态码的枚举对象HttpStatus(也就是常说的404,500等状态码),在它的父类HttpEntity中还存储着HTTP请求的头部信息对象HttPHeaders以及泛型类型的请求体对象。

在ConsumerController类中新建一个myEntity方法,该方法用于对外提供一个/getMyEntity接口,并在内部调用服务提供者resttemplate-provider的provider接口,相应的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@RestController
public class ConsumerController {
@Autowired
private RestTemplate restTemplate;

@GetMapping(value = "/getMyEntity")
public String myEntity(){
ResponseEntity<String> responseEntity = restTemplate.getForEntity("http://RESTTEMPLATE-PROVIDER/provider",String.class);
String body = responseEntity.getBody();
HttpStatus httpStatus = responseEntity.getStatusCode();
int statusCodeValue = responseEntity.getStatusCodeValue();
HttpHeaders httpHeaders = responseEntity.getHeaders();
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append("输出以下信息:").append("<hr>")
.append("responseEntity.getBody():").append(body).append("<hr>")
.append("responseEntity.getStatusCode():").append(httpStatus).append("<hr>")
.append("responseEntity.getStatusCodeValue():").append(statusCodeValue).append("<hr>")
.append("responseEntity.getHeaders():").append(httpHeaders).append("<hr>");

return stringBuffer.toString();
}
}

简单介绍一下上述中的restTemplate.getForEntity()方法,第一个参数是想要调用的服务的地址,请注意此处是通过服务名而不是服务地址来调用服务提供者resttemplate-provider提供的/provider接口(必须是服务名称,如果不是后面会出错,关于这一点后面会着重说明,否则将无法实现客户端的负载均衡)。第二个参数是返回responseEntity对象的类型,此处希望返回字符串类型,因此就传入String.class参数即可。最后返回的是一个字符串类型的ResponseEntity对象,开发者可以根据需要来调用它的不同方法,进而取出想要的数据。

之后启动服务注册中心peer1,两个服务提供者pr1和pr2,以及服务消费者resttemplate-consumer。主意服务提供者pr1和pr2的启动方式,可以在IDEA中通过设置-Dspring.profiles.active=pr1来进行多实例的启动:

同时可以看到这里访问的地址是服务名RESTTEMPLATE-PROVIDER,而不是一个具体的地址,在服务治理框架中,这是一个非常重要的特性,也符合本文开头对于服务治理的解释。

然后启动服务消费者,可以看到Eureka Server服务注册中心信息面板上已经出现了如下信息:

接着在浏览器访问http://peer1:9000/getMyEntity链接就可以看到如下信息:

如果开发者在运行过程中出现如下问题,那么极有可能是你在provider模块中,设置了多个服务名称。

为什么会这样呢,那是因为服务消费者是根据服务名RESTTEMPLATE-PROVIDER,而不是一个具体的地址来访问服务,因此无论某个服务提供者创建多少个服务实例,都只有唯一一个服务名,所以请不要在服务实例中取若干个名字,诸如下面的做法是错误的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
spring:
application:
name: resttemplate-provider
---
spring:
profiles: pr1
# 错误的做法
eureka:
instance:
hostname: pr1
---
spring:
profiles: pr2
# 错误的做法
eureka:
instance:
hostname: pr2

再来回过头分析下面的运行结果:

可以看到这些信息就是左侧方法的返回结果,因此开发者可以从responseEntity中得到想要的数据。这里比较重要的是RestTemplate.getForEntity()方法,查看该方法的源码可知,一共有三个重载实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public <T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Object... uriVariables) throws RestClientException {
RequestCallback requestCallback = this.acceptHeaderRequestCallback(responseType);
ResponseExtractor<ResponseEntity<T>> responseExtractor = this.responseEntityExtractor(responseType);
return (ResponseEntity)nonNull(this.execute(url, HttpMethod.GET, requestCallback, responseExtractor, uriVariables));
}

public <T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Map<String, ?> uriVariables) throws RestClientException {
RequestCallback requestCallback = this.acceptHeaderRequestCallback(responseType);
ResponseExtractor<ResponseEntity<T>> responseExtractor = this.responseEntityExtractor(responseType);
return (ResponseEntity)nonNull(this.execute(url, HttpMethod.GET, requestCallback, responseExtractor, uriVariables));
}

public <T> ResponseEntity<T> getForEntity(URI url, Class<T> responseType) throws RestClientException {
RequestCallback requestCallback = this.acceptHeaderRequestCallback(responseType);
ResponseExtractor<ResponseEntity<T>> responseExtractor = this.responseEntityExtractor(responseType);
return (ResponseEntity)nonNull(this.execute(url, HttpMethod.GET, requestCallback, responseExtractor));
}
getForEntity方法详解

接下来对上面三个重载的getForEntity方法进行详细介绍。

(1)getForEntity(String url, Class<T> responseType, Object... uriVariables),该方法提供了三个参数,其中url为想要调用的服务的地址(通过服务名调用),responseType是请求响应体body的包装类型,uriVariables为url中的参数绑定。

举个例子来说,有时候需要在调用服务Url的时候传递参数。我们知道Get请求的参数绑定通常采用在url中拼接的方式,如下面这个例子:

1
restTemplate.getForEntity("http://RESTTEMPLATE-PROVIDER/provider?name=envy",String.class);

这种方式固然可以,但是看起来似乎不优雅,此时可以采用占位符的方式配合uriVariables参数来实现Get请求的参数绑定,此时上面的代码可以修改为:

1
restTemplate.getForEntity("http://RESTTEMPLATE-PROVIDER/provider?name={1}",String.class,"envy");

这样第三个参数uriVariables就会替换url中{1}的占位符,但是需要注意的是,由于uriVariables是一个Obejct类型的数组,所以它的顺序会对应url中占位符定义的数字顺序。


(2)getForEntity(String url, Class<T> responseType, Map<String, ?> uriVariables),该方法提供了三个参数,其中url为想要调用的服务的地址(通过服务名调用),responseType是请求响应体body的包装类型,uriVariables为url中的参数绑定,注意现在使用的是Map类型,也就说之前是采用数组下标的方式来进行占位,那么下奶应当使用map对象中的key来进行占位。

同样还是以上面的例子来进行说明,和第一种方式不同的是此处需要在Map类型的uriVariables对象中,put一个key为name的参数来绑定url中{name}占位符的值,如下所示:

1
2
3
4
5
6
7
@GetMapping(value = "/entityThree")
public String entityThree(){
Map<String,String> map = new HashMap<>();
map.put("name","envy");
ResponseEntity<String> responseEntity = restTemplate.getForEntity("http://RESTTEMPLATE-PROVIDER/provider?name={name}",String.class,map);
return responseEntity.getBody();
}

(3)getForEntity(URI url, Class<T> responseType),该方法提供了两个参数,其中URI对象用于替代之前的url和uriVariables参数来指定访问地址和参数绑定。URI是JDK中java.net包下的一个类,它表示一个统一资源标识符(Uniform Resource Identifier)引用。Spring中也提供了UriComponents来构建Uri,非常方便。

同样使用URI来改写上面的例子,代码如下所示:

1
2
3
4
5
6
7
8
@GetMapping(value = "/entityFour")
public String entityFour(){
UriComponents uriComponents = UriComponentsBuilder.fromUriString("http://RESTTEMPLATE-PROVIDER/provider?name={name}")
.build().expand("envy").encode();
URI uri = uriComponents.toUri();
ResponseEntity<String> responseEntity = restTemplate.getForEntity(uri,String.class);
return responseEntity.getBody();
}

有人可能要问了,服务提供者不一定就是返回String对象,还可能是自定义类型的对象,的确如此。

我们在commons模块中自定义了一个Movie对象,接下来就尝试返回一个Movie对象。首先需要在服务提供者内定义一个方法,用于返回Movie对象,如下所示:

1
2
3
4
@GetMapping(value = "/watchMovie")
public Movie watch(){
return new Movie("朝花夕拾",128.00,"鲁迅",new Date(1911,5,4));
}

之后回到服务消费者consumer中,通过如下方式来调用这个服务接口:

1
2
3
4
5
@GetMapping(value = "/movie")
public Movie movie(){
ResponseEntity<Movie> responseEntity = restTemplate.getForEntity("http://RESTTEMPLATE-PROVIDER/watchMovie", Movie.class);
return responseEntity.getBody();
}

最后启动服务注册中心、服务提供者和服务消费者,然后在浏览器地址栏中访问http://peer1:9000/movie,结果如下图所示:

这样关于getForEntity方法的介绍就到此为止,接下来介绍另一个方法:getForObject。

getForObject方法

getForObject方法可以理解为是对getForEntity的进一步封装,它通过HttpMessageConverterExtractor来对Http的请求响应体body内容进行对象转换,以实现请求直接返回包装好的对象内容。查看一下该getForObjectHttpMessageConverterExtractor方法的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public <T> T getForObject(String url, Class<T> responseType, Object... uriVariables) throws RestClientException {
RequestCallback requestCallback = this.acceptHeaderRequestCallback(responseType);
HttpMessageConverterExtractor<T> responseExtractor = new HttpMessageConverterExtractor(responseType, this.getMessageConverters(), this.logger);
return this.execute(url, HttpMethod.GET, requestCallback, responseExtractor, (Object[])uriVariables);
}

HttpMessageConverterExtractor(Type responseType, List<HttpMessageConverter<?>> messageConverters, Log logger) {
Assert.notNull(responseType, "'responseType' must not be null");
Assert.notEmpty(messageConverters, "'messageConverters' must not be empty");
Assert.noNullElements(messageConverters, "'messageConverters' must not contain null elements");
this.responseType = responseType;
this.responseClass = responseType instanceof Class ? (Class)responseType : null;
this.messageConverters = messageConverters;
this.logger = logger;
}

同时可以发现这个getForObject方法返回的就是body,因此当开发者不需要关注请求响应除body之外的内容时,优先推荐使用getForObject方法,因为它比之前少一个从Response中获取body的步骤。

那么此时为了返回一个Movie对象,之前的代码可以修改为如下所示:

1
2
3
4
5
@GetMapping(value = "/objectOne")
public Movie objectOne(){
Movie movie = restTemplate.getForObject("http://RESTTEMPLATE-PROVIDER/watchMovie", Movie.class);
return movie;
}

既然是对getForEntity方法的封装,那么它同样有三个重载的方法,如下图所示:

由于这些方法的使用和getForEntity三个重载方法的使用非常相似,因此笔者就跳过介绍。

POST请求

说完了Get请求,接下来学习RestTemplate中如何对POST请求进行调用。在RestTemplate中,我们可以通过以下三种方式来调用使用POST请求。

postForEntity方法

postForEntity方法和Get请求中的getForEntity方法非常相似,会在调用后返回一个ResponseEntity对象,其中T为请求响应的body类型。

同样举一个例子,首先需要在服务消费者内定义一个方法,用于返回Movie对象,请注意此处尽管是介绍POST请求,但这是RestTemplate中的postForEntity方法发起的POST请求。你可能会有些好奇,这里为什么先在服务消费者内定义一个方法,而不是和之前一样在服务提供者内先定义呢?那是因为首先必须在服务消费者内使用postFotEntity方法来提交POST请求到RESTTEMPLATE-PROVIDER服务的 /postOneMovie接口,注意提交的body内容为Movie对象,请求响应返回的body类型为也为Movie类型。往服务消费者ConsumerController类中新增postOne方法,如下所示:

1
2
3
4
5
6
7
@GetMapping(value = "/postOne")
public Movie postOne(){
Movie movie = new Movie();
movie.setAuthor("张艺谋");
ResponseEntity<Movie> responseEntity = restTemplate.postForEntity("http://RESTTEMPLATE-PROVIDER/postOneMovie",movie,Movie.class);
return responseEntity.getBody();
}

简单解释一下上述restTemplate.postForEntity方法中的三个参数,第一个参数表示希望调用的服务的地址;第二个参数表示上传的参数;第三个参数表示返回的消息体的数据类型。

笔者在这个方法中创建了一个Movie对象,该对象只有一个author属性,然后当开发者调用这个postOne接口的时候,会将这个POST请求传递到服务提供者RESTTEMPLATE-PROVIDER的/postOneMovie接口中。那么此时就可以在服务提供者/postOneMovie接口中得到这个Movie对象,然后可以进行一些自定义操作,相应的代码如下:

1
2
3
4
5
6
7
8
@PostMapping(value = "/postOneMovie")
public Movie postOneMovie(@RequestBody Movie movie){
System.out.println(movie.getAuthor());
movie.setName("十面埋伏");
movie.setPrice(118.00);
movie.setOnTime(new Date(2002,1,1));
return movie;
}

也就是说服务提供者在接收到服务消费者传过来的Movie对象后,就可以按照自己的要求来进行相应的操作。

最后启动服务注册中心、服务提供者和服务消费者,然后在浏览器地址栏中访问http://peer1:9000/postOne,结果如下图所示:

同样对于postForEntity方法来说,它也有三个重载方法,如下所示:

1
2
3
postForEntity(String url, @Nullable Object request, Class<T> responseType, Object... uriVariables)
postForEntity(String url, @Nullable Object request, Class<T> responseType, Map<String, ?> uriVariables)
postForEntity(URI url, @Nullable Object request, Class<T> responseType)

这些方法中的参数用法大部分与getForEntity方法中的参数用法一致。比如第一个重载函数和第二个重载函数中的uriVariables参数都是用来对url中的参数进行绑定使用;responseType参数是对请求响应的body内容的类型定义。这里需要注意的是各个方法中的第二个参数request,该参数是一个Object类型的对象,因此可以是一个普通对象,也可以是一个HttpEntity对象。

如果是一个普通对象,而非HttpEntity对象的时候,RestTemplate会将请求对象转换为一个HttpEntity对象来处理,其中Object就是request的类型,request内容会被视作完整的body来处理;而如果request是一个HttpEntity对象,那么就会被当作一个完成的HTTP请求对象来处理,这个request中不仅包含了body的内容,也包含了header的内容。

postForObject方法

postForObject方法和Get请求中的getForObject方法非常相似,用于简化postForEntity的后续处理。

同样如果你只关系返回的消息体,那么可以直接使用postForObject方法,它可以直接将请求响应的body内容包装成对象来返回使用。

接下来通过一个例子来进行学习,此处调用了postForObject方法来直接返回一个Movie对象,可以看到相比于之前采用postForEntity方法还需要借助于responseEntity.getBody()方法来返回一个Movie对象的做法,这里的做法就比较精简,直接就返回了一个对象:

1
2
3
4
5
6
7
@GetMapping(value = "/postTwo")
public Movie postTwo(){
Movie movie = new Movie();
movie.setAuthor("张艺谋");
Movie result = restTemplate.postForObject("http://RESTTEMPLATE-PROVIDER/postOneMovie",movie,Movie.class);
return result;
}

同样对于postForObject方法来说,它也有三个重载方法,如下图所示:

这些方法中的参数用法大部分与postForEntity方法中的参数用法一致,因此不再赘述。

postForLocation方法

postForLocation方法实现了以POST请求提交资源并返回新资源的URI。postForLocation方法的参数和前面postForEntity、postForObject方法的参数基本上是一致的,唯一不同的是postForLocation方法的返回值为Uri类型,也就是新资源的URI。

接下来通过一个例子来进行学习,此处调用了postForLocation方法来直接返回一个Uri对象,相应的代码为:

1
2
3
4
5
6
@GetMapping(value = "/postThree")
public void postThree(){
Movie movie = new Movie();
movie.setAuthor("张艺谋");
URI uri = restTemplate.postForLocation("http://RESTTEMPLATE-PROVIDER/postOneMovie",movie,Movie.class);
}

同样对于postForLocation方法来说,它也有三个重载方法,如下图所示:

由于postForLocation方法会返回新资源的URI,该URI就相当于指定了返回类型,所以此方法实现的POST请求不需要像postForEntity和postForObject那样指定responseType,至于其他参数的用法和前面保持一致。

PUT请求

在RestTemplate中对于PUT请求可以通过put方法来进行调用。

接下来通过一个例子来进行学习,此处调用了put方法,该方法没有返回值,其中movie是需要提交的对象,{1}是占位符,后面的”风华”用于替换它,相应的代码为:

1
2
3
4
5
6
@GetMapping(value = "/putOne")
public void putOne(){
Movie movie = new Movie();
movie.setAuthor("冯小刚");
restTemplate.put("http://RESTTEMPLATE-PROVIDER/putOneMovie/{1}",movie,"风华");
}

同样对于put方法来说,它也有三个重载方法,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void put(String url, @Nullable Object request, Object... uriVariables) throws RestClientException {
RequestCallback requestCallback = this.httpEntityCallback(request);
this.execute(url, HttpMethod.PUT, requestCallback, (ResponseExtractor)null, (Object[])uriVariables);
}

public void put(String url, @Nullable Object request, Map<String, ?> uriVariables) throws RestClientException {
RequestCallback requestCallback = this.httpEntityCallback(request);
this.execute(url, HttpMethod.PUT, requestCallback, (ResponseExtractor)null, (Map)uriVariables);
}

public void put(URI url, @Nullable Object request) throws RestClientException {
RequestCallback requestCallback = this.httpEntityCallback(request);
this.execute(url, HttpMethod.PUT, requestCallback, (ResponseExtractor)null);
}

请注意put方法是void类型,也是就没有返回类型,因此就不存在前面所说的responseType参数,其他的参数和用法就和postForObject差不多。

DELETE请求

在RestTemplate中对于DELETE请求可以通过delete方法来进行调用。

接下来通过一个例子来进行学习,此处调用了delete方法,该方法没有返回值,其中{1}是占位符,后面的”风华”用于替换它,相应的代码为:

1
2
3
4
@GetMapping(value = "/deleteOne")
public void deleteOne(){
restTemplate.delete("http://RESTTEMPLATE-PROVIDER/putOneMovie/{1}","风华");
}

同样对于delete方法来说,它也有三个重载方法,如下所示:

1
2
3
4
5
6
7
8
9
10
11
public void delete(String url, Object... uriVariables) throws RestClientException {
this.execute(url, HttpMethod.DELETE, (RequestCallback)null, (ResponseExtractor)null, (Object[])uriVariables);
}

public void delete(String url, Map<String, ?> uriVariables) throws RestClientException {
this.execute(url, HttpMethod.DELETE, (RequestCallback)null, (ResponseExtractor)null, (Map)uriVariables);
}

public void delete(URI url) throws RestClientException {
this.execute(url, HttpMethod.DELETE, (RequestCallback)null, (ResponseExtractor)null);
}

由于我们在进行REST请求时,通常都将DELETE请求的唯一标识拼接在url中,所以DELETE请求也不需要request的body信息,就如上面的三个函数实现一样,非常简单。url指定DELETE请求的位置,uriVariables绑定url中的参数即可。

其实除了上述介绍的get、post、delete和put以外,还有patch、head等方法,只是不常用因此笔者就没有介绍。