RestTemplate详解
写在前面
在前面介绍服务发现与消费实现的时候,就使用到了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 | @RestController |
简单介绍一下上述中的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 | spring: |
再来回过头分析下面的运行结果:
可以看到这些信息就是左侧方法的返回结果,因此开发者可以从responseEntity中得到想要的数据。这里比较重要的是RestTemplate.getForEntity()
方法,查看该方法的源码可知,一共有三个重载实现:
1 | public <T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Object... uriVariables) throws RestClientException { |
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 | @GetMapping(value = "/entityThree") |
(3)getForEntity(URI url, Class<T> responseType)
,该方法提供了两个参数,其中URI对象用于替代之前的url和uriVariables参数来指定访问地址和参数绑定。URI是JDK中java.net包下的一个类,它表示一个统一资源标识符(Uniform Resource Identifier)引用。Spring中也提供了UriComponents来构建Uri,非常方便。
同样使用URI来改写上面的例子,代码如下所示:
1 | @GetMapping(value = "/entityFour") |
有人可能要问了,服务提供者不一定就是返回String对象,还可能是自定义类型的对象,的确如此。
我们在commons模块中自定义了一个Movie对象,接下来就尝试返回一个Movie对象。首先需要在服务提供者内定义一个方法,用于返回Movie对象,如下所示:
1 | @GetMapping(value = "/watchMovie") |
之后回到服务消费者consumer中,通过如下方式来调用这个服务接口:
1 | @GetMapping(value = "/movie") |
最后启动服务注册中心、服务提供者和服务消费者,然后在浏览器地址栏中访问http://peer1:9000/movie
,结果如下图所示:
这样关于getForEntity方法的介绍就到此为止,接下来介绍另一个方法:getForObject。
getForObject方法
getForObject方法可以理解为是对getForEntity的进一步封装,它通过HttpMessageConverterExtractor
来对Http的请求响应体body内容进行对象转换,以实现请求直接返回包装好的对象内容。查看一下该getForObject
和HttpMessageConverterExtractor
方法的源码:
1 | public <T> T getForObject(String url, Class<T> responseType, Object... uriVariables) throws RestClientException { |
同时可以发现这个getForObject方法返回的就是body,因此当开发者不需要关注请求响应除body之外的内容时,优先推荐使用getForObject方法,因为它比之前少一个从Response中获取body的步骤。
那么此时为了返回一个Movie对象,之前的代码可以修改为如下所示:
1 | @GetMapping(value = "/objectOne") |
既然是对getForEntity方法的封装,那么它同样有三个重载的方法,如下图所示:
由于这些方法的使用和getForEntity三个重载方法的使用非常相似,因此笔者就跳过介绍。
POST请求
说完了Get请求,接下来学习RestTemplate中如何对POST请求进行调用。在RestTemplate中,我们可以通过以下三种方式来调用使用POST请求。
postForEntity方法
postForEntity方法和Get请求中的getForEntity方法非常相似,会在调用后返回一个ResponseEntity
同样举一个例子,首先需要在服务消费者内定义一个方法,用于返回Movie对象,请注意此处尽管是介绍POST请求,但这是RestTemplate中的postForEntity方法发起的POST请求。你可能会有些好奇,这里为什么先在服务消费者内定义一个方法,而不是和之前一样在服务提供者内先定义呢?那是因为首先必须在服务消费者内使用postFotEntity方法来提交POST请求到RESTTEMPLATE-PROVIDER服务的 /postOneMovie
接口,注意提交的body内容为Movie对象,请求响应返回的body类型为也为Movie类型。往服务消费者ConsumerController类中新增postOne方法,如下所示:
1 | @GetMapping(value = "/postOne") |
简单解释一下上述restTemplate.postForEntity
方法中的三个参数,第一个参数表示希望调用的服务的地址;第二个参数表示上传的参数;第三个参数表示返回的消息体的数据类型。
笔者在这个方法中创建了一个Movie对象,该对象只有一个author属性,然后当开发者调用这个postOne接口的时候,会将这个POST请求传递到服务提供者RESTTEMPLATE-PROVIDER的/postOneMovie
接口中。那么此时就可以在服务提供者/postOneMovie
接口中得到这个Movie对象,然后可以进行一些自定义操作,相应的代码如下:
1 | @PostMapping(value = "/postOneMovie") |
也就是说服务提供者在接收到服务消费者传过来的Movie对象后,就可以按照自己的要求来进行相应的操作。
最后启动服务注册中心、服务提供者和服务消费者,然后在浏览器地址栏中访问http://peer1:9000/postOne
,结果如下图所示:
同样对于postForEntity方法来说,它也有三个重载方法,如下所示:
1 | postForEntity(String url, @Nullable Object request, Class<T> responseType, Object... uriVariables) |
这些方法中的参数用法大部分与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 | @GetMapping(value = "/postTwo") |
同样对于postForObject方法来说,它也有三个重载方法,如下图所示:
这些方法中的参数用法大部分与postForEntity方法中的参数用法一致,因此不再赘述。
postForLocation方法
postForLocation方法实现了以POST请求提交资源并返回新资源的URI。postForLocation方法的参数和前面postForEntity、postForObject方法的参数基本上是一致的,唯一不同的是postForLocation方法的返回值为Uri类型,也就是新资源的URI。
接下来通过一个例子来进行学习,此处调用了postForLocation方法来直接返回一个Uri对象,相应的代码为:
1 | @GetMapping(value = "/postThree") |
同样对于postForLocation方法来说,它也有三个重载方法,如下图所示:
由于postForLocation方法会返回新资源的URI,该URI就相当于指定了返回类型,所以此方法实现的POST请求不需要像postForEntity和postForObject那样指定responseType,至于其他参数的用法和前面保持一致。
PUT请求
在RestTemplate中对于PUT请求可以通过put方法来进行调用。
接下来通过一个例子来进行学习,此处调用了put方法,该方法没有返回值,其中movie是需要提交的对象,{1}是占位符,后面的”风华”用于替换它,相应的代码为:
1 | @GetMapping(value = "/putOne") |
同样对于put方法来说,它也有三个重载方法,如下所示:
1 | public void put(String url, @Nullable Object request, Object... uriVariables) throws RestClientException { |
请注意put方法是void类型,也是就没有返回类型,因此就不存在前面所说的responseType参数,其他的参数和用法就和postForObject差不多。
DELETE请求
在RestTemplate中对于DELETE请求可以通过delete方法来进行调用。
接下来通过一个例子来进行学习,此处调用了delete方法,该方法没有返回值,其中{1}是占位符,后面的”风华”用于替换它,相应的代码为:
1 | @GetMapping(value = "/deleteOne") |
同样对于delete方法来说,它也有三个重载方法,如下所示:
1 | public void delete(String url, Object... uriVariables) throws RestClientException { |
由于我们在进行REST请求时,通常都将DELETE请求的唯一标识拼接在url中,所以DELETE请求也不需要request的body信息,就如上面的三个函数实现一样,非常简单。url指定DELETE请求的位置,uriVariables绑定url中的参数即可。
其实除了上述介绍的get、post、delete和put以外,还有patch、head等方法,只是不常用因此笔者就没有介绍。