一起學 Spring 之 RestTemplate

  • 2019 年 10 月 5 日
  • 筆記

前言

在 Java 服務端開發領域裡,Spring 是繞不開的話題,尤其是現在微服務概念盛行,Spring Boot 的出現更是給 Spring 注入了新的活力,除此之外還有 Spring Cloud,這些框架讓 Spring 技術體系更加豐富。Spring 從 2014 年的 1.0.0 版本迭代到 現在的 5.2.0 M1 版本,緊隨著 Java 語言發展,不斷引入新的特性和功能。

本文關注的是 Spring 框架中 RestTemplate 內容,可以減少我們平時開發常使用的 HttpClient API 依賴。文章所涉及的示例 Demo 詳見Github 地址:Resttemplate demo 。感謝小七同學的認真校對,如果有任何技術問題或者文章紕漏,歡迎留言聯繫,一起交流探討 ?。

認識 RestTemplate

首先在我們學習使用 RestTemplate 之前,先認識下這個類,來看 Spring 官方怎麼描述的。 從官方 API 文檔 RestTemplate javadoc 可以找該類的描述如下:

Synchronous client to perform HTTP requests, exposing a simple, template method API over underlying HTTP client libraries such as the JDK HttpURLConnection, Apache HttpComponents, and others. The RestTemplate offers templates for common scenarios by HTTP method, in addition to the generalized exchange and execute methods that support of less frequent cases.

從這裡可以清楚地了解到 RestTemplate 採用同步方式執行 HTTP 請求的類,底層使用 JDK 原生 HttpURLConnection API ,或者 HttpComponents等其他 HTTP 客戶端請求類庫。還有一處強調的就是 RestTemplate 提供模板化的方法讓開發者能更簡單地發送 HTTP 請求。

值得注意的是, RestTemplate 類是在 Spring Framework 3.0 開始引入的,這裡我們使用的 Spring 版本為當前最新的 GA 版本 5.1.6。而在 5.0 以上,官方標註了更推薦使用非阻塞的響應式 HTTP 請求處理類 org.springframework.web.reactive.client.WebClient 來替代 RestTemplate,尤其是對應非同步請求處理的場景上 。

這裡我們先簡單總結下什麼是 RestTemplateRestTemplate 就是 Spring 封裝的處理同步 HTTP 請求的類。具體如何使用這個類進行 HTTP 請求操作,可見文章的實戰部分。

接下來我們看下 RestTemplate 類提供的 API 有哪些, RestTemplate 提供了將近 30 個請求方法,其中多數是單個方法重載實現,這裡我主要參考官方文檔 rest-client-access 進行如下分類:

看到那麼多方法也記不全,為了更好理解,可以簡單看下 RestTemplate 的類層級體系,通過官方源程式碼就能看到:

/**   * Interface specifying a basic set of RESTful operations.   * Implemented by {@link RestTemplate}. Not often used directly, but a useful   * option to enhance testability, as it can easily be mocked or stubbed.   *   * @author Arjen Poutsma   * @author Juergen Hoeller   * @since 3.0   * @see RestTemplate   */  public interface RestOperations {      ...  }

其實 RestTemplate 類的請求方法都是來自 RestOperations 介面的,根據這個名字就可以大概知道這個介面主要就是提供了 RESTful 請求操作的介面,如 GET,POST,PUT,DELETE 等,具體資訊可以參見 RestOperation javadoc。

關於 RESTful: 來自Wikipedia 定義:表現層狀態轉換,一種設計提供萬維網路服務的軟體構建風格,又簡稱為 REST。 用 URL 定位資源,用 HTTP 動詞描述操作,如 GET,POST,DELETE,PUT,簡單來說通過 URL 就能知道訪問什麼資源,通過 HTTP Method 就知道執行什麼操作,通過 HTTP Status Code 就能知道執行結果。

實戰 RestTemplate

好了,簡單認識了 RestTemplate 類之後,我們先牛刀小試,看看如何上手使用。

1. 生成 Demo 項目,導入 IDE

為了能快速搭建一個 Demo,我們這邊用 Spring Boot 框架搭建,首先用官方提供的 Spring Initializr 來生成快速構建項目骨架,選擇 Spring Boot 版本 2.1.4,其底層依賴的 Spring Framework 版本為最新發布版本 5.1.6,對於 POM 依賴只選擇一個 Web 模組即可,這樣便於快速搭建 Web 應用。

點擊生成工程按鈕,就可以下載到項目的壓縮包,解壓後用自己常用的 IDE 導入項目,項目結構整理如下:

項目中 ResttemplateApplication.java 為整個程式的引導類,用於啟動項目。

2. 編寫請求控制器類 ProductController

首先為了能夠使用 RestTemplate 發送多種方式 HTTP 請求,先本地構建接受 HTTP 請求的產品控制器,新建包 com.one.learn.resttemplate.controller,新建產品 Controller ProductController , 程式碼如下:

@RequestMapping("/product")  @RestController  public class ProductController {        @GetMapping("/get_product1")      public Product get_product1() {          return new Product(1, "ProductA", BigDecimal.valueOf(6666.0));      }        @GetMapping("/get_product2")      public Product get_product2(Integer id) {          return new Product(id, "ProductC", BigDecimal.valueOf(6666.0));      }        @GetMapping("/get_product3")      public String get_product3(Product product) {          return product.toString();      }          @PostMapping("/post_product1")      public String post_product1(Product product) {          return product.toString();      }        @PostMapping("/post_product2")      public String post_product2(@RequestBody Product product) {          return product.toString();      }        @DeleteMapping("/delete/{id}")      public String delete(@PathVariable Integer id) {          String result = String.format("編號為%s的產品刪除成功", id);          System.out.println(result);          return result;      }        @PutMapping("/update")      public String updateByPut(Product product) {          String result = product.toString() + " 更新成功";          System.out.println(result);          return result;      }        @PostMapping("/upload")      public String upload(MultipartRequest request) {          // Spring MVC 使用 MultipartRequest 接受帶文件的 HTTP 請求          MultipartFile file = request.getFile("file");          String originalFilename = file.getOriginalFilename();          return "upload success filename: " + originalFilename;      }  }

Product 控制器中涉及的實體類 Product 創建在 com.one.learn.resttemplate.bean 包下,程式碼如下:

public class Product {      private Integer id;      private String name;      private BigDecimal price;        public Product() {      }        public Product(Integer id, String name, BigDecimal price) {          this.id = id;          this.name = name;          this.price = price;      }          // 省去 setter getter 方法      @Override      public String toString() {          return "Product{" +                  "id='" + id + ''' +                  ", name='" + name + ''' +                  ", price='" + price + ''' +                  '}';      }  }

有了這些類,就可以利用程式引導類 ResttemplateApplication 啟動 Spring Boot 項目,一個簡單的 Web 應用就誕生了,監聽 8080 埠,結果如下圖所示:

我們可以簡單測試一下,打開瀏覽器,訪問 http://localhost:8080/product/get_product1,會看到如圖所示的結果:

3. 編寫測試類用 RestTemplate 發送 HTTP 請求

有了 Web 服務,接下來該使用 RestTemplate 來發送請求並處理響應了。我們在 test 文件下新建一個測試類 com.one.learn.resttemplate.RestTemplateTests,程式碼如下:

public class RestTemplateTests {      RestTemplate restTemplate = null;        @Before      public void setup() {          restTemplate = new RestTemplate();      }    }

這裡我們通過編寫測試方法來用 RestTemplate API 實現對 Product 控制器各個介面的請求。

GET 請求

我們先最簡單的下手,嘗試用 RestTemplate 訪問請求路徑為 product/get_product1, 一個不帶任何參數 的 GET 請求,程式碼如下:

@Test  public void testGet_product1() {     String url = "http://localhost:8080/product/get_product1";     //方式一:GET 方式獲取 JSON 串數據     String result = restTemplate.getForObject(url, String.class);     System.out.println("get_product1返回結果:" + result);     Assert.hasText(result, "get_product1返回結果為空");         //方式二:GET 方式獲取 JSON 數據映射後的 Product 實體對象   Product product = restTemplate.getForObject(url, Product.class);     System.out.println("get_product1返回結果:" + product);     Assert.notNull(product, "get_product1返回結果為空");   //方式三:GET 方式獲取包含 Product 實體對象 的響應實體 ResponseEntity 對象,用 getBody() 獲取     ResponseEntity<Product> responseEntity = restTemplate.getForEntity(url, Product.class);     System.out.println("get_product1返回結果:" + responseEntity);     Assert.isTrue(responseEntity.getStatusCode().equals(HttpStatus.OK), "get_product1響應不成功");    }

首先看下運行測試方法 testGet_product1 後控制台的輸出日誌:

...  get_product1返回結果:{"id":1,"name":"ProductA","price":6666.0}  ...  get_product1返回結果:Product{id='1', name='ProductA', price='6666.0'}  ...  get_product1返回結果:<200,Product{id='1', name='ProductA', price='6666.0'},[Content-Type:"application/json;charset=UTF-8", Transfer-Encoding:"chunked", Date:"Thu, 09 May 2019 15:37:25 GMT"]>  ...

可以看到 testGet_product1 請求都是成功響應並獲取到了數據,從上面程式碼上看是不是很簡單。現在來點略複雜的請求方式,使用 RestTemplate API 中 exchangeexecute 方法發送 GET 請求,可以更加細粒度控制請求的行為,如 Header 資訊,數據處理方式等,同樣在 testGet_product1 方法里添加程式碼如下:

@Test  public void testGet_product1() {      String url = "http://localhost:8080/product/get_product1";      //....        //方式一: 構建請求實體 HttpEntity 對象,用於配置 Header 資訊和請求參數      MultiValueMap header = new LinkedMultiValueMap();      header.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);      HttpEntity<Object> requestEntity = new HttpEntity<>(header);      //方式二: 執行請求獲取包含 Product 實體對象 的響應實體 ResponseEntity 對象,用 getBody() 獲取      ResponseEntity<Product> exchangeResult = restTemplate.exchange(url, HttpMethod.GET, requestEntity, Product.class);      System.out.println("get_product1返回結果:" + exchangeResult);      Assert.isTrue(exchangeResult.getStatusCode().equals(HttpStatus.OK), "get_product1響應不成功");        //方式三: 根據 RequestCallback 介面實現類設置Header資訊,用 ResponseExtractor 介面實現類讀取響應數據      String executeResult = restTemplate.execute(url, HttpMethod.GET, request -> {          request.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);      }, (clientHttpResponse) -> {          InputStream body = clientHttpResponse.getBody();          byte[] bytes = new byte[body.available()];          body.read(bytes);          return new String(bytes);      }); // 備註:這裡使用了 Java8 特性:Lambda 表達式語法,若未接觸 Lambda 表達式後可以使用匿名內部類代替實現      System.out.println("get_product1返回結果:" + executeResult);      Assert.hasText(executeResult, "get_product1返回結果為空");  }

同樣再運行測試方法 testGet_product1 後控制台的輸出日誌:

...  get_product1返回結果:<200,Product{id='1', name='ProductA', price='6666.0'},[Content-Type:"application/json;charset=UTF-8", Transfer-Encoding:"chunked", Date:"Thu, 09 May 2019 16:00:22 GMT"]>  ...  get_product1返回結果:{"id":1,"name":"ProductA","price":6666.0}  ...

結果也都是正常返回,說明執行的請求都是正確的。

現在來嘗試執行帶有參數的 GET 請求,同樣的方式編寫一個新的測試方法,實現程式碼如下:

@Test  public void testGet_product2() {      String url = "http://localhost:8080/product/get_product2/id={id}";          //方式一:將參數的值存在可變長度參數里,按照順序進行參數匹配    ResponseEntity<Product> responseEntity = restTemplate.getForEntity(url, Product.class, 101);      System.out.println(responseEntity);      Assert.isTrue(responseEntity.getStatusCode().equals(HttpStatus.OK), "get_product2 請求不成功");      Assert.notNull(responseEntity.getBody().getId(), "get_product2  傳遞參數不成功");          //方式二:將請求參數以鍵值對形式存儲到 Map 集合中,用於請求時URL上的拼接    Map<String, Object> uriVariables = new HashMap<>();      uriVariables.put("id", 101);      Product result = restTemplate.getForObject(url, Product.class, uriVariables);      System.out.println(result);      Assert.notNull(result.getId(), "get_product2  傳遞參數不成功");  }

正常運行結果如下:

...  <200,Product{id='101', name='ProductC', price='6666.0'},[Content-Type:"application/json;charset=UTF-8", Transfer-Encoding:"chunked", Date:"Fri, 10 May 2019 14:53:41 GMT"]>  ...  Product{id='101', name='ProductC', price='6666.0'}  ...

POST 請求

了解完如何用 RestTemplate API 發送 GET 請求後,再看下平時也很常見的 POST 請求如何使用。由於 POST 請求數據的內容類型 Content-Type 不同,發送 POST 請求情況相對就多了,我們這裡以常用的 application/x-www-form-urlencodedapplication/json 這兩種內容類型為例子。

  • 發送 Content-Typeapplication/x-www-form-urlencoded 的 POST 請求
@Test  public void testPost_product1() {      String url = "http://localhost:8080/product/post_product1";      Product product = new Product(201, "Macbook", BigDecimal.valueOf(10000));      // 設置請求的 Content-Type 為 application/x-www-form-urlencoded      MultiValueMap<String, String> header = new LinkedMultiValueMap();      header.add(HttpHeaders.CONTENT_TYPE, (MediaType.APPLICATION_FORM_URLENCODED_VALUE));        //方式二: 將請求參數值以 K=V 方式用 & 拼接,發送請求使用      String productStr = "id=" + product.getId() + "&name=" + product.getName() + "&price=" + product.getPrice();      HttpEntity<String> request = new HttpEntity<>(productStr, header);      ResponseEntity<String> exchangeResult = restTemplate.exchange(url, HttpMethod.POST, request, String.class);      System.out.println("post_product1: " + exchangeResult);      Assert.isTrue(exchangeResult.getStatusCode().equals(HttpStatus.OK), "post_product1 請求不成功");        //方式一: 將請求參數以鍵值對形式存儲在 MultiValueMap 集合,發送請求時使用      MultiValueMap<String, Object> map = new LinkedMultiValueMap();      map.add("id", (product.getId()));      map.add("name", (product.getName()));      map.add("price", (product.getPrice()));      HttpEntity<MultiValueMap> request2 = new HttpEntity<>(map, header);      ResponseEntity<String> exchangeResult2 = restTemplate.exchange(url, HttpMethod.POST, request2, String.class);      System.out.println("post_product1: " + exchangeResult2);      Assert.isTrue(exchangeResult.getStatusCode().equals(HttpStatus.OK), "post_product1 請求不成功");  }

對應的輸出日誌如下:

...  <200,Product{id='101', name='ProductC', price='6666.0'},[Content-Type:"application/json;charset=UTF-8", Transfer-Encoding:"chunked", Date:"Fri, 10 May 2019 14:53:41 GMT"]>  ...  Product{id='101', name='ProductC', price='6666.0'}  ...
  • 發送 Content-Typeapplication/json 的 POST 請求
@Test  public void testPost_product1() {      String url = "http://localhost:8080/product/post_product1";      Product product = new Product(201, "Macbook", BigDecimal.valueOf(10000));      // 設置請求的 Content-Type 為 application/x-www-form-urlencoded      MultiValueMap<String, String> header = new LinkedMultiValueMap();      header.add(HttpHeaders.CONTENT_TYPE, (MediaType.APPLICATION_FORM_URLENCODED_VALUE));        //方式一: 將請求參數值以 K=V 方式用 & 拼接,發送請求使用      String productStr = "id=" + product.getId() + "&name=" + product.getName() + "&price=" + product.getPrice();      HttpEntity<String> request = new HttpEntity<>(productStr, header);      ResponseEntity<String> exchangeResult = restTemplate.exchange(url, HttpMethod.POST, request, String.class);      System.out.println("post_product1: " + exchangeResult);      Assert.isTrue(exchangeResult.getStatusCode().equals(HttpStatus.OK), "post_product1 請求不成功");        //方式二: 將請求參數以鍵值對形式存儲在 MultiValueMap 集合,發送請求時使用      MultiValueMap<String, Object> map = new LinkedMultiValueMap();      map.add("id", (product.getId()));      map.add("name", (product.getName()));      map.add("price", (product.getPrice()));      HttpEntity<MultiValueMap> request2 = new HttpEntity<>(map, header);      ResponseEntity<String> exchangeResult2 = restTemplate.exchange(url, HttpMethod.POST, request2, String.class);      System.out.println("post_product1: " + exchangeResult2);      Assert.isTrue(exchangeResult.getStatusCode().equals(HttpStatus.OK), "post_product1 請求不成功");  }

對應的輸出日誌如下:

...  post_product1: <200,Product{id='201', name='Macbook', price='10000'},[Content-Type:"text/plain;charset=UTF-8", Content-Length:"48", Date:"Fri, 10 May 2019 16:07:43 GMT"]>  ...  post_product1: <200,Product{id='201', name='Macbook', price='10000'},[Content-Type:"text/plain;charset=UTF-8", Content-Length:"48", Date:"Fri, 10 May 2019 16:07:43 GMT"]>

DELETE 請求 和 PUT 請求

DELETE 請求和 PUT 請求屬於 RESTful 請求方式的兩種,但通常不會被使用到,這裡也只是簡單演示下,具體程式碼如下:

// DELETE 方法請求  @Test  public void testDelete() {     String url = "http://localhost:8080/product/delete/{id}";     restTemplate.delete(url, 101);  }    // PUT 方法請求  @Test  public void testPut() {      String url = "http://localhost:8080/product/update";      Map<String, ?> variables = new HashMap<>();      MultiValueMap<String, String> header = new LinkedMultiValueMap();      header.put(HttpHeaders.CONTENT_TYPE, Arrays.asList(MediaType.APPLICATION_FORM_URLENCODED_VALUE));      Product product = new Product(101, "iWatch", BigDecimal.valueOf(2333));      String productStr = "id=" + product.getId() + "&name=" + product.getName() + "&price=" + product.getPrice();      HttpEntity<String> request = new HttpEntity<>(productStr, header);      restTemplate.put(url, request);  }

上傳文件

現在我們再試下如何使用 RestTemplate API 進行文件上傳,也比較簡單,首先看下實現程式碼:

@Test  public void testUploadFile() {      String url = "http://localhost:8080/product/upload";      MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();      FileSystemResource file = new FileSystemResource(new File("/Users/One/Desktop/b.txt"));      body.add("file", file);        MultiValueMap<String, String> header = new LinkedMultiValueMap();      header.put(HttpHeaders.CONTENT_TYPE, Arrays.asList(MediaType.MULTIPART_FORM_DATA_VALUE));      HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, header);      ResponseEntity<String> responseEntity = restTemplate.postForEntity(url, requestEntity, String.class);      System.out.println("upload: " + responseEntity);      Assert.isTrue(responseEntity.getStatusCode().equals(HttpStatus.OK), "upload 請求不成功");  }

如果需要上傳文件類型數據,就只能使用 POST 請求,並且內容類型為 multipart/form-data,需要手動給 Header 指定這個 Content-Type。而需要上傳的文件可以用 FileSystemResource 對象封裝,表示了一個文件資源,同時服務端需要用 MultipartRequest 對象來獲取文件數據。結合已運行的 Web 服務,運行上述測試方法即可得到下面日誌輸出:

...  upload: <200,upload success filename: b.txt,[Content-Type:"text/plain;charset=UTF-8", Content-Length:"30", Date:"Fri, 10 May 2019 17:00:45 GMT"]>  ...

進階 RestTemplate

到這裡我們就學習了 RestTemplate API 請求數據的幾種常見方式,現在來進一步地深入使用 RestTemplate

底層 HTTP 請求庫切換

我們首先看下官方文檔的描述:

The default constructor uses java.net.HttpURLConnection to perform requests. You can switch to a different HTTP library with an implementation of ClientHttpRequestFactory. There is built-in support for the following:

  • Apache HttpComponents
  • Netty
  • OkHttp

從上面可以看出 RestTemplate 默認使用 JDK 原生的 java.net.HttpURLConnection 執行請求。而除此之外,Spring 還封裝了 Apache HttpComponents, Netty, OkHttp 三種請求庫,第一個就是我們平常用的 HttpClient API 相關的庫,而 Netty 則是一個性能高的NIO 請求處理網路庫,OkHttp 為功能豐富且高效的網路框架,多用於 Android 程式。

而我們上文採用默認的構造器方法創建的 RestTemplate 實例,即採用了 JDK 原生的網路 API。想要切換,只需要在構造方法中傳入特定 ClientHttpRequestFactory 實現類即可,如下程式碼:

RestTemplate template = new RestTemplate(new HttpComponentsClientHttpRequestFactory());

我們查看 RestTemplate 源碼找不到默認採用JDK HttpURLConnection API 的程式碼,那就根據前文給出 RestTemplate 類層次圖向上查找,可以在父類 HttpAccessor 上能找到如下程式碼:

public abstract class HttpAccessor {      // ...      private ClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();      // ...  }

而 Spring 對工廠類 SimpleClientHttpRequestFactory 描述為: implementation that uses standard JDK facilities, 也正說明了默認構造 RestTemplate 實例的行為都會直接使用 JDK 網路 API。

請求超時設置

通常我們會對 HTTP 請求類進行執行行為的訂製,例如調用超時時間設置,連接時長的限制等,而採用默認的 HttpURLConnection 默認的配置時, 從 SimpleClientHttpRequestFactory 源碼類可以看到是沒有超時限制,也就意味著無限等待請求響應:

// RestTemplate 默認超時設置  ...  private int connectTimeout = -1;  private int readTimeout = -1;  ...

那麼我們該如何調整超時時間,可以參考如下程式碼:

RestTemplate customRestTemplate = new RestTemplate(getClientHttpRequestFactory());    private SimpleClientHttpRequestFactory getClientHttpRequestFactory() {      SimpleClientHttpRequestFactory clientHttpRequestFactory              = new SimpleClientHttpRequestFactory();      // 連接超時設置 10s      clientHttpRequestFactory.setConnectTimeout(10_000);        // 讀取超時設置 10s      clientHttpRequestFactory.setReadTimeout(10_000);      return clientHttpRequestFactory;  }

如果要調整 HttpComponentsClient 的超時設置,可以參考文章resttemplate-timeout-example 。當然除了設置超時時間之外,還有更多參數進行訂製,這裡就不一一列舉,可以參考文章 resttemplate-httpclient-java-config 進一步學習。

到這裡我們對 RestTemplate 的學習告一段落,如果有興趣可以進一步研究下相關源碼,有機會嘗試使起來吧。?

參考資料

The Guide to RestTemplate

http://blog.didispace.com/spring-boot-learning-21-1-1

https://www.baeldung.com/spring-rest-template-multipart-upload

https://www.zhihu.com/question/28557115

https://howtodoinjava.com/spring-boot2/resttemplate-timeout-example

https://docs.spring.io/spring/docs/5.1.6.RELEASE/spring-framework-reference/integration.html#rest-client-access

https://zh.wikipedia.org/wiki/%E8%A1%A8%E7%8E%B0%E5%B1%82%E7%8A%B6%E6%80%81%E8%BD%AC%E6%8D%A2

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/client/RestOperations.html