重学springboot系列番外篇之RestTemplate
基本介绍及配置使用
什么是 RestTemplate?
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 编程的需求并没有那么高。
非Spring环境下使用RestTemplate
为了方便后续开发测试,首先介绍一个网站给大家。JSONPlaceholder 是一个提供免费的在线REST API的网站,我们在开发时可以使用它提供的url地址测试下网络请求以及请求参数。或者当我们程序需要获取一些模拟数据、模拟图片时也可以使用它。
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>
测试一下Hello world,使用RestTemplate发送一个GET请求,并把请求得到的JSON数据结果打印出来。
@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);
}
服务端是JSONPlaceholder 网站,帮我们提供的服务端API。需要注意的是:"http://jsonplaceholder.typicode.com/posts/1"服务URL,虽然URL里面有posts 这个单词,但是它的英文含义是:帖子或者公告,而不是我们的HTTP Post 协议。
所以说"http://jsonplaceholder.typicode.com/posts/1",请求的数据是:id为1的Post公告资源。打印结果如下:
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客户端实现。我们还可以把底层实现切换为Apache HttpComponents,okHttp等,我们后续章节会为大家介绍。
@Configuration
public class ContextConfig {
@Bean
public RestTemplate restTemplate(){
RestTemplate restTemplate = new RestTemplate();
return restTemplate;
}
}
在需要使用RestTemplate 的位置,注入并使用即可。
@Resource
private RestTemplate restTemplate;
底层HTTP客户端库的切换
RestTemplate只是对其他的HTTP客户端的封装,其本身并没有实现HTTP相关的基础功能。其底层实现是可以配置切换的,我们本小节就带着大家来看一下RestTemplate底层实现,及如何实现底层基础HTTP库的切换。
源码分析
RestTemplate 有一个非常重要的类叫做HttpAccessor ,可以理解为用于HTTP 接触访问的基础类。下图为源码:
从源码中我们可以分析出以下几点信息
SimpleClientHttpRequestFactory 。对应的HTTP 库是java JDK 自带的HttpURLConnection 。 HttpComponentsAsyncClientHttpRequestFactory 。对应的HTTP 库是Apache HttpComponents。 OkHttp3ClientHttpRequestFactory 。对应的HTTP 库是OkHttp
底层实现切换方法
从开发人员的反馈,和网上的各种HTTP客户端性能以及易用程度评测来看,OkHttp 优于 Apache HttpComponents、Apache HttpComponents优于HttpURLConnection。所以我个人更建议大家将底层HTTP实现切换为okHTTP。
切换为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() 之后使用就可以了。
切换为Apache HttpComponents
与切换为okHTTP方法类似、不再赘述。
<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;
}
设置超时时间
引入依赖之后,就来开始使用吧,任何一个Http 的Api 我们都可以设置请求的连接超时时间,请求超时时间,如果不设置的话,就可能会导致连接得不到释放,造成内存溢出。这个是我们需要重点注意的点,下面就来看看RestTemplate 如何来设置超时时间呢?我们可以在SimpleClientHttpRequestFactory 类中设置这两个时间,然后将factory 传给RestTemplate 实例,设置如下:
@Configuration
public class RestTemplateConfig {
private static final Integer READ_TIME_OUT = 6000;
private static final Integer CONNECT_TIME_OUT = 6000;
@Bean
public RestTemplate restTemplate(){
ClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient());
return new RestTemplate(requestFactory);
}
@Bean
public HttpClient httpClient(){
SSLConnectionSocketFactory sslConnectionSocketFactory = SSLConnectionSocketFactory.getSocketFactory();
SSLContext sslContext = null;
try {
sslContext = SSLContextBuilder.create().setProtocol(SSLConnectionSocketFactory.SSL)
.loadTrustMaterial((x, y) -> true).build();
} catch (Exception e) {
e.printStackTrace();
}
if (sslContext != null) {
sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext);
}
Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create()
.register("http", PlainConnectionSocketFactory.getSocketFactory())
.register("https", sslConnectionSocketFactory)
.build();
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(registry);
connectionManager.setMaxTotal(200);
connectionManager.setDefaultMaxPerRoute(100);
connectionManager.setValidateAfterInactivity(2000);
RequestConfig requestConfig = RequestConfig.custom()
.setSocketTimeout(READ_TIME_OUT)
.setConnectTimeout(CONNECT_TIME_OUT)
.setConnectionRequestTimeout(1000)
.build();
return HttpClientBuilder.create().setDefaultRequestConfig(requestConfig).setConnectionManager(connectionManager).build();
}
}
GET请求使用详解
RestTemplate可以发送HTTP GET请求,经常使用到的方法有两个:
二者的主要区别在于,getForObject()返回值是HTTP协议的响应体。getForEntity()返回的是ResponseEntity,ResponseEntity是对HTTP响应的封装,除了包含响应体,还包含HTTP状态码、contentType、contentLength、Header等信息。
getForObject() 方法
以String的方式接受请求结果数据
在Spring环境下写一个单元测试用例,以String类型接收响应结果信息
@SpringBootTest
class ResttemplateWithSpringApplicationTests {
@Resource
private RestTemplate restTemplate;
@Test
void testSimple() {
String url = "http://jsonplaceholder.typicode.com/posts/1";
String str = restTemplate.getForObject(url, String.class);
System.out.println(str);
}
}
getForObject第二个参数为返回值的类型,String.class以字符串的形式接受getForObject响应结果,
以POJO对象的方式接受结果数据
在Spring环境下写一个单元测试用例,以java POJO对象接收响应结果信息
@Test
public void testPoJO() {
String url = "http://jsonplaceholder.typicode.com/posts/1";
PostDTO postDTO = restTemplate.getForObject(url, PostDTO.class);
System.out.println(postDTO.toString());
}
输出打印结果如下:
POJO的定义如下,根据JSON String的数据格式定义。
@Data
public class PostDTO {
private int userId;
private int id;
private String title;
private String body;
}
以数组的方式接收请求结果
访问http://jsonplaceholder.typicode.com/posts可以获得JSON数组方式的请求结果
下一步就是我们该如何接收,使用方法也很简单
@Test
public void testArrays() {
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",只是使用了占位符语法,这样在业务使用上更加灵活。
url = baseUrl+"?userName={?}&userId={?}";
resultData = restTemplate.getForObject(url, ResultData.class, "张三2",2);
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);
使用{xx} 来传递参数时,这个xx对应的就是map中的key
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()方法小结
@Nullable
<T> T getForObject(String url, Class<T> responseType, Object... uriVariables) throws RestClientException;
@Nullable
<T> T getForObject(String url, Class<T> responseType, Map<String, ?> uriVariables) throws RestClientException;
@Nullable
<T> T getForObject(URI url, Class<T> responseType) throws RestClientException;
@Test
public void getForObjectTest() {
String baseUrl = "http://localhost:8081/testRestTemplateApp/getUser.do";
String url =baseUrl+"?userName=张三1&userId=1";
ResultData resultData = restTemplate.getForObject(url, ResultData.class);
System.out.println("*****GET直接拼接参数查询返回结果={}" + JSON.toJSONString(resultData));
url = baseUrl+"?userName={?}&userId={?}";
resultData = restTemplate.getForObject(url, ResultData.class, "张三2",2);
System.out.println("*****GET传参替换查询返回结果={}" + JSON.toJSONString(resultData));
url = baseUrl + String.format("?userName=%s&userId=%s", "张三2",2);
resultData = restTemplate.getForObject(url, ResultData.class);
System.out.println("******GET使用String.format查询返回结果={}" + JSON.toJSONString(resultData));
url = baseUrl + "?userName={userName}&userId={userId}";
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("userName", "张三1");
paramMap.put("userId",1);
resultData = restTemplate.getForObject(url, ResultData.class, paramMap);
System.out.println("******GET使用Map查询返回结果={}" + JSON.toJSONString(resultData));
URI uri = URI.create(baseUrl+"?userName=%E5%BC%A0%E4%B8%891&userId=1");
ResultData resultData1 = restTemplate.getForObject(uri, ResultData.class);
System.out.println("******GET使用URI查询返回结果={}" + JSON.toJSONString(resultData1));
}
String url ="http://localhost:8081/testRestTemplateApp/getUser.do?userName=张三1&userId=1";
String resultData = restTemplate.getForObject(url, String.class);
URI uri = URI.create(baseUrl+"?userName=%E5%BC%A0%E4%B8%891&userId=1");
ResultData resultData1 = restTemplate.getForObject(uri, ResultData.class);
getForEntity()方法
上面的所有的getForObject 请求传参方法,getForEntity 都可以使用,使用方法上也几乎是一致的,只是在返回结果接收的时候略有差别。使用ResponseEntity<T> responseEntity 来接收响应结果。用responseEntity.getBody() 获取响应体。响应体内容同getForObject 方法返回结果一致。剩下的这些响应信息就是getForEntity比getForObject 多出来的内容。
getForEntity()的三个重载方法:
<T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Object... uriVariables)
throws RestClientException;
<T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Map<String, ?> uriVariables)
throws RestClientException;
<T> ResponseEntity<T> getForEntity(URI url, Class<T> responseType) throws RestClientException;
使用演示:
@Test
public void testEntityPoJo() {
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());
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);
}
输出打印结果
POST请求使用详解
其实POST请求方法和GET请求方法上大同小异,RestTemplate的POST请求也包含两个主要方法:
二者的主要区别在于,postForObject()返回值是HTTP协议的响应体。postForEntity()返回的是ResponseEntity,ResponseEntity是对HTTP响应的封装,除了包含响应体,还包含HTTP状态码、contentType、contentLength、Header等信息。
postForObject发送JSON格式请求
三个重载方法
@Nullable
<T> T postForObject(String url, @Nullable Object request, Class<T> responseType,
Object... uriVariables) throws RestClientException;
@Nullable
<T> T postForObject(String url, @Nullable Object request, Class<T> responseType,
Map<String, ?> uriVariables) throws RestClientException;
@Nullable
<T> T postForObject(URI url, @Nullable Object request, Class<T> responseType) throws RestClientException;
写一个单元测试用例,测试用例的内容是向指定的URL提交一个Post(帖子).
@SpringBootTest
class PostTests {
@Resource
private RestTemplate restTemplate;
@Test
void testSimple() {
String url = "http://jsonplaceholder.typicode.com/posts";
PostDTO postDTO = new PostDTO();
postDTO.setUserId(110);
postDTO.setTitle("dhy 发布文章");
postDTO.setBody("dhy 发布文章 测试内容");
PostDTO result = restTemplate.postForObject(url, postDTO, PostDTO.class);
System.out.println(result);
}
}
url支持占位符语法
如果url地址上面需要传递一些动态参数,可以使用占位符的方式:
String url = "http://jsonplaceholder.typicode.com/{1}/{2}";
String url = "http://jsonplaceholder.typicode.com/{type}/{id}";
具体的用法和使用GET方法请求是一致的
注意
@Test
public void testPostForObjectForForm() {
String baseUrl = "http://localhost:8081/testRestTemplateApp/getUser.do";
MultiValueMap<String, Object> request = new LinkedMultiValueMap<>();
request.set("userName","张三1");
request.set("userId",1);
ResultData resultData = restTemplate.postForObject(baseUrl,request, ResultData.class);
System.out.println("*****POST表单提交使用URI查询返回结果={}" + JSON.toJSONString(resultData));
URI uri = URI.create(baseUrl);
resultData = restTemplate.postForObject(uri,request, ResultData.class);
System.out.println("******POST使用URI查询返回结果={}" + JSON.toJSONString(resultData));
}
运行结果如下:
从运行结果我们可以看出,如果传入的参数是MultiValueMap 类型的对象是,Spring会通过AllEncompassingFormHttpMessageConverter 转换器来将参数通过表单提交。
如果直接传入一个Map 对象,则会通过MappingJackson2HttpMessageConverter 转换器对参数进行转换。
说完了表单提交,下面我们看看另外一种场景,如下,这个接口是一个保存用户数据的接口,参数需要格式化后放在请求体中。
@ResponseBody
@PostMapping("/addUserJSON.do")
public ResultData<Boolean> addUserJSON(@RequestBody User user) {
if (user == null) {
return new ResultData<>(HttpStatus.BAD_REQUEST.value(), null, "参数不能为空");
}
return new ResultData<>(HttpStatus.OK.value(),true,"保存成功");
}
当我们需要调用接口是通过@RequestBody 来接受参数时,也就是需要传入一个JSON 对象,我们该如何请求呢?
我们调用可以postForObject 可以直接传入User 对象, 也可以将请求头设置成application/json ,然后将User对象序列化 ,代码如下所示:
@Test
public void testPostForObject() {
String baseUrl = "http://localhost:8081/testRestTemplateApp/addUserJSON.do";
User user = new User();
user.setUserName("李四");
user.setAge(23);
ResultData resultData = restTemplate.postForObject(baseUrl, user, ResultData.class);
System.out.println("*********不序列化传入参数请求结果={}" + JSON.toJSONString(resultData));
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity httpEntity = new HttpEntity(JSON.toJSONString(user),headers);
resultData = restTemplate.postForObject(baseUrl, httpEntity, ResultData.class);
System.out.println("*********序列化参数请求结果={}" + JSON.toJSONString(resultData));
}
第一种方式是由于Spring内部的MappingJackson2HttpMessageConverter 会将参数进行序列化并请求接口
第二种方式是直接设置好请求头为application/json ,并将参数序列化。所以就不需要通过MappingJackson2HttpMessageConverter 进行转换。比较推荐
运行结果如下:
postForEntity()方法
上面的所有的postForObject 请求传参方法,postForEntity 都可以使用,使用方法上也几乎是一致的,只是在返回结果接收的时候略有差别。使用ResponseEntity<T> responseEntity 来接收响应结果。用responseEntity.getBody() 获取响应体。响应体内容同postForObject 方法返回结果一致。剩下的这些响应信息就是postForEntity比postForObject 多出来的内容。
@Test
public void testEntityPoJo() {
String url = "http://jsonplaceholder.typicode.com/posts";
PostDTO postDTO = new PostDTO();
postDTO.setUserId(110);
postDTO.setTitle("dhy 发布文章");
postDTO.setBody("dhy 发布文章 测试内容");
ResponseEntity<String> responseEntity
= restTemplate.postForEntity(url, postDTO, String.class);
String body = responseEntity.getBody();
System.out.println("HTTP 响应body:" + postDTO.toString());
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);
}
输出打印结果
postForLocation() 方法的使用
postForLocation 的定义是POST 数据到一个URL ,返回新创建资源的URL,就是重定向或者页面跳转。
同样提供了三个方法,分别如下,需要注意的是返回结果为URI对象,即网络资源
public URI postForLocation(String url, @Nullable Object request, Object... uriVariables)
throws RestClientException ;
public URI postForLocation(String url, @Nullable Object request, Map<String, ?> uriVariables)
throws RestClientException ;
public URI postForLocation(URI url, @Nullable Object request) throws RestClientException ;
这类接口主要应用在需要跳转页面的请求,比如,登录,注册,支付等post请求,请求成功之后需要跳转到成功的页面。这种场景下我们可以使用postForLocation了,提交数据,并获取放回的URI,一个测试如下:
首先mock一个接口
@ResponseBody
@RequestMapping(path = "loginSuccess")
public String loginSuccess(String userName, String password) {
return "welcome " + userName;
}
@RequestMapping(path = "login", method = {RequestMethod.GET, RequestMethod.OPTIONS, RequestMethod.POST}
,produces = "charset/utf8")
public String login(@RequestParam(value = "userName", required = false) String userName,
@RequestParam(value = "password", required = false) String password) {
return "redirect:/loginSuccess?userName=" + userName + "&password=" + password + "&status=success";
}
测试请求是:
@Test
public void testPostLocation() {
String url = "http://localhost:8081/testRestTemplateApp/login";
MultiValueMap<String, String> paramMap = new LinkedMultiValueMap<>();
paramMap.add("userName", "bob");
paramMap.add("password", "1212");
URI location = restTemplate.postForLocation(url, paramMap);
System.out.println("*******返回的数据=" + location);
}
运行结果如下:
HTTP method使用方法详解
RESTful风格与HTTP method
熟悉RESTful风格的朋友,应该了解RESTful风格API使用HTTP method表达对资源的操作。
常用HTTP方法 | RESTful风格语义(操作) |
---|
GET | 查询、获取数据 | POST | 新增、提交数据 | DELETE | 删除数据 | PUT | 更新、修改数据 | HEAD | 获取HTTP请求头数据 | OPTIONS | 判断URL提供的当前API支持哪些HTTP method方法 |
在前面的章节,我已经为大家详细的介绍了RestTemplate的GET和POST的相关的使用方法,本节来为大家介绍DELETE、PUT、HEAD、OPTIONS。
使用 DELETE方法去删除资源
删除一个已经存在的资源,使用RestTemplate的delete(uri)方法。该方法会向URL代表的资源发送一个HTTP DELETE方法请求。
@Test
void testDelete() {
String url = "http://jsonplaceholder.typicode.com/posts/1";
restTemplate.delete(url);
}
使用PUT方法去修改资源
修改一个已经存在的资源,使用RestTemplate的put()方法。该方法会向URL代表的资源发送一个HTTP PUT方法请求。
@Test
void testPut() {
String url = "http://jsonplaceholder.typicode.com/posts/1";
PostDTO postDTO = new PostDTO();
postDTO.setUserId(110);
postDTO.setTitle("zimug 发布文章");
postDTO.setBody("zimug 发布文章 测试内容");
restTemplate.put(url, postDTO);
}
通用请求方法exchange方法
exchange方法是一个通用的方法,它可以发送GET、POST、DELETE、PUT等等HTTP方法请求。
该方法以method方式的请求调用远程RESTFUL服务,其中httpEntity参数用于指定请求参数
public <T> T toPostEntity(String url, HttpEntity httpEntity, Class<T> responseType) {
ResponseEntity<T> responseEntity = restTemplate.exchange(url, HttpMethod.POST, httpEntity, responseType);
logger.info("请求地址是={},响应结果是={}", url, new Gson().toJson(responseEntity));
if (HttpStatus.OK.value() != responseEntity.getStatusCodeValue() || responseEntity.getStatusCode().isError()) {
throw new BusinessException(ErrorCode.RESULT_CODE_ERROR);
}
return responseEntity.getBody();
}
下面的两种方式发送GET请求效果是一样的
ResponseEntity<PostDTO> responseEntity
= restTemplate.getForEntity(url, PostDTO.class);
ResponseEntity<PostDTO> responseEntity = restTemplate.exchange(url, HttpMethod.GET,
null, PostDTO.class);
下面的两种方式发送POST请求效果是一样的
ResponseEntity<String> responseEntity
= restTemplate.postForEntity(url, postDTO, String.class);
ResponseEntity<String> responseEntity
= restTemplate.exchange(url, HttpMethod.POST,null, String.class);
下面的两种方式发送DELETE请求效果是一样的,只是一个有返回值,一个返回值为void
restTemplate.delete(url);
ResponseEntity<String> result = restTemplate.exchange(url, HttpMethod.DELETE,null,String.class);
上面为大家举了几个用exchange()发送请求的例子,exchange()还能针对很多的HTTP method类型发送请求,是通用方法!
使用HEAD方法获取HTTP请求头数据
使用headForHeaders()API 获取某个资源的URI的请求头信息,并且只专注于获取HTTP请求头信息。
@Test
public void testHEAD() {
String url = "http://jsonplaceholder.typicode.com/posts/1";
HttpHeaders httpHeaders = restTemplate.headForHeaders(url);
assertTrue(httpHeaders.getContentType()
.includes(MediaType.APPLICATION_JSON));
System.out.println(httpHeaders);
}
使用OPTIONS获取HTTP资源支持的method
下文代码使用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};
assertTrue(optionsForAllow.containsAll(Arrays.asList(supportedMethods)));
}
文件上传与下载
文件上传
写一个单元测试类,来完成RestTemplate文件上传功能,具体实现细节参考代码注释
@SpringBootTest
class UpDownLoadTests {
@Resource
private RestTemplate restTemplate;
@Test
void testUpload() {
String url = "http://localhost:8888/upload";
String filePath = "D:\\data\\local\\splash.png";
FileSystemResource resource = new FileSystemResource(new File(filePath));
MultiValueMap<String, Object> param = new LinkedMultiValueMap<>();
param.add("uploadFile", resource);
System.out.println("--- 开始上传文件 ---");
String result = restTemplate.postForObject(url, param, String.class);
System.out.println("--- 访问地址:" + result);
}
}
输出结果如下:
--- 开始上传文件 ---
--- 访问地址:http://localhost:8888/2020/08/12/028b38f1-3f9b-4088-9bea-1af8c18cd619.png
文件上传之后,可以通过上面的访问地址,在浏览器访问。或者通过RestTemplate客户端进行下载。
文件下载
执行下列代码之后,被下载文件url,会被正确的保存到本地磁盘目录targetPath。
@Test
void testDownLoad() throws IOException {
String url = "http://localhost:8888/2020/08/12/028b38f1-3f9b-4088-9bea-1af8c18cd619.png";
ResponseEntity<byte[]> rsp = restTemplate.getForEntity(url, byte[].class);
System.out.println("文件下载请求结果状态码:" + rsp.getStatusCode());
String targetPath = "D:\\data\\local\\splash-down.png";
Files.write(Paths.get(targetPath), Objects.requireNonNull(rsp.getBody(),
"未获取到下载文件"));
}
这种下载方法实际上是将下载文件一次性加载到客户端本地内存,然后从内存将文件写入磁盘。这种方式对于小文件的下载还比较适合,如果文件比较大或者文件下载并发量比较大,容易造成内存的大量占用,从而降低应用的运行效率。
大文件的下载
这种下载方式的区别在于
@Test
void testDownLoadBigFile() throws IOException {
String url = "http://localhost:8888/2020/08/12/028b38f1-3f9b-4088-9bea-1af8c18cd619.png";
String targetPath = "D:\\data\\local\\splash-down-big.png";
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;
});
}
execute方法是restTemplate的底层实现
注意:使用execute方法调用restFul服务的时候,HttpMessageConverter不会自动起作用,因此开发者需要直接才能够底层I/O级别来发送请求处理响应,因此如果使用excute方法,还想把响应的JSON字符串或者请求参数直接转换为一个pojo对象,会报下面这个错误,原因一开始就说了
请求失败异常处理
异常现象
在使用RestTemplate进行远程接口服务调用的时候,当请求的服务出现异常:超时、服务不存在等情况的时候(响应状态非200、而是400、500HTTP状态码),就会抛出如下异常:
该异常我是模拟出来的,将正确的请求服务地址由“/posts/1”改成“/postss/1”。服务不存在所以抛出404异常。
@Test
public void testEntity() {
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);
}
异常抛出之后,程序后面的代码就执行不到了,无法进行后面的代码执行。实际的业务开发中,有的时候我们更期望的结果是:不管你服务端是超时了还是服务不存在,我们都应该获得最终的请求结果(HTTP请求结果状态400、500),而不是获得一个抛出的异常。
源码解析-默认实现
首先我要说一个结论:RestTemplate请求结果异常是可以自定义处理的。在开始进行自定义的异常处理逻辑之前,我们有必要看一下异常处理的默认实现。也就是:为什么会产生上面小节提到的现象?
接口的第一个方法hasError用于判断HttpResponse是否是异常响应(通过状态码) 接口的第二个方法handleError用于处理异常响应结果(非200状态码段)
所以我们就来看看DefaultResponseErrorHandler是如何来处理异常响应的?
从HttpResponse解析出Http StatusCode,如果状态码StatusCode为null,就抛出UnknownHttpStatusCodeException异常。
如果StatusCode存在,则解析出StatusCode的series,也就是状态码段(除了200段,其他全是异常状态码),解析规则是StatusCode/100取整。
public enum Series {
INFORMATIONAL(1),
SUCCESSFUL(2),
REDIRECTION(3),
CLIENT_ERROR(4),
SERVER_ERROR(5);
}
进一步针对客户端异常和服务端异常进行处理,处理的方法是抛出HttpClientErrorException。也就是第一小节出现的异常的原因
RestTemplate自定义异常处理
所以我们要实现自定义异常,实现ResponseErrorHandler 接口就可以。
public class MyRestErrorHandler implements ResponseErrorHandler {
@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 {
}
}
将MyRestErrorHandler 在RestTemplate实例化的时候进行注册
这时再去执行第一小节中的示例代码,就不会抛出异常。而是得到一个HTTP Status 404的结果。我们可以根据这个结果,在程序中继续向下执行代码。
自动重试机制
在上一节我们为大家介绍了,当RestTemplate发起远程请求异常时的自定义处理方法,我们可以通过自定义的方式解析出HTTP Status Code状态码,然后根据状态码和业务需求决定程序下一步该如何处理。
本节为大家介绍另外一种通用的异常的处理机制:那就是自动重试。也就是说,在RestTemplate发送请求得到非200状态结果的时候,间隔一定的时间再次发送n次请求。n次请求都失败之后,最后抛出HttpClientErrorException。
在开始本节代码之前,将上一节的RestTemplate自定义异常处理的代码注释掉,否则自动重试机制不会生效。如下(参考上一节代码):
Spring Retry配置生效
通过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注解,表示让重试机制生效。
使用案例
@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 testEntity() {
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 注解为重试等待的策略,参数说明:
写一个测试的RetryController 对RetryService 的testEntity方法进行调用
@RestController
public class RetryController {
@Resource
private RetryService retryService;
@GetMapping("/retry")
public HttpStatus test() {
return retryService.testEntity();
}
}
测试结果
向http://localhost:8080/retry发起请求,结果如下:
从结果可以看出:
通过BasicAuth认证
服务提供方通常会通过一定的授权、鉴权认证逻辑来保护API接口。其中比较简单、容易实现的方式就是使用HTTP 的Basic Auth来实现接口访问用户的认证。我们本节就来为大家介绍一下,在服务端加入Basic Auth认证的情况下,该如何使用RestTemplate访问服务端接口。
HttpBasic认证原理说明
HTTP Basic Auth服务端实现
如果你想自己搭建一个服务端,那么如何为Spring Boot 服务添加Basic Auth认证?
给大家介绍一个提供免费在线的RESTful接口服务的网站:http://httpbin.org/。这个网站为我们提供了Basic Auth认证测试服务接口。如果我们只是为了学习RestTemplate,直接用这个网站提供的服务就可以了。
浏览器访问地址:http://www.httpbin.org/#/Auth/get_basic_auth__user___passwd_ ,这个接口服务是通过OpenAPI(swagger)实现的,所以可以进行在线的访问测试。所以可以先通过页面操作测试一下,再开始下面学习使用RestTemplate访问服务端接口。
请求头方式携带认证信息
在HTTP请求头中携带Basic Auth认证的用户名和密码,具体实现参考下文代码注释:
@SpringBootTest
class BasicAuthTests {
@Resource
private RestTemplate restTemplate;
@Test
void testBasicAuth() {
String url = "http://www.httpbin.org/basic-auth/admin/adminpwd";
HttpHeaders headers = new HttpHeaders();
headers.set("authorization",
"Basic " +
Base64.getEncoder()
.encodeToString("admin:adminpwd".getBytes()));
HttpEntity<String> ans = restTemplate
.exchange(url,
HttpMethod.GET,
new HttpEntity<>(null, headers),
String.class);
System.out.println(ans);
}
}
测试用例执行成功,说明RestTemplate 正确的携带了Basic 认证信息,得到正常的响应结果:200。
拦截器方式携带认证信息
上面的代码虽然实现了功能,但是不够好。因为每一次发送HTTP请求,我们都需要去组装HttpHeaders 信息,这样不好,造成大量的代码冗余。那么有没有一种方式可以实现可以一次性的为所有RestTemplate请求API添加Http Basic认证信息呢?答案就是:在RestTemplate Bean初始化的时候加入拦截器,以拦截器的方式统一添加Basic认证信息。
@Configuration
public class ContextConfig {
@Bean("OKHttp3")
public RestTemplate OKHttp3RestTemplate(){
RestTemplate restTemplate = new RestTemplate(getClientHttpRequestFactory());
restTemplate.getInterceptors().add(getCustomInterceptor());
return restTemplate;
}
private ClientHttpRequestInterceptor getCustomInterceptor(){
ClientHttpRequestInterceptor interceptor = (httpRequest, bytes, execution) -> {
httpRequest.getHeaders().set("authorization",
"Basic " +
Base64.getEncoder()
.encodeToString("admin:adminpwd".getBytes()));
return execution.execute(httpRequest, bytes);
};
return interceptor;
}
private ClientHttpRequestFactory getClientHttpRequestFactory() {
int timeout = 100000;
OkHttp3ClientHttpRequestFactory clientHttpRequestFactory
= new OkHttp3ClientHttpRequestFactory();
clientHttpRequestFactory.setConnectTimeout(timeout);
return clientHttpRequestFactory;
}
}
在RestTemplate Bean初始化的时候加入拦截器之后,上面的代码就可以省略HttpHeaders Basic Auth请求头携带信息的组装过程。发送请求,结果和上面的效果是一样的。
进一步简化
上面的方式使用了拦截器,但仍然是我们自己来封装HTTP headers请求头信息。进一步的简化方法就是,Spring RestTemplate 已经为我们提供了封装好的Basic Auth拦截器,我们直接使用就可以了,不需要我们自己去实现拦截器。
下面的方法是在RestTemplate Bean实例化的时候使用RestTemplateBuilder,自带basicAuthentication。所以到这里拦截器也不需要了(实际底层代码实现仍然是拦截器,只是api层面不需要指定拦截器了)。
发送请求,结果和第三小节中的效果是一样的。
这里没有对RestTemplateBuilder和拦截器进行深入分析,大家可以自行查阅资料了解,包括还可以替换消息转换器等功能,由于篇幅原因,这里就不多讲了
总结
介绍完了restTemplate 的常用方法,但是,我们或许会感觉到restTemplate 的方法太多了,调用起来不太方便,为了使用方便,我们就对restTemplate 做一个封装。代码如下所示:主要封装成了四个方法,一个是通过get请求的方法,一个是通过表单提交的post请求方法,一个是通过json提交的post请求方法,最后就是上传图片的方法。
@Component
public class RestTemplateProxy {
@Autowired
private RestTemplate restTemplate;
public <T> T getForObject(String url, Class<T> responseType) {
return restTemplate.getForObject(url, responseType);
}
public <T> T postForObjectJSON(String url, Object requestParam,Class<T> responseType) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity httpEntity = new HttpEntity(requestParam, headers);
return restTemplate.postForObject(url, httpEntity, responseType);
}
public <T> T postForObjectForm(String url, @NotNull Object requestParam, Class<T> responseType) {
MultiValueMap<String, Object> valueRequestMap = createValueMap(requestParam);
return restTemplate.postForObject(url, valueRequestMap, responseType);
}
public <T> T postForEntityHeader(String url, Object requestParam, HttpHeaders headers, Class<T> response) {
MultiValueMap<String, Object> requestEntity = createValueMap(requestParam);
HttpEntity<MultiValueMap<String, Object>> httpEntity = new HttpEntity<>(requestEntity,headers);
return restTemplate.postForObject(url, httpEntity, response);
}
public <T> T uploadImg(@NotNull String url, @NotNull MultiValueMap<String, Object> body,Class<T> responseType) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body,headers);
return restTemplate.postForObject(url,requestEntity,responseType);
}
public <T> T toPostEntity(String url, HttpEntity httpEntity, Class<T> responseType) {
ResponseEntity<T> responseEntity = restTemplate.exchange(url, HttpMethod.POST, httpEntity, responseType);
logger.info("请求地址是={},响应结果是={}", url, new Gson().toJson(responseEntity));
if (HttpStatus.OK.value() != responseEntity.getStatusCodeValue() || responseEntity.getStatusCode().isError()) {
throw new BusinessException(ErrorCode.RESULT_CODE_ERROR);
}
return responseEntity.getBody();
}
private MultiValueMap createValueMap(Object requestParam) {
MultiValueMap<String, Object> valueRequestMap = new LinkedMultiValueMap<>();
Map<String, Object> param = null;
if (requestParam instanceof Map) {
param = (Map<String, Object>) requestParam;
} else {
param = BeanUtil.beanToMap(requestParam);
}
for (String key : param.keySet()) {
valueRequestMap.add(key, param.get(key));
}
return valueRequestMap;
}
}
这里需要重点说下,图片上传的方法,上传图片的话,我们一定要把请求头设置成multipart/form-data ,然后其余的参数通过MultiValueMap 来设置。
public <T> T uploadImg(@NotNull String url, @NotNull MultiValueMap<String, Object> body,Class<T> responseType) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body,headers);
return restTemplate.postForObject(url,requestEntity,responseType);
}
Spring RestTemplate为何必须搭配MultiValueMap?
Spring RestTemplate为何必须搭配MultiValueMap?
一言蔽之:MultiValueMap会以表单形式提交给服务器端,而HashMap会以json请求体形式提交过去
|