重学springboot系列番外篇之RestTemplate

x33g5p2x  于2021-12-14 转载在 Spring  
字(36.8k)|赞(0)|评价(0)|浏览(1452)

基本介绍及配置使用

什么是 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等)提供重载的方法,提供了一种非常方便的方法访问基于HTTPWeb服务。如果你的Web服务API基于标准的RESTful风格设计,使用效果将更加的完美

非Spring环境下使用RestTemplate

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 {

    //默认使用JDK 自带的HttpURLConnection作为底层实现
    @Bean
    public RestTemplate restTemplate(){
        RestTemplate restTemplate = new RestTemplate();
        return restTemplate;
    }
}

在需要使用RestTemplate 的位置,注入并使用即可。

@Resource //@AutoWired
  private RestTemplate restTemplate;

底层HTTP客户端库的切换

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。

切换为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;
}

设置超时时间

引入依赖之后,就来开始使用吧,任何一个HttpApi我们都可以设置请求的连接超时时间,请求超时时间,如果不设置的话,就可能会导致连接得不到释放,造成内存溢出。这个是我们需要重点注意的点,下面就来看看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();
    }
}

GET请求使用详解

RestTemplate可以发送HTTP GET请求,经常使用到的方法有两个:

  • getForObject()
  • getForEntity()

二者的主要区别在于,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);
  • 我们也可以使用 map 装载参数:

使用{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()方法小结

/** 方法一,直接将参数添加到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);
  • 不推荐直接使用方法三传入URI,原因主要有如下两点: 1. 传入的参数包含中文时必须要转码,直接传中文会报400的错误,2. 响应的结果必须要跟接口的返回值保持一致,不然回报406的错误
//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);

getForEntity()方法

上面的所有的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请求使用详解

其实POST请求方法和GET请求方法上大同小异,RestTemplate的POST请求也包含两个主要方法:

  • postForObject()
  • postForEntity()

二者的主要区别在于,postForObject()返回值是HTTP协议的响应体。postForEntity()返回的是ResponseEntity,ResponseEntity是对HTTP响应的封装,除了包含响应体,还包含HTTP状态码、contentType、contentLength、Header等信息。

postForObject发送JSON格式请求

三个重载方法

/** * @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);
   }
}
  • jsonplaceholder.typicode.com是一个可以提供在线免费RESTful测试服务的一个网站
  • ”/posts"服务接收PostDTO参数对象,并将请求结果以JSON字符串的形式进行响应。响应结果就是请求参数对象对应的JSON字符串。
  • 所以postForObject方法第二个参数是请求数据对象,第三个参数是返回值类型

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 = 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进行转换。比较推荐

运行结果如下:

postForEntity()方法

上面的所有的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() 方法的使用

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);
    }

运行结果如下:

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 发布文章 测试内容");

   // 发送PUT请求
   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请求效果是一样的

//使用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类型发送请求,是通用方法!

使用HEAD方法获取HTTP请求头数据

使用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);
}

使用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};
   //测试该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。也就是第一小节出现的异常的原因

RestTemplate自定义异常处理

所以我们要实现自定义异常,实现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());

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注解的方法在发生异常时会重试,参数说明:

  • value:当指定异常发生时会进行重试,HttpClientErrorException是RestClientException的子类。
  • include:和value一样,默认空。如果 exclude也为空时,所有异常都重试
  • exclude:指定异常不重试,默认空。如果 include也为空时,所有异常都重试
  • maxAttemps:最大重试次数,默认3
  • backoff:重试等待策略,默认空

@Backoff注解为重试等待的策略,参数说明:

  • delay:指定重试的延时时间,默认为1000毫秒
  • multiplier:指定延迟的倍数,比如设置delay=5000,multiplier=2时,第一次重试为5秒后,第二次为10(5x2)秒,第三次为20(10x2)秒。

写一个测试的RetryController 对RetryService 的testEntity方法进行调用

@RestController
public class RetryController {
 
    @Resource
    private RetryService retryService;
 
    @GetMapping("/retry")
    public HttpStatus test() {
        return retryService.testEntity();
    }
}

测试结果

向http://localhost:8080/retry发起请求,结果如下:

从结果可以看出:

  • 第一次请求失败之后,延迟5秒后重试
  • 第二次请求失败之后,延迟10秒后重试
  • 第三次请求失败之后,抛出异常

通过BasicAuth认证

服务提供方通常会通过一定的授权、鉴权认证逻辑来保护API接口。其中比较简单、容易实现的方式就是使用HTTP 的Basic Auth来实现接口访问用户的认证。我们本节就来为大家介绍一下,在服务端加入Basic Auth认证的情况下,该如何使用RestTemplate访问服务端接口。

HttpBasic认证原理说明

  • 首先,HttpBasic模式要求传输的用户名密码使用Base64模式进行加密。如果用户名是 "admin" ,密码是“ admin”,则将字符串"admin:admin"使用Base64编码算法加密。加密结果可能是:YWtaW46YWRtaW4=
  • 然后,在Http请求中使用authorization作为一个HTTP请求头Header name“Basic YWtaW46YWRtaW4=“作为Header的值,发送给服务端。(注意这里使用Basic+空格+加密串
  • 服务器在收到这样的请求时,到达BasicAuthenticationFilter过滤器,将提取“authorization”的Header值,并使用用于验证用户身份的相同算法Base64进行解码
  • 解码结果与登录验证的用户名密码匹配,匹配成功则可以继续过滤器后续的访问。

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() {
      //该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请求体形式提交过去

相关文章