Springboot 系列 (18) - 在 Spring Boot 项目里使用 RestTemplate 访问 REST 服务



RestTemplate 是从 Spring 3.0 开始支持的一个 HTTP 请求工具,它提供了常见的 REST 请求方案的模版,例如 GET 请求、POST 请求、PUT 请求、DELETE 请求以及一些通用的请求执行方法 exchange 以及 execute。

RestTemplate 继承自 InterceptingHttpAccessor 并且实现了 RestOperations 接口,其中 RestOperations 接口定义了基本的 RESTful 操作,这些操作在 RestTemplate 中都得到了实现。

RestTemplate 的方法列表如下:
方法 描述
getForObject 使用 GET 请求获取返回数据。
getForEntity 使用 GET 请求获取 ResponseEntity(包含状态、标题和返回数据)。
postForObject 使用 POST 请求创建新资源,并从响应中返回数据。
postForEntity 使用 POST 请求创建新资源,并从响应中返回数据。
postForLocation 使用 POST 请求创建新资源,并从响应中返回 Location 信息。
patchForObject 使用 PATCH 请求更新资源,并从响应中返回数据。请注意,JDK HttpURLConnection 不支持该补丁,Apache HttpComponents 和其他组件支持 PATCH。
headForHeaders 使用 HEAD 请求获取一个资源的 header 信息。
put 使用 PUT 请求创建或更新一个资源。
delete 使用 DELETE 请求删除指定 URI 处的资源。
optionsForAllow 使用 ALLOW 请求为资源检索允许的 HTTP 方法。
exchange 执行请求的通用方法,调用的时候指定请求类型。
execute 执行请求的最通用方法,通过回调接口完全控制请求准备和响应提取。


上述方法大致可以分为三组:

    (1) getForObject ~ optionsForAllow 分为一组,这类方法是常规的 Rest API(GET、POST、DELETE 等)方法调用;
    (2) exchange:接收一个 RequestEntity 参数,可以自己设置 HTTP method,URL,headers 和 body,返回 ResponseEntity;
    (3) execute:通过 callback 接口,可以对请求和返回做更加全面的自定义控制。

本文使用 RestTemplate 的几个常用的方法,创建一个实例访问 REST 服务。


1. 开发环境

    Windows版本:Windows 10 Home (20H2)   
    IntelliJ IDEA (https://www.jetbrains.com/idea/download/):Community Edition for Windows 2020.1.4
    Apache Maven (https://maven.apache.org/):3.8.1

    注:Spring 开发环境的搭建,可以参考 “ ”。


2. 创建 Spring Boot 基础项目

    项目实例名称:SpringbootExample18
    Spring Boot 版本:2.6.6

    创建步骤:

        (1) 创建 Maven 项目实例 SpringbootExample18;
        (2) Spring Boot Web 配置;
        
    具体操作请参考 “” 里的项目实例 SpringbootExample02,文末包含如何使用 spring-boot-maven-plugin 插件运行打包的内容。

    SpringbootExample18 和 SpringbootExample02 相比,SpringbootExample18 不导入 Thymeleaf 依赖包,不配置 jQuery、Bootstrap、模版文件(templates/*.html)和国际化。


3. 配置 RestTemplate

    RestTemplate 默认配置是使用了 JDK 自带的 HttpURLConnection 作为底层 HTTP 客户端实现。可以通过 setRequestFactory 属性来切换使用不同的 HTTP 库,如 Apache HttpComponents、OkHttp、Netty 等。

    本文提供了三种配置方式:默认配置、HttpClient 配置、OkHttp 配置,一般情况下三选一就可以满足需要了。


    1) 默认配置

        默认配置的依赖已经包含在 spring-boot-starter-web 中,无需添加其它依赖。

        创建 src/main/java/com/example/config/RestTemplateConfig.java 文件

 1             package com.example.config;
 2 
 3             import org.springframework.context.annotation.Bean;
 4             import org.springframework.context.annotation.Configuration;
 5             import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
 6             import org.springframework.web.client.RestTemplate;
 7 
 8             @Configuration
 9             public class RestTemplateConfig {
10 
11                 @ConditionalOnMissingBean(RestTemplate.class)
12                 @Bean
13                 public RestTemplate restTemplate(){
14                     RestTemplate restTemplate = new RestTemplate();
15                     return restTemplate;
16                 }
17             }


    2) HttpClient 配置

        (1) 修改 pom.xml,导入 HttpClient 依赖包

 1             <project ... >
 2                 ...
 3                 <dependencies>
 4                     ...
 5 
 6                     <dependency>
 7                         <groupId>org.apache.httpcomponentsgroupId>
 8                         <artifactId>httpclientartifactId>
 9                         <version>4.5.13version>
10                     dependency>
11 
12                     ...
13                 dependencies>
14 
15                 ...
16             project>


            在IDE中项目列表 -> SpringbootExample18 -> 点击鼠标右键 -> Maven -> Reload Project

        (2) 创建 src/main/java/com/example/config/RestTemplateConfig.java 文件

 1             package com.example.config;
 2 
 3             import org.springframework.context.annotation.Bean;
 4             import org.springframework.context.annotation.Configuration;
 5             import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
 6             import org.springframework.http.client.ClientHttpRequestFactory;
 7             import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
 8             import org.apache.http.client.config.RequestConfig;
 9             import org.apache.http.impl.client.CloseableHttpClient;
10             import org.apache.http.impl.client.HttpClientBuilder;
11             import org.springframework.web.client.RestTemplate;
12 
13             @Configuration
14             public class RestTemplateConfig {
15 
16                 @ConditionalOnMissingBean(RestTemplate.class)
17                 @Bean
18                 public RestTemplate restTemplate(){
19                     RestTemplate restTemplate = new RestTemplate(getClientHttpRequestFactory());
20                     return restTemplate;
21                 }
22 
23                 private ClientHttpRequestFactory getClientHttpRequestFactory() {
24                     int timeout = 5000;
25                     RequestConfig config = RequestConfig.custom()
26                             .setConnectTimeout(timeout)
27                             .setConnectionRequestTimeout(timeout)
28                             .setSocketTimeout(timeout)
29                             .build();
30                     CloseableHttpClient client = HttpClientBuilder
31                             .create()
32                             .setDefaultRequestConfig(config)
33                             .build();
34                     return new HttpComponentsClientHttpRequestFactory(client);
35                 }
36 
37             }


    3) OkHttp 配置

        (1) 修改 pom.xml,导入 OkHttp 依赖包

 1             <project ... >
 2                 ...
 3                 <dependencies>
 4                     ...
 5 
 6                     <dependency>
 7                         <groupId>com.squareup.okhttp3groupId>
 8                         <artifactId>okhttpartifactId>
 9                         <version>4.3.1version>
10                     dependency>
11 
12                     ...
13                 dependencies>
14 
15                 ...
16             project>


            在IDE中项目列表 -> SpringbootExample18 -> 点击鼠标右键 -> Maven -> Reload Project

        (2) 创建 src/main/java/com/example/config/RestTemplateConfig.java 文件

 1             package com.example.config;
 2 
 3             import java.util.concurrent.TimeUnit;
 4             import org.springframework.context.annotation.Bean;
 5             import org.springframework.context.annotation.Configuration;
 6             import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
 7             import org.springframework.http.client.ClientHttpRequestFactory;
 8             import org.springframework.http.client.OkHttp3ClientHttpRequestFactory;
 9             import org.springframework.web.client.RestTemplate;
10             import okhttp3.OkHttpClient;
11 
12             @Configuration
13             public class RestTemplateConfig3 {
14 
15                 @ConditionalOnMissingBean(RestTemplate.class)
16                 @Bean
17                 public RestTemplate restTemplate(){
18                     RestTemplate restTemplate = new RestTemplate(getClientHttpRequestFactory());
19                     return restTemplate;
20                 }
21 
22                 private ClientHttpRequestFactory getClientHttpRequestFactory(){
23                     OkHttpClient okHttpClient = new OkHttpClient.Builder()
24                             .connectTimeout(5, TimeUnit.SECONDS)
25                             .writeTimeout(5, TimeUnit.SECONDS)
26                             .readTimeout(5, TimeUnit.SECONDS)
27                             .build();
28                     return new OkHttp3ClientHttpRequestFactory(okHttpClient);
29                 }
30             }


4. 示例

    示例需要用到实体类,为了简化实体类的书写,这里使用了 lombok (https://github.com/projectlombok/lombok)。

    1) 修改 pom.xml,导入 lombok 依赖包

 1         
 2             ...
 3             
 4                 ...
 5 
 6                 
 7                     org.projectlombok
 8                     lombok
 9                     1.18.8
10                 
11 
12                 ...
13             
14 
15             ...
16         


        lombok 提供了一些简化实体类定义的注解,常用的注解如下:

            @Getter 使用方法同上,区别在于生成的是 getter 方法。
            @Setter 注解在类或字段,注解在类时为所有字段生成 setter 方法,注解在字段上时只为该字段生成setter 方法。
            @ToString 注解在类,添加 toString 方法。
            @NoArgsConstructor 注解在类,生成无参的构造方法。
            @AllArgsConstructor 注解在类,生成包含类中所有字段的构造方法。
            @RequiredArgsConstructor 注解在类,为类中需要特殊处理的字段生成构造方法,比如final和被@NonNull注解的字段。
            @EqualsAndHashCode 注解在类,生成 hashCode 和 equals 方法。
            @Data 注解在类,生成 setter/getter、equals、canEqual、hashCode、toString 方法,如为 final 属性,则不会为该属性生成 setter 方法。
            @Slf4j 注解在类,生成 log 变量,严格意义来说是常量。

        IDEA 还需要安装 lombok 插件,这里以 Windows 版的 IDEA 为例,安装步骤如下:

            菜单 File -> Settings -> 选中左侧 Plugin ->  搜索 "lombok" -> Install lombok plugin -> Restart IDEA

        在IDE中项目列表 -> SpringbootExample18 -> 点击鼠标右键 -> Maven -> Reload Project

    2) 创建 src/main/java/com/example/entity/User.java 文件

 1         package com.example.entity;
 2 
 3         import lombok.Data;
 4         import lombok.NoArgsConstructor;
 5         import lombok.experimental.Accessors;
 6         import java.io.Serializable;
 7 
 8         @NoArgsConstructor // 无参构造函数
 9         @Data // 提供类的 get、set、equals、hashCode、canEqual、toString 方法
10         @Accessors(chain = true)
11         public class User implements Serializable {
12             private Integer id;
13             private String username;
14             private String password;
15 
16         }


    3) 创建 src/main/java/com/example/controller/ApiController.java 文件

 1         package com.example.controller;
 2 
 3         import javax.servlet.http.HttpServletResponse;
 4 
 5         import org.springframework.stereotype.Controller;
 6         import org.springframework.web.bind.annotation.PathVariable;
 7         import org.springframework.web.bind.annotation.RequestMapping;
 8         import org.springframework.web.bind.annotation.RequestMethod;
 9         import org.springframework.web.bind.annotation.ResponseBody;
10 
11         import com.example.entity.User;
12 
13         @Controller
14         @RequestMapping(value = "/api")
15         public class ApiController {
16 
17             @ResponseBody
18             @RequestMapping(value = "/user/get", method = RequestMethod.GET)
19             public User getUser(){
20                 User user = new User();
21                 user.setId(1);
22                 user.setUsername("admin");
23                 user.setPassword("123456");
24                 return user;
25             }
26 
27             @ResponseBody
28             @RequestMapping(value = "/user/post", method = RequestMethod.POST)
29             public User postUser(HttpServletResponse response, User user){
30                 response.setHeader("Location", "http://localhost:9090/test");
31                 return user;
32             }
33 
34             @RequestMapping(value = "/user/post/location", method = RequestMethod.POST)
35             public void postLocation(HttpServletResponse response, User user) {
36                 response.setHeader("Location", "http://localhost:9090/test");
37             }
38 
39             @ResponseBody
40             @RequestMapping(value = "/user/put", method = RequestMethod.PUT)
41             public void putUser(User user){
42                 System.out.println("PUT: " + user.toString());
43             }
44 
45             @ResponseBody
46             @RequestMapping(value = "/user/delete/{id}", method = RequestMethod.DELETE)
47             public void deleteUser(@PathVariable Integer id){
48                 System.out.println("DELETE: " + id);
49             }
50         }


    4) 修改 src/main/java/com/example/controller/IndexController.java 文件

  1         package com.example.controller;
  2 
  3         import java.net.URI;
  4         import java.util.Set;
  5         import javax.servlet.http.HttpServletRequest;
  6 
  7         import org.springframework.beans.factory.annotation.Autowired;
  8         import org.springframework.http.*;
  9         import org.springframework.stereotype.Controller;
 10         import org.springframework.util.LinkedMultiValueMap;
 11         import org.springframework.util.MultiValueMap;
 12         import org.springframework.web.bind.annotation.PathVariable;
 13         import org.springframework.web.bind.annotation.RequestMapping;
 14         import org.springframework.web.bind.annotation.ResponseBody;
 15         import org.springframework.web.client.RestTemplate;
 16 
 17         import com.example.entity.User;
 18 
 19         @Controller
 20         public class IndexController {
 21             @Autowired
 22             private RestTemplate restTemplate;
 23 
 24             @ResponseBody
 25             @RequestMapping("/test")
 26             public String test() {
 27                 return "Test Page";
 28             }
 29 
 30             @ResponseBody
 31             @RequestMapping("/user/get/object")
 32             public String getUserObject(HttpServletRequest request) {
 33                 String url = "http://" + request.getServerName() + ":" + request.getServerPort();
 34                 url += request.getContextPath() + "/api/user/get";
 35             // System.out.println("url: " + url);
 36 
 37                 User user = restTemplate.getForObject(url, User.class);
 38                 return user.toString();
 39             }
 40 
 41             @ResponseBody
 42             @RequestMapping("/user/get/entity")
 43             public String getUserEntity(HttpServletRequest request) {
 44                 String url = "http://" + request.getServerName() + ":" + request.getServerPort();
 45                 url += request.getContextPath() + "/api/user/get";
 46 
 47                 ResponseEntity responseEntity = restTemplate.getForEntity(url, User.class);
 48 
 49                 String str = "HTTP status code: " + responseEntity.getStatusCode() + "
"; 50 str += "HTTP status value: " + responseEntity.getStatusCodeValue() + "
"; 51 str += "HTTP headers: " + responseEntity.getHeaders() + "
"; 52 str += "HTTP body: " + responseEntity.getBody().toString() + "
"; 53 54 return str; 55 } 56 57 @ResponseBody 58 @RequestMapping("/user/get/options") 59 public String getUserOptions(HttpServletRequest request) { 60 String url = "http://" + request.getServerName() + ":" + request.getServerPort(); 61 url += request.getContextPath() + "/api/user/get"; 62 63 Set options = restTemplate.optionsForAllow(url); 64 65 return options.toString(); 66 } 67 68 @ResponseBody 69 @RequestMapping("/user/get/headers") 70 public String getUserHeader(HttpServletRequest request) { 71 String url = "http://" + request.getServerName() + ":" + request.getServerPort(); 72 url += request.getContextPath() + "/api/user/get"; 73 74 HttpHeaders httpHeaders = restTemplate.headForHeaders(url); 75 76 return httpHeaders.toString(); 77 } 78 79 @ResponseBody 80 @RequestMapping("/user/post/object") 81 public String postUserObject(HttpServletRequest request) { 82 String url = "http://" + request.getServerName() + ":" + request.getServerPort(); 83 url += request.getContextPath() + "/api/user/post"; 84 85 // 请求头设置, x-www-form-urlencoded格式的数据 86 HttpHeaders headers = new HttpHeaders(); 87 headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); 88 89 // 提交参数设置 90 MultiValueMap map = new LinkedMultiValueMap<>(); 91 map.add("id", "2"); 92 map.add("username", "user"); 93 map.add("password", "123123"); 94 95 // 组装请求体 96 HttpEntity> httpEntity = new HttpEntity<>(map, headers); 97 98 // 发起请求 99 User user = restTemplate.postForObject(url, httpEntity, User.class); 100 return user.toString(); 101 } 102 103 @ResponseBody 104 @RequestMapping("/user/post/entity") 105 public String postUserEntity(HttpServletRequest request) { 106 String url = "http://" + request.getServerName() + ":" + request.getServerPort(); 107 url += request.getContextPath() + "/api/user/post"; 108 109 // 请求头设置, x-www-form-urlencoded格式的数据 110 HttpHeaders headers = new HttpHeaders(); 111 headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); 112 113 // 提交参数设置 114 MultiValueMap map = new LinkedMultiValueMap<>(); 115 map.add("id", "2"); 116 map.add("username", "user"); 117 map.add("password", "123123"); 118 119 // 组装请求体 120 HttpEntity> httpEntity = new HttpEntity<>(map, headers); 121 122 // 发起请求 123 ResponseEntity responseEntity = restTemplate.postForEntity(url, httpEntity, User.class); 124 String str = "HTTP status code: " + responseEntity.getStatusCode() + "
"; 125 str += "HTTP status value: " + responseEntity.getStatusCodeValue() + "
"; 126 str += "HTTP headers: " + responseEntity.getHeaders() + "
"; 127 str += "HTTP body: " + responseEntity.getBody().toString() + "
"; 128 129 return str; 130 } 131 132 @ResponseBody 133 @RequestMapping("/user/post/location") 134 public String postLocation(HttpServletRequest request) { 135 String url = "http://" + request.getServerName() + ":" + request.getServerPort(); 136 url += request.getContextPath() + "/api/user/post/location"; 137 138 User user = new User(); 139 user.setId(3); 140 user.setUsername("test"); 141 user.setPassword("666666"); 142 143 URI uri = restTemplate.postForLocation(url, user); 144 return "Url: " + uri; 145 } 146 147 @ResponseBody 148 @RequestMapping("/user/put") 149 public String putUser(HttpServletRequest request) { 150 String url = "http://" + request.getServerName() + ":" + request.getServerPort(); 151 url += request.getContextPath() + "/api/user/put"; 152 153 User user = new User(); 154 user.setId(5); 155 user.setUsername("put123"); 156 user.setPassword("123456"); 157 158 restTemplate.put(url, user); 159 return "PUT: " + user.toString(); 160 } 161 162 @ResponseBody 163 @RequestMapping("/user/delete/{id}") 164 public String deleteUser(HttpServletRequest request, @PathVariable Integer id) { 165 String url = "http://" + request.getServerName() + ":" + request.getServerPort(); 166 url += request.getContextPath() + "/api/user/delete/" + id; 167 168 System.out.println("url: " + url); 169 170 restTemplate.delete(url); 171 return "Delete: " + id; 172 } 173 174 }


    5) 运行测试

        浏览器访问 http://localhost:9090/user/get/object,页面显示如下:

            User(id=1, username=admin, password=123456)

        访问 http://localhost:9090/user/get/entity,页面显示如下:

            HTTP status code: 200 OK
            HTTP status value: 200
            HTTP headers: [Connection:"keep-alive", Content-Type:"application/json", Date:"Wed, 22 Jun 2022 10:51:57 GMT", Keep-Alive:"timeout=60", Transfer-Encoding:"chunked"]
            HTTP body: User(id=1, username=admin, password=123456)

        访问 http://localhost:9090/user/get/options,页面显示如下:

            [GET, HEAD, OPTIONS]

        访问 http://localhost:9090/user/get/headers,页面显示如下:

            [Connection:"keep-alive", Content-Length:"47", Content-Type:"application/json", Date:"Wed, 22 Jun 2022 10:53:12 GMT", Keep-Alive:"timeout=60"]

        访问 http://localhost:9090/user/post/object,页面显示如下:

            User(id=2, username=user, password=123123)

        访问 http://localhost:9090/user/post/entity,页面显示如下:

            HTTP status code: 200 OK
            HTTP status value: 200
            HTTP headers: [Connection:"keep-alive", Content-Type:"application/json", Date:"Wed, 22 Jun 2022 10:54:43 GMT", Keep-Alive:"timeout=60", Location:"http://localhost:9090/test", Transfer-Encoding:"chunked"]
            HTTP body: User(id=2, username=user, password=123123)

        访问 http://localhost:9090/user/post/location,页面显示如下:

            Url: http://localhost:9090/test

        访问 http://localhost:9090/user/put,页面显示如下:

            PUT: User(id=5, username=put123, password=123456)

        访问 http://localhost:9090/user/delete/2,页面显示如下:

            Delete: 2



本文资料参考来源:

1. https://docs.spring.io/spring-framework/docs/5.2.22.RELEASE/spring-framework-reference/integration.html#rest-client-access
2. https://www.jianshu.com/p/58949f8334f5
3. https://www.jianshu.com/p/2a59bb937d21
4. https://zhuanlan.zhihu.com/p/258121569