Spring RestTemplate
在服务的调用过程中,使用到了一个组件,叫做 RestTemplate.
RestTemplate 是由 Spring 提供的一个 HTTP 请求工具。可以减少我们平时开发常使用的 HttpClient
API 依赖。
其实开发者也可以不使用 RestTemplate ,使用 Java 自带的 HttpUrlConnection 或者经典的网络访问框架 HttpClient 也可以完成,只是在 Spring 项目中,使用 RestTemplate 显然更方便一些。
RestTemplate 简介
RestTemplate 是从 Spring3.0 开始支持的一个 HTTP 请求工具,它提供常见的REST请求方案的模版,
例如 GET 请求、POST 请求、PUT 请求、DELETE 请求以及一些通用的请求执行方法 exchange 以及 execute。
Synchronous client to perform HTTP requests, exposing a simple, template method API over underlying HTTP client libraries such as the JDK HttpURLConnection, Apache HttpComponents, and others. The RestTemplate offers templates for common scenarios by HTTP method, in addition to the generalized exchange and execute methods that support of less frequent cases.
RestTemplate
采用同步方式执行 HTTP 请求的类,底层使用 JDK 原生HttpURLConnection
API ,或者HttpComponents
等其他 HTTP 客户端请求类库。还有一处强调的就是RestTemplate
提供模板化的方法让开发者能更简单地发送 HTTP 请求。值得注意的是,
RestTemplate
类是在 Spring Framework 3.0 开始引入的,这里我们使用的 Spring 版本为当前最新的 GA 版本 5.1.6。而在 5.0 以上,官方标注了更推荐使用非阻塞的响应式 HTTP 请求处理类org.springframework.web.reactive.client.WebClient
来替代RestTemplate
,尤其是对应异步请求处理的场景上 。
RestTemplate API
方法名 | 描述 |
---|---|
getForObject |
通过 GET 请求获得响应结果 |
getForEntity |
通过 GET 请求获取 ResponseEntity 对象,包容有状态码,响应头和响应数据 |
headForHeaders |
以 HEAD 请求资源返回所有响应头信息 |
postForLocation |
用 POST 请求创建资源,并返回响应数据中响应头的字段 Location 的数据 |
postForObject |
通过 POST 请求创建资源,获得响应结果 |
put |
通过 PUT 方式请求来创建或者更新资源 |
patchForObject |
通过 PATH 方式请求来更新资源,并获得响应结果。(JDK HttpURLConnection 不支持 PATH 方式请求,其他 HTTP 客户端库支持) |
delete |
通过 DELETE 方式删除资源 |
optionsForAllow |
通过 ALLOW 方式请求来获得资源所允许访问的所有 HTTP 方法,可用看某个请求支持哪些请求方式 |
exchange |
更通用版本的请求处理方法,接受一个 RequestEntity 对象,可以设置路径,请求头,请求信息等,最后返回一个 ResponseEntity 实体 |
execute |
最通用的执行 HTTP 请求的方法,上面所有方法都是基于 execute 的封装,全面控制请求信息,并通过回调接口获得响应数据 |
RestTemplate 继承自 InterceptingHttpAccessor 并且实现了 RestOperations 接口,其中 RestOperations 接口定义了基本的 RESTful 操作,这些操作在 RestTemplate 中都得到了实现。
使用方法
注入RestTemplate对象
@Bean注入RestTemplate的实际new对象
@Configuration
public class ConfigBean {
//@Configuration -- spring applicationContext.xml
@Bean
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
然后在Controller类中进行@Autowired
@RestController
public class OrderController {
@Autowired
private RestTemplate restTemplate;
private String url = "http://localhost:8081/user/";
@GetMapping("/order/{id}")
public User getOrder(@PathVariable Long id){
//访问提供者获取数据
User user = restTemplate.getForObject(url + id,User.class);
return user;
}
}
此时就会注入成功得到restTemplate对象
Example
get
尝试用 RestTemplate
访问请求路径为 product/get_product1
一个不带任何参数 的 GET 请求,
@Test
public void testGet_product1() {
String url = "http://localhost:8080/product/get_product1";
//方式一:GET 方式获取 JSON 串数据
String result = restTemplate.getForObject(url, String.class);
System.out.println("get_product1返回结果:" + result);
Assert.hasText(result, "get_product1返回结果为空");
//方式二:GET 方式获取 JSON 数据映射后的 Product 实体对象
//Product product = restTemplate.getForObject(url, Product.class);
System.out.println("get_product1返回结果:" + product);
Assert.notNull(product, "get_product1返回结果为空");
//方式三:GET 方式获取包含 Product 实体对象 的响应实体 ResponseEntity 对象,用 getBody() 获取
ResponseEntity<Product> responseEntity = restTemplate.getForEntity(url, Product.class);
System.out.println("get_product1返回结果:" + responseEntity);
Assert.isTrue(responseEntity.getStatusCode().equals(HttpStatus.OK), "get_product1响应不成功");
}
控制台的输出日志
...
get_product1返回结果:{"id":1,"name":"ProductA","price":6666.0}
...
get_product1返回结果:Product{id='1', name='ProductA', price='6666.0'}
...
get_product1返回结果:<200,Product{id='1', name='ProductA', price='6666.0'},[Content-Type:"application/json;charset=UTF-8", Transfer-Encoding:"chunked", Date:"Thu, 09 May 2019 15:37:25 GMT"]>
...
使用 RestTemplate
API 中 exchange
和 execute
方法发送 GET 请求,可以更加细粒度控制请求的行为,如 Header
信息,数据处理方式等,同样在 testGet_product1
方法里添加代码如下:
@Test
public void testGet_product1() {
String url = "http://localhost:8080/product/get_product1";
//....
//方式一: 构建请求实体 HttpEntity 对象,用于配置 Header 信息和请求参数
MultiValueMap header = new LinkedMultiValueMap();
header.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
HttpEntity<Object> requestEntity = new HttpEntity<>(header);
//方式二: 执行请求获取包含 Product 实体对象 的响应实体 ResponseEntity 对象,用 getBody() 获取
ResponseEntity<Product> exchangeResult = restTemplate.exchange(url, HttpMethod.GET, requestEntity, Product.class);
System.out.println("get_product1返回结果:" + exchangeResult);
Assert.isTrue(exchangeResult.getStatusCode().equals(HttpStatus.OK), "get_product1响应不成功");
//方式三: 根据 RequestCallback 接口实现类设置Header信息,用 ResponseExtractor 接口实现类读取响应数据
String executeResult = restTemplate.execute(url, HttpMethod.GET, request -> {
request.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
}, (clientHttpResponse) -> {
InputStream body = clientHttpResponse.getBody();
byte[] bytes = new byte[body.available()];
body.read(bytes);
return new String(bytes);
});
// 备注:这里使用了 Java8 特性:Lambda 表达式语法,若未接触 Lambda 表达式后可以使用匿名内部类代替实现
System.out.println("get_product1返回结果:" + executeResult);
Assert.hasText(executeResult, "get_product1返回结果为空");
}
控制台的输出日志
...
get_product1返回结果:<200,Product{id='1', name='ProductA', price='6666.0'},[Content-Type:"application/json;charset=UTF-8", Transfer-Encoding:"chunked", Date:"Thu, 09 May 2019 16:00:22 GMT"]>
...
get_product1返回结果:{"id":1,"name":"ProductA","price":6666.0}
...
执行带有参数的 GET 请求
@Test
public void testGet_product2() {
String url = "http://localhost:8080/product/get_product2/id={id}";
//方式一:将参数的值存在可变长度参数里,按照顺序进行参数匹配
ResponseEntity<Product> responseEntity = restTemplate.getForEntity(url, Product.class, 101);
System.out.println(responseEntity);
Assert.isTrue(responseEntity.getStatusCode().equals(HttpStatus.OK), "get_product2 请求不成功");
Assert.notNull(responseEntity.getBody().getId(), "get_product2 传递参数不成功");
//方式二:将请求参数以键值对形式存储到 Map 集合中,用于请求时URL上的拼接
Map<String, Object> uriVariables = new HashMap<>();
uriVariables.put("id", 101);
Product result = restTemplate.getForObject(url, Product.class, uriVariables);
System.out.println(result);
Assert.notNull(result.getId(), "get_product2 传递参数不成功");
}
...
<200,Product{id='101', name='ProductC', price='6666.0'},[Content-Type:"application/json;charset=UTF-8", Transfer-Encoding:"chunked", Date:"Fri, 10 May 2019 14:53:41 GMT"]>
...
Product{id='101', name='ProductC', price='6666.0'}
...
POST 请求
由于 POST 请求数据的内容类型 Content-Type
不同,发送 POST 请求情况相对就多了,我们这里以常用的 application/x-www-form-urlencoded
和 application/json
这两种内容类型为例子。
Content-Type
为 application/x-www-form-urlencoded
@Test
public void testPost_product1() {
String url = "http://localhost:8080/product/post_product1";
Product product = new Product(201, "Macbook", BigDecimal.valueOf(10000));
// 设置请求的 Content-Type 为 application/x-www-form-urlencoded
MultiValueMap<String, String> header = new LinkedMultiValueMap();
header.add(HttpHeaders.CONTENT_TYPE, (MediaType.APPLICATION_FORM_URLENCODED_VALUE));
//方式二: 将请求参数值以 K=V 方式用 & 拼接,发送请求使用
String productStr = "id=" + product.getId() + "&name=" + product.getName() + "&price=" + product.getPrice();
HttpEntity<String> request = new HttpEntity<>(productStr, header);
ResponseEntity<String> exchangeResult = restTemplate.exchange(url, HttpMethod.POST, request, String.class);
System.out.println("post_product1: " + exchangeResult);
Assert.isTrue(exchangeResult.getStatusCode().equals(HttpStatus.OK), "post_product1 请求不成功");
//方式一: 将请求参数以键值对形式存储在 MultiValueMap 集合,发送请求时使用
MultiValueMap<String, Object> map = new LinkedMultiValueMap();
map.add("id", (product.getId()));
map.add("name", (product.getName()));
map.add("price", (product.getPrice()));
HttpEntity<MultiValueMap> request2 = new HttpEntity<>(map, header);
ResponseEntity<String> exchangeResult2 = restTemplate.exchange(url, HttpMethod.POST, request2, String.class);
System.out.println("post_product1: " + exchangeResult2);
Assert.isTrue(exchangeResult.getStatusCode().equals(HttpStatus.OK), "post_product1 请求不成功");
}
对应的输出日志如下:
...
post_product1: <200,Product{id='201', name='Macbook', price='10000'},[Content-Type:"text/plain;charset=UTF-8", Content-Length:"48", Date:"Fri, 10 May 2019 16:07:43 GMT"]>
...
post_product1: <200,Product{id='201', name='Macbook', price='10000'},[Content-Type:"text/plain;charset=UTF-8", Content-Length:"48", Date:"Fri, 10 May 2019 16:07:43 GMT"]>
发送 Content-Type
为 application/json
的 POST 请求:
@Test
public void testPost_product2() {
String url = "http://localhost:8080/product/post_product2";
// 设置请求的 Content-Type 为 application/json
MultiValueMap<String, String> header = new LinkedMultiValueMap();
header.put(HttpHeaders.CONTENT_TYPE, Arrays.asList(MediaType.APPLICATION_JSON_VALUE));
// 设置 Accept 向服务器表明客户端可处理的内容类型
header.put(HttpHeaders.ACCEPT, Arrays.asList(MediaType.APPLICATION_JSON_VALUE));
// 直接将实体 Product 作为请求参数传入,底层利用 Jackson 框架序列化成 JSON 串发送请求
HttpEntity<Product> request = new HttpEntity<>(new Product(2, "Macbook", BigDecimal.valueOf(10000)), header);
ResponseEntity<String> exchangeResult = restTemplate.exchange(url, HttpMethod.POST, request, String.class);
System.out.println("post_product2: " + exchangeResult);
Assert.isTrue(exchangeResult.getStatusCode().equals(HttpStatus.OK), "post_product2 请求不成功");
}
对应的输出日志如下:
...
post_product1: <200,Product{id='201', name='Macbook', price='10000'},[Content-Type:"text/plain;charset=UTF-8", Content-Length:"48", Date:"Fri, 10 May 2019 16:07:43 GMT"]>
...
post_product1: <200,Product{id='201', name='Macbook', price='10000'},[Content-Type:"text/plain;charset=UTF-8", Content-Length:"48", Date:"Fri, 10 May 2019 16:07:43 GMT"]>
复制代码
Content-Type
为 application/json
@Test
public void testPost_product2() {
String url = "http://localhost:8080/product/post_product2";
// 设置请求的 Content-Type 为 application/json
MultiValueMap<String, String> header = new LinkedMultiValueMap();
header.put(HttpHeaders.CONTENT_TYPE, Arrays.asList(MediaType.APPLICATION_JSON_VALUE));
// 设置 Accept 向服务器表明客户端可处理的内容类型
header.put(HttpHeaders.ACCEPT, Arrays.asList(MediaType.APPLICATION_JSON_VALUE));
// 直接将实体 Product 作为请求参数传入,底层利用 Jackson 框架序列化成 JSON 串发送请求
HttpEntity<Product> request = new HttpEntity<>(new Product(2, "Macbook", BigDecimal.valueOf(10000)), header);
ResponseEntity<String> exchangeResult = restTemplate.exchange(url, HttpMethod.POST, request, String.class);
System.out.println("post_product2: " + exchangeResult);
Assert.isTrue(exchangeResult.getStatusCode().equals(HttpStatus.OK), "post_product2 请求不成功");
}
验证的输出日志如下:
···
post_product2: <200,Product{id='2', name='Macbook', price='10000'},[Content-Type:"application/json;charset=UTF-8", Content-Length:"46", Date:"Fri, 10 May 2019 16:09:11 GMT"]>
···
复制代码
DELETE 请求 和 PUT 请求
DELETE 请求和 PUT 请求属于 RESTful 请求方式的两种,但通常不会被使用到,这里也只是简单演示下,具体代码如下:
// DELETE 方法请求
@Test
public void testDelete() {
String url = "http://localhost:8080/product/delete/{id}";
restTemplate.delete(url, 101);
}
// PUT 方法请求
@Test
public void testPut() {
String url = "http://localhost:8080/product/update";
Map<String, ?> variables = new HashMap<>();
MultiValueMap<String, String> header = new LinkedMultiValueMap();
header.put(HttpHeaders.CONTENT_TYPE, Arrays.asList(MediaType.APPLICATION_FORM_URLENCODED_VALUE));
Product product = new Product(101, "iWatch", BigDecimal.valueOf(2333));
String productStr = "id=" + product.getId() + "&name=" + product.getName() + "&price=" + product.getPrice();
HttpEntity<String> request = new HttpEntity<>(productStr, header);
restTemplate.put(url, request);
}
上传文件
@Test
public void testUploadFile() {
String url = "http://localhost:8080/product/upload";
//设置body
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
FileSystemResource file = new FileSystemResource(new File("/Users/One/Desktop/b.txt"));
body.add("file", file);
//设置请求方式
MultiValueMap<String, String> header = new LinkedMultiValueMap();
header.put(HttpHeaders.CONTENT_TYPE, Arrays.asList(MediaType.MULTIPART_FORM_DATA_VALUE));
//发送请求
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, header);
ResponseEntity<String> responseEntity = restTemplate.postForEntity(url, requestEntity, String.class);
System.out.println("upload: " + responseEntity);
Assert.isTrue(responseEntity.getStatusCode().equals(HttpStatus.OK), "upload 请求不成功");
}
如果需要上传文件类型数据,就只能使用 POST 请求,并且内容类型为 multipart/form-data
,需要手动给 Header
指定这个 Content-Type
。而需要上传的文件可以用 FileSystemResource
对象封装,表示了一个文件资源,同时服务端需要用 MultipartRequest
对象来获取文件数据。结合已运行的 Web 服务,运行上述测试方法即可得到下面日志输出:
...
upload: <200,upload success filename: b.txt,[Content-Type:"text/plain;charset=UTF-8", Content-Length:"30", Date:"Fri, 10 May 2019 17:00:45 GMT"]>
...
进阶 RestTemplate
The default constructor uses java.net.HttpURLConnection to perform requests. You can switch to a different HTTP library with an implementation of ClientHttpRequestFactory. There is built-in support for the following:
- Apache HttpComponents
- Netty
- OkHttp
RestTemplate
默认使用 JDK 原生的java.net.HttpURLConnection
执行请求。而除此之外,Spring 还封装了 Apache HttpComponents, Netty, OkHttp 三种请求库,第一个就是我们平常用的HttpClient
API 相关的库,而 Netty 则是一个性能高的NIO 请求处理网络库,OkHttp 为功能丰富且高效的网络框架,多用于 Android 程序。上文采用默认的构造器方法创建的
RestTemplate
实例,即采用了 JDK 原生的网络 API。
想要切换构造器,只需要在构造方法中传入特定 ClientHttpRequestFactory
实现类即可
@Configuration
public class ConfigBean {
//@Configuration -- spring applicationContext.xml
@Bean
public RestTemplate getRestTemplate() {
RestTemplate template = new RestTemplate(new HttpComponentsClientHttpRequestFactory());
return template;
}
}
请求超时设置
通常我们会对 HTTP 请求类进行执行行为的定制,例如调用超时时间设置,连接时长的限制等,而采用默认的 HttpURLConnection
默认的配置时, 从 SimpleClientHttpRequestFactory
源码类可以看到是没有超时限制,也就意味着无限等待请求响应
// RestTemplate 默认超时设置
...
private int connectTimeout = -1;
private int readTimeout = -1;
...
那么我们该如何调整超时时间,可以参考如下代码:
@Configuration
public class ConfigBean {
//@Configuration -- spring applicationContext.xml
@Bean
public RestTemplate getRestTemplate() {
RestTemplate template = new RestTemplate(getClientHttpRequestFactory());
return template;
}
`
private SimpleClientHttpRequestFactory getClientHttpRequestFactory() {
SimpleClientHttpRequestFactory clientHttpRequestFactory
= new SimpleClientHttpRequestFactory();
// 连接超时设置 10s
clientHttpRequestFactory.setConnectTimeout(10_000);
// 读取超时设置 10s
clientHttpRequestFactory.setReadTimeout(10_000);
return clientHttpRequestFactory;
}
}