RestTemplate
是执行HTTP请求的同步阻塞式的客户端,它在HTTP
客户端库(例如JDK HttpURLConnection,Apache HttpComponents,okHttp等)基础封装了更加简单易用的模板方法API。也就是说RestTemplate是一个封装,底层的实现还是java应用开发中常用的一些HTTP客户端。但是相对于直接使用底层的HTTP客户端库,它的操作更加方便、快捷,能很大程度上提升我们的开发效率。
RestTemplate
作为spring-web项目的一部分,在Spring 3.0版本开始被引入。RestTemplate
类通过为HTTP方法(例如GET,POST,PUT,DELETE等)提供重载的方法,提供了一种非常方便的方法访问基于HTTP的Web服务。如果你的Web服务API基于标准的RESTful
风格设计,使用效果将更加的完美。
根据Spring
官方文档及源码中的介绍,RestTemplate
在将来的版本中它可能会被弃用,因为他们已在Spring 5
中引入了WebClient
作为非阻塞式Reactive HTTP
客户端。但是RestTemplate
目前在Spring
社区内还是很多项目的“重度依赖”,比如说Spring Cloud
。另外,RestTemplate
说白了是一个客户端API封装,和服务端相比,非阻塞Reactive 编程的需求并没有那么高。
RestTemplate
是Spring
的一个rest
客户端,在Spring-web
这个包下。这个包虽然叫做Spring-web
,但是它的RestTemplate
可以脱离Spring
环境使用。
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.2.6.RELEASE</version>
</dependency>
如果是在Spring环境下使用RestTemplate,将maven坐标从spring-web换成spring-boot-starter-web
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
将RestTemplate
配置初始化为一个Bean
。这种初始化方法,是使用了JDK
自带的HttpURLConnection
作为底层HTTP
客户端实现。
@Configuration
public class ContextConfig {
//默认使用JDK 自带的HttpURLConnection作为底层实现
@Bean
public RestTemplate restTemplate(){
RestTemplate restTemplate = new RestTemplate();
return restTemplate;
}
}
RestTemplate
只是对其他的HTTP客户端的封装,其本身并没有实现HTTP相关的基础功能。其底层实现是可以配置切换的,我们本小节就带着大家来看一下RestTemplate
底层实现,及如何实现底层基础HTTP
库的切换。
RestTemplate
有一个非常重要的类叫做HttpAccessor
,可以理解为用于HTTP
接触访问的基础类。下图为源码:
public abstract class HttpAccessor {
/** Logger available to subclasses. */
protected final Log logger = HttpLogging.forLogName(getClass());
private ClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
private final List<ClientHttpRequestInitializer> clientHttpRequestInitializers = new ArrayList<>();
/**
* Set the request factory that this accessor uses for obtaining client request handles.
* <p>The default is a {@link SimpleClientHttpRequestFactory} based on the JDK's own
* HTTP libraries ({@link java.net.HttpURLConnection}).
* <p><b>Note that the standard JDK HTTP library does not support the HTTP PATCH method.
* Configure the Apache HttpComponents or OkHttp request factory to enable PATCH.</b>
* @see #createRequest(URI, HttpMethod)
* @see SimpleClientHttpRequestFactory
* @see org.springframework.http.client.HttpComponentsAsyncClientHttpRequestFactory
* @see org.springframework.http.client.OkHttp3ClientHttpRequestFactory
*/
public void setRequestFactory(ClientHttpRequestFactory requestFactory) {
Assert.notNull(requestFactory, "ClientHttpRequestFactory must not be null");
this.requestFactory = requestFactory;
}
......
}
从源码中我们可以分析出以下几点信息:
RestTemplate
支持至少三种HTTP客户端库。SimpleClientHttpRequestFactory
。对应的HTTP
库是Java JDK
自带的HttpURLConnection
。HttpComponentsAsyncClientHttpRequestFactory
。对应的HTTP
库是Apache HttpComponents
。OkHttp3ClientHttpRequestFactory
。对应的HTTP
库是OkHttp
Java JDK
自带的HttpURLConnection
是默认的底层HTTP
实现客户端SimpleClientHttpRequestFactory
,即Java JDK
自带的HttpURLConnection
不支持HTTP
协议的Patch
方法,如果希望使用Patch
方法,需要将底层HTTP客户端实现切换为Apache HttpComponents
或 OkHttp
setRequestFactory
方法,来切换RestTemplate
的底层HTTP
客户端实现类库。各种HTTP客户端性能以及易用程度评测来看,OkHttp
优于 Apache HttpComponents
、Apache HttpComponents
优于HttpURLConnection
。所以我个人更建议大家将底层HTTP
实现切换为okHTTP
。
首先通过maven
坐标将okHTTP
的包引入到项目中来。
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.7.2</version>
</dependency>
如果是Spring
环境下通过如下方式使用OkHttp3ClientHttpRequestFactory
初始化RestTemplate bean
对象。
@Configuration
public class ContextConfig {
@Bean("OKHttp3")
public RestTemplate OKHttp3RestTemplate(){
RestTemplate restTemplate = new RestTemplate(new OkHttp3ClientHttpRequestFactory());
return restTemplate;
}
}
如果是非Spring
环境,直接new RestTemplate(new OkHttp3ClientHttpRequestFactory()
之后使用就可以了。
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.12</version>
</dependency>
使用HttpComponentsClientHttpRequestFactory
初始化RestTemplate bean
对象
@Bean("httpClient")
public RestTemplate httpClientRestTemplate(){
RestTemplate restTemplate = new RestTemplate(new HttpComponentsClientHttpRequestFactory());
return restTemplate;
}
RestTemplate
可以发送HTTP GET
请求,经常使用到的方法有两个:
getForObject()
getForEntity()
二者的主要区别在于,getForObject()
返回值是HTTP
协议的响应体。getForEntity()
返回的是ResponseEntity
,ResponseEntity
是对HTTP
响应的封装,除了包含响应体,还包含HTTP
状态码、contentType
、contentLength
、Header
等信息。
在Spring
环境下写一个单元测试用例,以String
类型接收响应结果信息
/**
* 以String的方式接受请求结果数据
*/
@Test
public void simpleTest()
{
RestTemplate restTemplate = new RestTemplate();
String url = "http://jsonplaceholder.typicode.com/posts/1";
String str = restTemplate.getForObject(url, String.class);
System.out.println(str);
}
getForObject第二个参数为返回值的类型,String.class以字符串的形式接受getForObject响应结果。
{
"userId": 1,
"id": 1,
"title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
"body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
}
/**
* 以POJO对象的方式接受结果数据
*/
@Test
public void simpleTest2()
{
RestTemplate restTemplate = new RestTemplate();
String url = "http://jsonplaceholder.typicode.com/posts/1";
PostDTO postDTO = restTemplate.getForObject(url, PostDTO.class);
System.out.println(postDTO.toString());
}
POJO的定义如下,根据JSON String的数据格式定义。
@Getter
@Setter
public class PostDTO {
private int userId;
private int id;
private String title;
private String body;
@Override
public String toString() {
return "PostDTO{" +
"userId=" + userId +
", id=" + id +
", title='" + title + '\'' +
", body='" + body + '\'' +
'}';
}
}
输出打印结果如下:
PostDTO{userId=1, id=1, title='sunt aut facere repellat provident occaecati excepturi optio reprehenderit', body='quia et suscipit
suscipit recusandae consequuntur expedita et cum
reprehenderit molestiae ut ut quas totam
nostrum rerum est autem sunt rem eveniet architecto'}
访问http://jsonplaceholder.typicode.com/posts可以获得JSON数组方式的请求结果
/**
* 以数组的方式接收请求结果
*/
@Test
public void testArrays() {
RestTemplate restTemplate = new RestTemplate();
String url = "http://jsonplaceholder.typicode.com/posts";
PostDTO[] postDTOs = restTemplate.getForObject(url, PostDTO[].class);
System.out.println("数组长度:" + postDTOs.length);
}
请求的结果被以数组的方式正确接收,输出如下:
数组长度:100
下的几个请求都是在访问"http://jsonplaceholder.typicode.com/posts/1",只是使用了占位符语法,这样在业务使用上更加灵活。
String url = "http://jsonplaceholder.typicode.com/{1}/{2}";
PostDTO postDTO = restTemplate.getForObject(url, PostDTO.class, "posts", 1);
String url = "http://jsonplaceholder.typicode.com/{type}/{id}";
String type = "posts";
int id = 1;
PostDTO postDTO = restTemplate.getForObject(url, PostDTO.class, type, id);
String url = "http://jsonplaceholder.typicode.com/{type}/{id}";
Map<String,Object> map = new HashMap<>();
map.put("type", "posts");
map.put("id", 1);
PostDTO postDTO = restTemplate.getForObject(url, PostDTO.class, map);
上面的所有的getForObject请求传参方法,getForEntity都可以使用,使用方法上也几乎是一致的,只是在返回结果接收的时候略有差别。使用ResponseEntity<T> responseEntity
来接收响应结果。用responseEntity.getBody()获取响应体。响应体内容同getForObject方法返回结果一致。剩下的这些响应信息就是getForEntity比getForObject多出来的内容。
HttpStatus statusCode = responseEntity.getStatusCode();
获取整体的响应状态信息int statusCodeValue = responseEntity.getStatusCodeValue();
获取响应码值HttpHeaders headers = responseEntity.getHeaders();
获取响应头@Test
public void testEntityPoJo() {
RestTemplate restTemplate = new RestTemplate();
String url = "http://jsonplaceholder.typicode.com/posts/5";
ResponseEntity<PostDTO> responseEntity = restTemplate.getForEntity(url, PostDTO.class);
PostDTO postDTO = responseEntity.getBody(); // 获取响应体
System.out.println("HTTP 响应body:" + postDTO.toString());
//以下是getForEntity比getForObject多出来的内容
HttpStatus statusCode = responseEntity.getStatusCode(); // 获取响应码
int statusCodeValue = responseEntity.getStatusCodeValue(); // 获取响应码值
HttpHeaders headers = responseEntity.getHeaders(); // 获取响应头
System.out.println("HTTP 响应状态:" + statusCode);
System.out.println("HTTP 响应状态码:" + statusCodeValue);
System.out.println("HTTP Headers信息:" + headers);
}
输出打印结果:
HTTP 响应body:PostDTO{userId=1, id=5, title='nesciunt quas odio', body='repudiandae veniam quaerat sunt sed
alias aut fugiat sit autem sed est
voluptatem omnis possimus esse voluptatibus quis
est aut tenetur dolor neque'}
HTTP 响应状态:200 OK
HTTP 响应状态码:200
HTTP Headers信息:[Date:"Wed, 18 Aug 2021 10:15:35 GMT", Content-Type:"application/json; charset=utf-8", Content-Length:"225", Connection:"keep-alive", x-powered-by:"Express", x-ratelimit-limit:"1000", x-ratelimit-remaining:"995", x-ratelimit-reset:"1628549805", vary:"Origin, Accept-Encoding", access-control-allow-credentials:"true", cache-control:"max-age=43200", pragma:"no-cache", expires:"-1", x-content-type-options:"nosniff", etag:"W/"e1-IivojO0CtPZmcMK0iydTbsfG7Wc"", via:"1.1 vegur", CF-Cache-Status:"HIT", Age:"16347", Accept-Ranges:"bytes", Report-To:"{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=mNiimkAj9nxgJY2mPgt%2BZiLtyBznKyGTACp2FaHMN69AQpEXNkB690z0Isfj3PQ5v39XckkOy3YNoK7tfGdRpRciZpEeOeA6%2BGXdZIMtV9asOAgcVNx%2F81%2Fy3nmbax2ygY9Cq7dCiq00HoDkOuIz"}],"group":"cf-nel","max_age":604800}", NEL:"{"success_fraction":0,"report_to":"cf-nel","max_age":604800}", Server:"cloudflare", CF-RAY:"680a61c1de2f368c-LAX", alt-svc:"h3-27=":443"; ma=86400, h3-28=":443"; ma=86400, h3-29=":443"; ma=86400, h3=":443"; ma=86400"]
RestTemplate
的POST
请求也包含两个主要方法:
postForObject()
postForEntity()
二者的主要区别在于,postForObject()
返回值是HTTP
协议的响应体。postForEntity()
返回的是ResponseEntity
,ResponseEntity
是对HTTP
响应的封装,除了包含响应体,还包含HTTP
状态码、contentType、contentLength、Header
等信息。
/**
* postForObject发送JSON格式请求
*/
@Test
public void simpleTest() {
RestTemplate restTemplate = new RestTemplate();
// 请求地址
String url = "http://jsonplaceholder.typicode.com/posts";
// 要发送的数据对象
PostDTO postDTO = new PostDTO();
postDTO.setUserId(110);
postDTO.setTitle("zimug 发布文章");
postDTO.setBody("zimug 发布文章 测试内容");
// 发送post请求,并输出结果
PostDTO result = restTemplate.postForObject(url, postDTO, PostDTO.class);
System.out.println(result);
}
PostDTO{userId=110, id=101, title='zimug 发布文章', body='zimug 发布文章 测试内容'}
使用postForObject模拟表单数据提交的例子,即:提交x-www-form-urlencoded格式的数据
/**
* postForObject模拟表单数据提交
*/
@Test
public void simpleTest2() {
RestTemplate restTemplate = new RestTemplate();
// 请求地址
String url = "http://jsonplaceholder.typicode.com/posts";
// 请求头设置,x-www-form-urlencoded格式的数据
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
//提交参数设置
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
map.add("title", "zimug 发布文章第二篇");
map.add("body", "zimug 发布文章第二篇 测试内容");
// 组装请求体
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<MultiValueMap<String, String>>(map, headers);
// 发送post请求,并打印结果,以String类型接收响应结果JSON字符串
String result = restTemplate.postForObject(url, request, String.class);
System.out.println(result);
}
请求数据打印结果如下:
{
"title": "zimug 发布文章第二篇",
"body": "zimug 发布文章第二篇 测试内容",
"id": 101
}
如果url地址上面需要传递一些动态参数,可以使用占位符的方式:
String url = "http://jsonplaceholder.typicode.com/{1}/{2}";
String url = "http://jsonplaceholder.typicode.com/{type}/{id}";
具体的用法和使用GET方法请求是一致的。
上面的所有的postForObject请求传参方法,postForEntity都可以使用,使用方法上也几乎是一致的,只是在返回结果接收的时候略有差别。使用ResponseEntity<T> responseEntity
来接收响应结果。用responseEntity.getBody()获取响应体。响应体内容同postForObject方法返回结果一致。剩下的这些响应信息就是postForEntity比postForObject多出来的内容。
HttpStatus statusCode = responseEntity.getStatusCode();
获取整体的响应状态信息int statusCodeValue = responseEntity.getStatusCodeValue();
获取响应码值HttpHeaders headers = responseEntity.getHeaders();
获取响应头@Test
public void simpleTest() {
RestTemplate restTemplate = new RestTemplate();
// 请求地址
String url = "http://jsonplaceholder.typicode.com/posts";
// 要发送的数据对象
PostDTO postDTO = new PostDTO();
postDTO.setUserId(110);
postDTO.setTitle("zimug 发布文章");
postDTO.setBody("zimug 发布文章 测试内容");
// 发送post请求,并输出结果
ResponseEntity<String> responseEntity = restTemplate.postForEntity(url, postDTO, String.class);
String body = responseEntity.getBody(); // 获取响应体
System.out.println("HTTP 响应body:" + postDTO.toString());
//以下是postForEntity比postForObject多出来的内容
HttpStatus statusCode = responseEntity.getStatusCode(); // 获取响应码
int statusCodeValue = responseEntity.getStatusCodeValue(); // 获取响应码值
HttpHeaders headers = responseEntity.getHeaders(); // 获取响应头
System.out.println("HTTP 响应状态:" + statusCode);
System.out.println("HTTP 响应状态码:" + statusCodeValue);
System.out.println("HTTP Headers信息:" + headers);
}
输出打印结果
HTTP 响应body:PostDTO{userId=110, id=0, title='zimug 发布文章', body='zimug 发布文章 测试内容'}
HTTP 响应状态:201 CREATED
HTTP 响应状态码:201
HTTP Headers信息:[Date:"Wed, 18 Aug 2021 10:33:01 GMT", Content-Type:"application/json; charset=utf-8", Content-Length:"110", Connection:"keep-alive", x-powered-by:"Express", x-ratelimit-limit:"1000", x-ratelimit-remaining:"999", x-ratelimit-reset:"1629282825", vary:"Origin, X-HTTP-Method-Override, Accept-Encoding", access-control-allow-credentials:"true", cache-control:"no-cache", pragma:"no-cache", expires:"-1", access-control-expose-headers:"Location", location:"http://jsonplaceholder.typicode.com/posts/101", x-content-type-options:"nosniff", etag:"W/"6e-CLaV3nE0EYRnzjaVSaMnfjSuvGo"", via:"1.1 vegur", CF-Cache-Status:"DYNAMIC", Report-To:"{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=uhnKwOL%2FPmY6EfrTsM2AjEcQqpe%2FrQ%2FR1PghzZ9s%2FvPrjjechh1cp5jshrPlQV%2BTwr%2FHnEQnTPRgtr5B8BSPWEAGCmsP4YjAWj9PMD8YnuWL6Ol7pBN6E%2B%2Bfj3k5polSy5IyR7kQ2x12X013Uym6"}],"group":"cf-nel","max_age":604800}", NEL:"{"success_fraction":0,"report_to":"cf-nel","max_age":604800}", Server:"cloudflare", CF-RAY:"680a7b44a8ca530d-LAX", alt-svc:"h3-27=":443"; ma=86400, h3-28=":443"; ma=86400, h3-29=":443"; ma=86400, h3=":443"; ma=86400"]
postForLocation
的传参的类型、个数、用法基本都和postForObject()
或postForEntity()
一致。和前两者的唯一区别在于返回值是一个URI
。该URI
返回值体现的是:用于提交完成数据之后的页面跳转,或数据提交完成之后的下一步数据操作URI
。
@Test
public void testURI() {
RestTemplate restTemplate = new RestTemplate();
// 请求地址
String url = "http://jsonplaceholder.typicode.com/posts";
PostDTO postDTO = new PostDTO();
postDTO.setUserId(110);
postDTO.setTitle("zimug 发布文章");
postDTO.setBody("zimug 发布文章 测试内容");
// 发送post请求,并输出结果
URI uri = restTemplate.postForLocation(url,postDTO);
System.out.println(uri);
}
输出结果如下,含义是:提交了post之后,该post的id是101,可以通过如下的连接去获取数据。
http://jsonplaceholder.typicode.com/posts/101
熟悉RESTful风格的朋友,应该了解RESTful风格API使用HTTP method表达对资源的操作。
常用HTTP方法 | RESTful风格语义(操作) |
---|---|
GET | 查询、获取数据 |
POST | 新增、提交数据 |
DELETE | 删除数据 |
PUT | 更新、修改数据 |
HEAD | 获取HTTP请求头数据 |
OPTIONS | 判断URL提供的当前API支持哪些HTTP method方法 |
/**
* 删除一个已经存在的资源,使用RestTemplate的delete(uri)方法。
* 该方法会向URL代表的资源发送一个HTTP DELETE方法请求。
*/
@Test
public void testDelete() {
String url = "http://jsonplaceholder.typicode.com/posts/1";
restTemplate.delete(url);
}
11:35:05.344 [main] DEBUG org.springframework.web.client.RestTemplate - HTTP DELETE http://jsonplaceholder.typicode.com/posts/1
11:35:06.194 [main] DEBUG org.springframework.web.client.RestTemplate - Response 200 OK
/**
* 修改一个已经存在的资源,使用RestTemplate的put()方法。
* 该方法会向URL代表的资源发送一个HTTP PUT方法请求。
*/
@Test
public void testPut() {
// 请求地址
String url = "http://jsonplaceholder.typicode.com/posts/1";
// 要发送的数据对象(修改数据)
PostDTO postDTO = new PostDTO();
postDTO.setUserId(110);
postDTO.setTitle("zimug 发布文章");
postDTO.setBody("zimug 发布文章 测试内容");
// 发送PUT请求
restTemplate.put(url, postDTO);
}
11:37:21.468 [main] DEBUG org.springframework.web.client.RestTemplate - HTTP PUT http://jsonplaceholder.typicode.com/posts/1
11:37:21.512 [main] DEBUG org.springframework.web.client.RestTemplate - Writing [PostDTO{userId=110, id=0, title='zimug 发布文章', body='zimug 发布文章 测试内容'}] with org.springframework.http.converter.json.MappingJackson2HttpMessageConverter
11:37:22.580 [main] DEBUG org.springframework.web.client.RestTemplate - Response 200 OK
exchange方法是一个通用的方法,它可以发送GET、POST、DELETE、PUT等等HTTP方法请求。
//使用getForEntity发送GET请求
ResponseEntity<PostDTO> responseEntity
= restTemplate.getForEntity(url, PostDTO.class);
//使用exchange发送GET请求
ResponseEntity<PostDTO> responseEntity = restTemplate.exchange(url, HttpMethod.GET,
null, PostDTO.class);
// 使用postForEntity发送POST请求
ResponseEntity<String> responseEntity
= restTemplate.postForEntity(url, postDTO, String.class);
// 使用exchange发送POST请求
ResponseEntity<String> responseEntity
= restTemplate.exchange(url, HttpMethod.POST,null, String.class);
// 使用delete发送DELETE请求,返回值为void
restTemplate.delete(url);
// 使用exchange发送DELETE请求
ResponseEntity<String> result = restTemplate.exchange(url, HttpMethod.DELETE,null,String.class);
/**
* 获取某个资源的URI的请求头信息,并且只专注于获取HTTP请求头信息。
*/
@Test
public void testHEAD() {
String url = "http://jsonplaceholder.typicode.com/posts/1";
HttpHeaders httpHeaders = restTemplate.headForHeaders(url);
//断言该资源接口数据为JSON类型
Assert.assertTrue(httpHeaders.getContentType().includes(MediaType.APPLICATION_JSON));
System.out.println(httpHeaders);
}
11:42:34.621 [main] DEBUG org.springframework.web.client.RestTemplate - HTTP HEAD http://jsonplaceholder.typicode.com/posts/1
11:42:35.209 [main] DEBUG org.springframework.web.client.RestTemplate - Response 200 OK
[Date:"Thu, 19 Aug 2021 03:42:35 GMT", Content-Type:"application/json; charset=utf-8", Content-Length:"292", Connection:"keep-alive", X-Powered-By:"Express", X-Ratelimit-Limit:"1000", X-Ratelimit-Remaining:"996", X-Ratelimit-Reset:"1628129969", Vary:"Origin, Accept-Encoding", Access-Control-Allow-Credentials:"true", Cache-Control:"max-age=43200", Pragma:"no-cache", Expires:"-1", X-Content-Type-Options:"nosniff", Etag:"W/"124-yiKdLzqO5gfBrJFrcdJ8Yq0LGnU"", Via:"1.1 vegur", CF-Cache-Status:"HIT", Age:"5897", Accept-Ranges:"bytes", Report-To:"{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=hWkTRUmOhESgmonXAQsTbnj9P5kkUsNARt0JLem2yaEhGmIeKxAEv9trcpOYHUgqmziyFJgYKvFXjf8rruxTEovs02Kc5nzrDbAgFRtNEUAmSbZM9Y4x9CL8LFN57F%2BJXr5mVJz5drWUAkQVBio%2B"}],"group":"cf-nel","max_age":604800}", NEL:"{"success_fraction":0,"report_to":"cf-nel","max_age":604800}", Server:"cloudflare", CF-RAY:"68105f6eaca40538-LAX", alt-svc:"h3-27=":443"; ma=86400, h3-28=":443"; ma=86400, h3-29=":443"; ma=86400, h3=":443"; ma=86400"]
/**
* optionsForAllow测试该URL资源是否支持GET、POST、PUT、DELETE,即增删改查。
*/
@Test
public void testOPTIONS() {
String url = "http://jsonplaceholder.typicode.com/posts/1";
Set<HttpMethod> optionsForAllow = restTemplate.optionsForAllow(url);
HttpMethod[] supportedMethods = {HttpMethod.GET, HttpMethod.POST, HttpMethod.PUT, HttpMethod.DELETE};
//测试该url资源是否支持GET、POST、PUT、DELETE,即增删改查
Assert.assertTrue(optionsForAllow.containsAll(Arrays.asList(supportedMethods)));
}
RestTemplate是HTTP客户端库,所以为了使用RestTemplate进行文件上传和下载,需要我们先编写服务端的支持文件上传和下载的程序。
RestTemplate restTemplate = new RestTemplate();
@Test
public void testUpload() {
// 文件上传服务上传接口
String url = "http://localhost:2000/spring-master/fileUpload/upload";
// 待上传的文件(存在客户端本地磁盘)
String filePath = "/Users/booker/Documents/material/图片/4.jpeg";
// 封装请求参数
FileSystemResource resource = new FileSystemResource(new File(filePath));
MultiValueMap<String, Object> param = new LinkedMultiValueMap<>();
param.add("uploadFile", resource); //服务端MultipartFile uploadFile
//param.add("param1", "test"); //服务端如果接受额外参数,可以传递
// 发送请求并输出结果
System.out.println("--- 开始上传文件 ---");
String result = restTemplate.postForObject(url, param, String.class);
System.out.println("--- 访问地址:" + result);
}
文件上传之后,可以通过上面的访问地址,在浏览器访问。或者通过RestTemplate客户端进行下载。
执行下列代码之后,被下载文件url,会被正确的保存到本地磁盘目录targetPath。
@Test
public void testDownLoad() throws IOException {
// 待下载的文件地址
String url = "http://localhost:2000/spring-master/0968094e-7332-4705-9893-1884d42a5028.jpeg";
ResponseEntity<byte[]> rsp = restTemplate.getForEntity(url, byte[].class);
System.out.println("文件下载请求结果状态码:" + rsp.getStatusCode());
// 将下载下来的文件内容保存到本地
String targetPath = "/Users/booker/Documents/projects/git/master/target/1.jpeg";
Files.write(Paths.get(targetPath), Objects.requireNonNull(rsp.getBody(), "未获取到下载文件"));
}
14:56:13.076 [main] DEBUG org.springframework.web.client.RestTemplate - HTTP GET http://localhost:2000/spring-master/0968094e-7332-4705-9893-1884d42a5028.jpeg
14:56:13.098 [main] DEBUG org.springframework.web.client.RestTemplate - Accept=[application/octet-stream, application/json, application/x-jackson-smile, application/cbor, application/*+json, */*]
14:56:13.112 [main] DEBUG org.springframework.web.client.RestTemplate - Response 200 OK
14:56:13.114 [main] DEBUG org.springframework.web.client.RestTemplate - Reading to [[B] as "image/jpeg"
文件下载请求结果状态码:200 OK
这种下载方法实际上是将下载文件一次性加载到客户端本地内存,然后从内存将文件写入磁盘。这种方式对于小文件的下载还比较适合,如果文件比较大或者文件下载并发量比较大,容易造成内存的大量占用,从而降低应用的运行效率。
这种下载方式的区别在于
/**
* 大文件下载
* @throws IOException
*/
@Test
public void testDownLoadBigFile() throws IOException {
// 待下载的文件地址
String url = "http://localhost:2000/spring-master/0968094e-7332-4705-9893-1884d42a5028.jpeg";
// 文件保存的本地路径
String targetPath = "/Users/lihuan/Documents/projects/git/master/target/2.jpeg";
//定义请求头的接收类型
RequestCallback requestCallback = request -> request.getHeaders()
.setAccept(Arrays.asList(MediaType.APPLICATION_OCTET_STREAM, MediaType.ALL));
//对响应进行流式处理而不是将其全部加载到内存中
restTemplate.execute(url, HttpMethod.GET, requestCallback, clientHttpResponse -> {
Files.copy(clientHttpResponse.getBody(), Paths.get(targetPath));
return null;
});
}
14:59:17.443 [main] DEBUG org.springframework.web.client.RestTemplate - HTTP GET http://localhost:2000/spring-master/0968094e-7332-4705-9893-1884d42a5028.jpeg
14:59:17.463 [main] DEBUG org.springframework.web.client.RestTemplate - Response 200 OK
RestTemplate请求结果异常是可以自定义处理的。
# hasError用于判断HttpResponse是否是异常响应(通过状态码)
boolean hasError(ClientHttpResponse response) throws IOException;
# handleError用于处理异常响应结果(非200状态码段)
void handleError(ClientHttpResponse response) throws IOException;
从HttpResponse解析出Http StatusCode,如果状态码StatusCode为null,就抛出UnknownHttpStatusCodeException异常。
@Override
public void handleError(ClientHttpResponse response) throws IOException {
HttpStatus statusCode = HttpStatus.resolve(response.getRawStatusCode());
if (statusCode == null) {
byte[] body = getResponseBody(response);
String message = getErrorMessage(response.getRawStatusCode(),
response.getStatusText(), body, getCharset(response));
throw new UnknownHttpStatusCodeException(message,
response.getRawStatusCode(), response.getStatusText(),
response.getHeaders(), body, getCharset(response));
}
handleError(response, statusCode);
}
实现自定义异常,实现ResponseErrorHandler 接口就可以。
public class RestErrorHandler implements ResponseErrorHandler {
/**
* 判断返回结果response是否是异常结果
* 主要是去检查response 的HTTP Status
* 仿造DefaultResponseErrorHandler实现即可
*/
@Override
public boolean hasError(ClientHttpResponse response) throws IOException {
int rawStatusCode = response.getRawStatusCode();
HttpStatus statusCode = HttpStatus.resolve(rawStatusCode);
return (statusCode != null ? statusCode.isError(): hasError(rawStatusCode));
}
protected boolean hasError(int unknownStatusCode) {
HttpStatus.Series series = HttpStatus.Series.resolve(unknownStatusCode);
return (series == HttpStatus.Series.CLIENT_ERROR || series == HttpStatus.Series.SERVER_ERROR);
}
@Override
public void handleError(ClientHttpResponse response) throws IOException {
// 里面可以实现你自己遇到了Error进行合理的处理
//TODO 将接口请求的异常信息持久化
System.out.println("handle Error");
}
}
@Test
public void testEntity() {
// 初始化RestTemplate
RestTemplate restTemplate = new RestTemplate();
restTemplate.setErrorHandler(new RestErrorHandler());
String url = "http://jsonplaceholder.typicode.com/postss/1";
ResponseEntity<String> responseEntity = restTemplate.getForEntity(url, String.class); //这行抛出异常
//下面两行代码执行不到
HttpStatus statusCode = responseEntity.getStatusCode(); // 获取响应码
System.out.println("HTTP 响应状态:" + statusCode);
}
17:59:48.378 [main] DEBUG org.springframework.web.client.RestTemplate - HTTP GET http://jsonplaceholder.typicode.com/postss/1
17:59:48.389 [main] DEBUG org.springframework.web.client.RestTemplate - Accept=[text/plain, application/json, application/x-jackson-smile, application/cbor, application/*+json, */*]
17:59:48.989 [main] DEBUG org.springframework.web.client.RestTemplate - Response 404 NOT_FOUND
handle Error
17:59:48.996 [main] DEBUG org.springframework.web.client.RestTemplate - Reading to [java.lang.String] as "application/json;charset=utf-8"
HTTP 响应状态:404 NOT_FOUND
通用的异常的处理机制:那就是自动重试。也就是说,在RestTemplate发送请求得到非200状态结果的时候,间隔一定的时间再次发送n次请求。n次请求都失败之后,最后抛出HttpClientErrorException。
通过maven坐标引入spring-retry,spring-retry的实现依赖于面向切面编程,所以引入aspectjweaver。以下配置过程都是基于Spring Boot应用。
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<version>1.2.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
在Spring Boot 应用入口启动类,也就是配置类的上面加上@SpringRetry注解,表示让重试机制生效。
import org.springframework.retry.annotation.EnableRetry;
@EnableRetry
将正确的请求服务地址由“/posts/1”改成“/postss/1”。服务不存在所以抛出404异常,是为了触发重试机制。
@RestController
@RequestMapping(value = "/restRetry")
public class RetryController {
@Resource
private RetryService retryService;
@GetMapping("/retry")
public HttpStatus retry() {
return retryService.retry();
}
}
@Service
public class RetryService {
@Resource
private RestTemplate restTemplate;
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Retryable(value = RestClientException.class, maxAttempts = 3, backoff = @Backoff(delay = 5000L,multiplier = 2))
public HttpStatus retry() {
System.out.println("发起远程API请求:" + DATE_TIME_FORMATTER.format(LocalDateTime.now()));
String url = "http://jsonplaceholder.typicode.com/postss/1";
ResponseEntity<String> responseEntity = restTemplate.getForEntity(url, String.class);
// 获取响应码
return responseEntity.getStatusCode();
}
}
@Retryable
注解的方法在发生异常时会重试,参数说明:@Backoff
注解为重试等待的策略,参数说明:http://localhost:2000/spring-master/restRetry/retry
发起远程API请求:2021-08-19 18:23:00
发起远程API请求:2021-08-19 18:23:06
发起远程API请求:2021-08-19 18:23:16
2021-08-19 18:23:17.263 ERROR 5111 --- [nio-2000-exec-4] c.s.m.自.c.GlobalExceptionHandlerAdvice : 系统异常:404 Not Found: [{}]
服务提供方通常会通过一定的授权、鉴权认证逻辑来保护API接口。其中比较简单、容易实现的方式就是使用HTTP 的Basic Auth来实现接口访问用户的认证。在服务端加入Basic Auth认证的情况下,该如何使用RestTemplate访问服务端接口。
"admin"
,密码是“ admin”,则将字符串"admin:admin"
使用Base64编码算法加密。加密结果可能是:YWtaW46YWRtaW4=。当然我们也可以不用自己去搭建服务端,给大家介绍一个提供免费在线的RESTful接口服务的网站:https://www.httpbin.org/。这个网站为我们提供了Basic Auth认证测试服务接口。如果我们只是为了学习RestTemplate,直接用这个网站提供的服务就可以了。
这个接口服务是通过OpenAPI(swagger)实现的,所以可以进行在线的访问测试。所以可以先通过页面操作测试一下,再开始下面学习使用RestTemplate访问服务端接口。
在HTTP请求头中携带Basic Auth认证的用户名和密码,具体实现参考下文代码注释:
@Test
public void testBasicAuth() {
//该url上携带用户名密码是httpbin网站测试接口的要求,
//真实的业务是不需要在url上体现basic auth用户名密码的
String url = "http://www.httpbin.org/basic-auth/admin/adminpwd";
//在请求头信息中携带Basic认证信息(这里才是实际Basic认证传递用户名密码的方式)
HttpHeaders headers = new HttpHeaders();
headers.set("authorization", "Basic " + Base64.getEncoder().encodeToString("admin:adminpwd".getBytes()));
//发送请求
HttpEntity<String> ans = restTemplate
.exchange(url, HttpMethod.GET, //GET请求
new HttpEntity<>(null, headers), //加入headers
String.class); //body响应数据接收类型
System.out.println(ans);
}
18:40:17.659 [main] DEBUG org.springframework.web.client.RestTemplate - HTTP GET http://www.httpbin.org/basic-auth/admin/adminpwd
18:40:17.670 [main] DEBUG org.springframework.web.client.RestTemplate - Accept=[text/plain, application/json, application/x-jackson-smile, application/cbor, application/*+json, */*]
18:40:18.389 [main] DEBUG org.springframework.web.client.RestTemplate - Response 200 OK
18:40:18.390 [main] DEBUG org.springframework.web.client.RestTemplate - Reading to [java.lang.String] as "application/json"
<200,{
"authenticated": true,
"user": "admin"
}
,[Date:"Thu, 19 Aug 2021 10:40:18 GMT", Content-Type:"application/json", Content-Length:"48", Connection:"keep-alive", Server:"gunicorn/19.9.0", Access-Control-Allow-Origin:"*", Access-Control-Allow-Credentials:"true"]>
上小节中的代码虽然实现了功能,但是不够好。因为每一次发送HTTP请求,我们都需要去组装HttpHeaders 信息,这样不好,造成大量的代码冗余。那么有没有一种方式可以实现可以一次性的为所有RestTemplate请求API添加Http Basic认证信息呢?答案就是:在RestTemplate Bean初始化的时候加入拦截器,以拦截器的方式统一添加Basic认证信息。
@Test
public void testBasicAuth2() {
// 初始化restTemplate
RestTemplate restTemplate = new RestTemplate();
// 实现一个拦截器:使用拦截器为每一个HTTP请求添加Basic Auth认证用户名密码信息
ClientHttpRequestInterceptor interceptor = (httpRequest, bytes, execution) -> {
httpRequest.getHeaders().set("authorization",
"Basic " + Base64.getEncoder().encodeToString("admin:adminpwd".getBytes()));
return execution.execute(httpRequest, bytes);
};
//添加拦截器
restTemplate.getInterceptors().add(interceptor);
// 该url上携带用户名密码是httpbin网站测试接口的要求,
// 真实的业务是不需要在url上体现basic auth用户名密码的
String url = "http://www.httpbin.org/basic-auth/admin/adminpwd";
// 发送请求
HttpEntity<String> ans = restTemplate.exchange(url, HttpMethod.GET, //GET请求
new HttpEntity<>(null), //加入headers
String.class); //body响应数据接收类型
System.out.println(ans);
}
18:49:43.763 [main] DEBUG org.springframework.web.client.RestTemplate - HTTP GET http://www.httpbin.org/basic-auth/admin/adminpwd
18:49:43.773 [main] DEBUG org.springframework.web.client.RestTemplate - Accept=[text/plain, application/json, application/x-jackson-smile, application/cbor, application/*+json, */*]
18:49:44.402 [main] DEBUG org.springframework.web.client.RestTemplate - Response 200 OK
18:49:44.403 [main] DEBUG org.springframework.web.client.RestTemplate - Reading to [java.lang.String] as "application/json"
<200,{
"authenticated": true,
"user": "admin"
}
,[Date:"Thu, 19 Aug 2021 10:49:44 GMT", Content-Type:"application/json", Content-Length:"48", Connection:"keep-alive", Server:"gunicorn/19.9.0", Access-Control-Allow-Origin:"*", Access-Control-Allow-Credentials:"true"]>
上面的方式使用了拦截器,但仍然是我们自己来封装HTTP headers请求头信息。进一步的简化方法就是,Spring RestTemplate 已经为我们提供了封装好的Basic Auth拦截器,我们直接使用就可以了,不需要我们自己去实现拦截器。
@Test
public void testBasicAuth3() {
// 初始化restTemplate
// 方式一
// RestTemplate restTemplate = new RestTemplate();
// restTemplate.getInterceptors().add(new BasicAuthenticationInterceptor("admin", "adminpwd"));
// 方式二
RestTemplate restTemplate = new RestTemplateBuilder().basicAuthentication("admin", "adminpwd").build();
// 该url上携带用户名密码是httpbin网站测试接口的要求,
// 真实的业务是不需要在url上体现basic auth用户名密码的
String url = "http://www.httpbin.org/basic-auth/admin/adminpwd";
// 在请求头信息中携带Basic认证信息(这里才是实际Basic认证传递用户名密码的方式)
HttpHeaders headers = new HttpHeaders();
headers.set("authorization", "Basic " + Base64.getEncoder().encodeToString("admin:adminpwd".getBytes()));
// 发送请求
HttpEntity<String> ans = restTemplate.exchange(url, HttpMethod.GET, //GET请求
new HttpEntity<>(null, headers), //加入headers
String.class); //body响应数据接收类型
System.out.println(ans);
}
代理Proxy作为跳板成为服务的直接访问者,代理使用者(真正的客户端)是间接访问服务。这样在服务端看来,每次请求是代理发出的,从代理IP池中一直更换代理发送请求,这样能够降低IP封锁的可能。
/**
* 代理使用者RestTemplate
*/
@Test
public void testProxyIp() {
RestTemplate restTemplate = new RestTemplate();
String url = "http://www.httpbin.org/ip";
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
requestFactory.setProxy(
new Proxy(Proxy.Type.HTTP, new InetSocketAddress("88.99.10.251", 1080) //设置代理服务
)
);
restTemplate.setRequestFactory(requestFactory);
//发送请求
String result = restTemplate.getForObject(url, String.class);
System.out.println(result); //打印响应结果
}
代理类型可以是HTTP也可以是SOCKS。下图是"http://www.httpbin.org/ip"的请求响应结果,返回的是代理服务器的ip,而不是我家里的ip。说明我们为RestTemplate 设置的代理生效了。
15:29:39.354 [main] DEBUG org.springframework.web.client.RestTemplate - HTTP GET http://www.httpbin.org/ip
15:29:39.368 [main] DEBUG org.springframework.web.client.RestTemplate - Accept=[text/plain, application/json, application/x-jackson-smile, application/cbor, application/*+json, */*]
15:29:45.479 [main] DEBUG org.springframework.web.client.RestTemplate - Response 200 OK
15:29:45.480 [main] DEBUG org.springframework.web.client.RestTemplate - Reading to [java.lang.String] as "application/json"
{
"origin": "88.99.10.251"
}
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。