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
风格设计,使用效果将更加的完美
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公告资源。打印结果如下:
将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 {
//默认使用JDK 自带的HttpURLConnection作为底层实现
@Bean
public RestTemplate restTemplate(){
RestTemplate restTemplate = new RestTemplate();
return restTemplate;
}
}
在需要使用RestTemplate 的位置,注入并使用即可。
@Resource //@AutoWired
private RestTemplate restTemplate;
RestTemplate只是对其他的HTTP客户端的封装,其本身并没有实现HTTP相关的基础功能。其底层实现是可以配置切换的,我们本小节就带着大家来看一下RestTemplate底层实现,及如何实现底层基础HTTP库的切换。
RestTemplate
有一个非常重要的类叫做HttpAccessor
,可以理解为用于HTTP
接触访问的基础类。下图为源码:
从源码中我们可以分析出以下几点信息
RestTemplate 支持至少三种HTTP客户端库。
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()
之后使用就可以了。
与切换为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 {
/** * 服务器返回数据(response)的时间 */
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 {
//信任所有的SSL证书
sslContext = SSLContextBuilder.create().setProtocol(SSLConnectionSocketFactory.SSL)
.loadTrustMaterial((x, y) -> true).build();
} catch (Exception e) {
e.printStackTrace();
}
if (sslContext != null) {
sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext);
}
// 支持HTTP、HTTPS
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()
// 服务器返回数据(response)的时间,超时抛出read timeout
.setSocketTimeout(READ_TIME_OUT)
// 连接上服务器(握手成功)的时间,超时抛出connect timeout
.setConnectTimeout(CONNECT_TIME_OUT)
// 从连接池中获取连接的超时时间,超时抛出ConnectionPoolTimeoutException
.setConnectionRequestTimeout(1000)
.build();
return HttpClientBuilder.create().setDefaultRequestConfig(requestConfig).setConnectionManager(connectionManager).build();
}
}
RestTemplate可以发送HTTP GET请求,经常使用到的方法有两个:
二者的主要区别在于,getForObject()返回值是HTTP协议的响应体。getForEntity()返回的是ResponseEntity,ResponseEntity是对HTTP响应的封装,除了包含响应体,还包含HTTP状态码、contentType、contentLength、Header等信息。
在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响应结果,
在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);
/** 方法一,直接将参数添加到url上面。 * Retrieve a representation by doing a GET on the specified URL. * The response (if any) is converted and returned. * <p>URI Template variables are expanded using the given URI variables, if any. * @param url the URL 请求地址 * @param responseType the type of the return value 响应体的类型 * @param uriVariables the variables to expand the template 传入的参数 * @return the converted object */
@Nullable
<T> T getForObject(String url, Class<T> responseType, Object... uriVariables) throws RestClientException;
/** 方法二,通过Map来提交参数。 * Retrieve a representation by doing a GET on the URI template. * The response (if any) is converted and returned. * <p>URI Template variables are expanded using the given map. * @param url the URL * @param responseType the type of the return value * @param uriVariables the map containing variables for the URI template * @return the converted object */
@Nullable
<T> T getForObject(String url, Class<T> responseType, Map<String, ?> uriVariables) throws RestClientException;
/** 方法三,用URI来请求。 * Retrieve a representation by doing a GET on the URL . * The response (if any) is converted and returned. * @param url the URL * @param responseType the type of the return value * @return the converted object */
@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));
//方法一:传参替换,使用String.format,推荐使用
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));
//方法二:使用Map,不推荐使用
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 = 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));
}
application/json;charset=UTF-8
格式的时候,返回的数据类型可以直接写String.class,
如下String url ="http://localhost:8081/testRestTemplateApp/getUser.do?userName=张三1&userId=1";
String resultData = restTemplate.getForObject(url, String.class);
//userName不能直接传入张三1,不然会报400的错误
URI uri = URI.create(baseUrl+"?userName=%E5%BC%A0%E4%B8%891&userId=1");
//responseType不能传入String.class,不然会报406的错误
ResultData resultData1 = restTemplate.getForObject(uri, ResultData.class);
上面的所有的getForObject
请求传参方法,getForEntity
都可以使用,使用方法上也几乎是一致的,只是在返回结果接收的时候略有差别。使用ResponseEntity<T> responseEntity
来接收响应结果。用responseEntity.getBody()
获取响应体。响应体内容同getForObject
方法返回结果一致。剩下的这些响应信息就是getForEntity比getForObject
多出来的内容。
HttpStatus statusCode =responseEntity.getStatusCode()
;获取整体的响应状态信息int statusCodeValue = responseEntity.getStatusCodeValue()
; 获取响应码值HttpHeaders headers = responseEntity.getHeaders()
;获取响应头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());
//以下是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);
}
输出打印结果
其实POST请求方法和GET请求方法上大同小异,RestTemplate的POST请求也包含两个主要方法:
二者的主要区别在于,postForObject()返回值是HTTP协议的响应体。postForEntity()返回的是ResponseEntity,ResponseEntity是对HTTP响应的封装,除了包含响应体,还包含HTTP状态码、contentType、contentLength、Header等信息。
三个重载方法
/** * @param url the URL 请求地址 * @param request the Object to be POSTed (may be {@code null}) 请求体,可以传入一个Bean对象,也可以传入HttpEntity对象,包装请求头 * @param responseType the type of the return value 响应对象的类型 * @param uriVariables the variables to expand the template 传入的参数 * @return the converted object * @see HttpEntity */
@Nullable
<T> T postForObject(String url, @Nullable Object request, Class<T> responseType,
Object... uriVariables) throws RestClientException;
/** * @param url the URL 请求地址 * @param request the Object to be POSTed (may be {@code null}) 请求体,可以传入一个Bean对象,也可以传入HttpEntity对象,包装请求头 * @param responseType the type of the return value 响应对象的类型 * @param uriVariables the variables to expand the template 传入的map * @return the converted object * @see HttpEntity */
@Nullable
<T> T postForObject(String url, @Nullable Object request, Class<T> responseType,
Map<String, ?> uriVariables) throws RestClientException;
/** * @param url the URL * @param request the Object to be POSTed (may be {@code null}) * @param responseType the type of the return value * @return the converted object * @see HttpEntity */
@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 发布文章 测试内容");
// 发送post请求,并输出结果
PostDTO result = restTemplate.postForObject(url, postDTO, PostDTO.class);
System.out.println(result);
}
}
postForObject方法第二个参数是请求数据对象,第三个参数是返回值类型
如果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 = 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);
//第一种方式:不传入JSON的参数,不设置请求头
ResultData resultData = restTemplate.postForObject(baseUrl, user, ResultData.class);
System.out.println("*********不序列化传入参数请求结果={}" + JSON.toJSONString(resultData));
//第二种方式:传入JSON类型的参数,设置请求头
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
进行转换。比较推荐
运行结果如下:
上面的所有的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 testEntityPoJo() {
// 请求地址
String url = "http://jsonplaceholder.typicode.com/posts";
// 要发送的数据对象
PostDTO postDTO = new PostDTO();
postDTO.setUserId(110);
postDTO.setTitle("dhy 发布文章");
postDTO.setBody("dhy 发布文章 测试内容");
// 发送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);
}
输出打印结果
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;
}
/** * @param userName * @param password * @return */
@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);
}
运行结果如下:
熟悉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。
删除一个已经存在的资源,使用RestTemplate的delete(uri)方法。该方法会向URL代表的资源发送一个HTTP DELETE方法请求。
@Test
void testDelete() {
String url = "http://jsonplaceholder.typicode.com/posts/1";
restTemplate.delete(url);
}
修改一个已经存在的资源,使用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 发布文章 测试内容");
// 发送PUT请求
restTemplate.put(url, postDTO);
}
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请求效果是一样的
//使用getForEntity发送GET请求
ResponseEntity<PostDTO> responseEntity
= restTemplate.getForEntity(url, PostDTO.class);
//使用exchange发送GET请求
ResponseEntity<PostDTO> responseEntity = restTemplate.exchange(url, HttpMethod.GET,
null, PostDTO.class);
下面的两种方式发送POST请求效果是一样的
// 使用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请求效果是一样的,只是一个有返回值,一个返回值为void
// 使用delete发送DELETE请求,返回值为void
restTemplate.delete(url);
// 使用exchange发送DELETE请求
ResponseEntity<String> result = restTemplate.exchange(url, HttpMethod.DELETE,null,String.class);
上面为大家举了几个用exchange()发送请求的例子,exchange()还能针对很多的HTTP method类型发送请求,是通用方法!
使用headForHeaders()API
获取某个资源的URI的请求头信息,并且只专注于获取HTTP请求头信息。
@Test
public void testHEAD() {
String url = "http://jsonplaceholder.typicode.com/posts/1";
HttpHeaders httpHeaders = restTemplate.headForHeaders(url);
//断言该资源接口数据为JSON类型
assertTrue(httpHeaders.getContentType()
.includes(MediaType.APPLICATION_JSON));
System.out.println(httpHeaders);
}
下文代码使用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,即增删改查
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); //服务端MultipartFile uploadFile
//param.add("param1", "test"); //服务端如果接受额外参数,可以传递
// 发送请求并输出结果
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和Path是nio里面的工具类
//Objects是jdk7以后新增的一个类,里面有很多静态方法有来简化操作
Files.write(Paths.get(targetPath), Objects.requireNonNull(rsp.getBody(),
"未获取到下载文件"));
}
这种下载方法实际上是将下载文件一次性加载到客户端本地内存,然后从内存将文件写入磁盘。这种方式对于小文件的下载还比较适合,如果文件比较大或者文件下载并发量比较大,容易造成内存的大量占用,从而降低应用的运行效率。
这种下载方式的区别在于
APPLICATION_OCTET_STREAM
,表示以流的形式进行数据加载RequestCallback
结合File.copy
保证了接收到一部分文件内容,就向磁盘写入一部分内容。而不是全部加载到内存,最后再写入磁盘文件。@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请求结果异常是可以自定义处理的。在开始进行自定义的异常处理逻辑之前,我们有必要看一下异常处理的默认实现。也就是:为什么会产生上面小节提到的现象?
ResponseErrorHandler是RestTemplate请求结果的异常处理器接口
DefaultResponseErrorHandler是ResponseErrorHandler的默认实现
所以我们就来看看DefaultResponseErrorHandler是如何来处理异常响应的?
从HttpResponse解析出Http StatusCode,如果状态码StatusCode为null,就抛出UnknownHttpStatusCodeException异常。
如果StatusCode存在,则解析出StatusCode的series,也就是状态码段(除了200段,其他全是异常状态码),解析规则是StatusCode/100取整。
public enum Series {
INFORMATIONAL(1), // 1xx/100
SUCCESSFUL(2), // 2xx/100
REDIRECTION(3), // 3xx/100
CLIENT_ERROR(4), // 4xx/100 ,客户端异常
SERVER_ERROR(5); // 5xx/100 ,服务端异常
}
进一步针对客户端异常和服务端异常进行处理,处理的方法是抛出HttpClientErrorException。也就是第一小节出现的异常的原因
所以我们要实现自定义异常,实现ResponseErrorHandler 接口就可以。
public class MyRestErrorHandler 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 将接口请求的异常信息持久化
}
}
将MyRestErrorHandler 在RestTemplate实例化的时候进行注册
这时再去执行第一小节中的示例代码,就不会抛出异常。而是得到一个HTTP Status 404的结果。我们可以根据这个结果,在程序中继续向下执行代码。
在上一节我们为大家介绍了,当RestTemplate发起远程请求异常时的自定义处理方法,我们可以通过自定义的方式解析出HTTP Status Code状态码,然后根据状态码和业务需求决定程序下一步该如何处理。
本节为大家介绍另外一种通用的异常的处理机制:那就是自动重试。也就是说,在RestTemplate发送请求得到非200状态结果的时候,间隔一定的时间再次发送n次请求。n次请求都失败之后,最后抛出HttpClientErrorException。
在开始本节代码之前,将上一节的RestTemplate自定义异常处理的代码注释掉,否则自动重试机制不会生效。如下(参考上一节代码):
//restTemplate.setErrorHandler(new MyRestErrorHandler());
通过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发起请求,结果如下:
从结果可以看出:
服务提供方通常会通过一定的授权、鉴权认证逻辑来保护API接口。其中比较简单、容易实现的方式就是使用HTTP 的Basic Auth来实现接口访问用户的认证。我们本节就来为大家介绍一下,在服务端加入Basic Auth认证的情况下,该如何使用RestTemplate访问服务端接口。
HttpBasic
模式要求传输的用户名密码使用Base64
模式进行加密。如果用户名是 "admin"
,密码是“ admin”
,则将字符串"admin:admin"
使用Base64
编码算法加密。加密结果可能是:YWtaW46YWRtaW4=
。Http
请求中使用authorization
作为一个HTTP
请求头Header name
,“Basic YWtaW46YWRtaW4=“
作为Header
的值,发送给服务端。(注意这里使用Basic+空格+加密串
)BasicAuthenticationFilter
过滤器,将提取“authorization”的Header
值,并使用用于验证用户身份的相同算法Base64
进行解码如果你想自己搭建一个服务端,那么如何为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() {
//该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);
}
}
测试用例执行成功,说明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;
}
//实现一个拦截器:使用拦截器为每一个HTTP请求添加Basic Auth认证用户名密码信息
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;
}
//这段代码是《第3节-底层HTTP客户端实现切换》的内容
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;
/** * * @param url 请求地址 * 参数可以通过 http://localhost:8888/juheServer/juhe/info/queryCustomer.do?taxNo=92330424MA29G7GY5W * 或者 http://localhost:8888/juheServer/juhe/info/queryCustomer.do+String.format("?taxNo=%s&order=%s", "92330424MA29G7GY5W","1212121212"); * @param responseType 返回值的类型 * @return * @author xiagwei * @date 2020/3/5 5:28 PM * */
public <T> T getForObject(String url, Class<T> responseType) {
return restTemplate.getForObject(url, responseType);
}
/** * 通过json的方式请求服务,不需要将数据格式化,直接将请求对象传入即可 * 可以是map,可以是一个bean * @param url 请求接口 * @param requestParam 请求实体 * @param responseType 返回对象的clazz * @return * @author xiagwei * @date 2020/3/5 5:36 PM */
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);
}
/** * 通过Form表单的方式提交 * @param url 请求接口 * @param requestParam 请求实体,可以是一个实体,也可以一个map * @param responseType 返回对象的clazz * @return * @author xiagwei * @date 2020/3/5 5:42 PM */
public <T> T postForObjectForm(String url, @NotNull Object requestParam, Class<T> responseType) {
MultiValueMap<String, Object> valueRequestMap = createValueMap(requestParam);
return restTemplate.postForObject(url, valueRequestMap, responseType);
}
/** * 最通用的请求方法 * * @param url 请求的URL * @param requestParam 请求参数 * @param headers 请求头 * @param response 响应结果的类型 * @return * @date 2021/3/10 14:21 */
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);
}
/** * 图片上传 * * @param url 请求地址 * @param body 请求体 * MultiValueMap<String, Object> body = new LinkedMultiValueMap<>(); body.add("uploadFile", new FileSystemResource(ImageUtil.downloadImgByUrl(url))); * @param responseType 返回结果的clazz对象 * @return * @author xiagwei * @date 2020/3/5 6:05 PM */
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);
}
/** * 基础的请求方法 * * @param url * @param httpEntity * @param responseType * @return * @Author weixiang * @date 2020/3/5 6:05 PM */
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请求体形式提交过去
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://blog.csdn.net/m0_53157173/article/details/121766555
内容来源于网络,如有侵权,请联系作者删除!