Spring之RestTemplate中級使用篇

一灰灰發表於2018-08-15

logo

Spring之RestTemplate中級使用篇

前面一篇介紹瞭如何使用RestTemplate發起post和get請求,然而也只能滿足一些基本的場景,對於一些特殊的如需要設定請求頭,新增認證資訊等場景,卻沒有提及可以怎麼做,這一篇則相當於進階版,將主要介紹

  • get/post請求如何攜帶 header
  • post傳檔案可以怎麼玩, post提交json串怎麼處理
  • exchange方法的使用姿勢

I. 請求頭設定

首先一個問題就是為什麼要設定請求頭?

我們通過瀏覽器正常訪問的介面,可能通過程式碼直接訪問時,就會提示403

而這樣的原因,較多的一個可能就是後端的請求做了限制,比如根據請求的agent,判斷是否為爬蟲;根據referer判斷是否要返回資料等等;而後端進行校驗的條件中,往往會拿請求頭的資料,因此這也就要求我們在使用時,主動的塞入一些請求頭資訊

1. Get請求

直接看RestTemplate提供的幾個Get請求介面,並沒有發現有設定請求頭的地方,是不是就表明沒法設定請求頭了?

答案檔案是能設定了,具體的使用思路有點類似mvc中的攔截器,自定義一個攔截器,然後在你實際發起請求時,攔截並設定request的請求頭

注意到 RestTemplate 的父類InterceptingHttpAccessor提供了一個接收Interceptor的介面org.springframework.http.client.support.InterceptingHttpAccessor#setInterceptors,這個就是我們所需要的關鍵點(講道理,除非事先就知道有這麼個玩意,不然真讓你去找,還不一定能找到)

所以第一步就是寫一個ClientHttpRequestInterceptor類,新增請求頭

public class UserAgentInterceptor implements ClientHttpRequestInterceptor {
    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
            throws IOException {
        HttpHeaders headers = request.getHeaders();
        headers.add(HttpHeaders.USER_AGENT,
                "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36");
        return execution.execute(request, body);
    }
}
複製程式碼

下一步就是在建立RestTemplate物件之後,宣告直譯器並測試使用了

@Test
public void testGetHeader() {
    String url = "http://localhost:8080/agent?name=一灰灰Blog";
    RestTemplate restTemplate = new RestTemplate();
    restTemplate.setInterceptors(Collections.singletonList(new UserAgentInterceptor()));
    ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
    System.out.println(response.getStatusCode() + " | " + response.getBody());
}
複製程式碼

首先在測試之前,先搭一個服務,簡單判斷agent,不滿足條件的直接403, 後端mock程式碼如下

@ResponseBody
@RequestMapping(path = "agent")
public String agent(HttpServletRequest request, HttpServletResponse response,
        @RequestParam(value = "name", required = false) String name) throws IOException {
    String agent = request.getHeader(HttpHeaders.USER_AGENT);
    if (StringUtils.isEmpty(agent) || !agent.contains("WebKit")) {
        response.sendError(403, " illegal agent ");
    }
    return "welcome " + name;
}
複製程式碼

上面執行後輸出如下,新增請求頭後正常返回

C420EE9FB481154F53D442684F8A7B9A.jpg

當然也需要對比下不設定agent的情況了,直接拋了一個異常出來了(說明,不顯示覆蓋User-Agent時,後端接收到的agent為: Java/1.8.0_171

CA4B422728A6868A4366F7FE65F22BE6.jpg

上面雖然只給了設定User-Agent的例子,但是其他的請求頭,都是可以放在自定義的Interceptor中新增進去的

2. Post請求

當然get請求使用的這種姿勢,對於post而言或者對於其他的http請求方法而言,都是通用的,而對於post請求來說,還有另外一種方式,就是requset引數,可以攜帶request headers

首先mock一個後端介面

@ResponseBody
@RequestMapping(path = "post", method = {RequestMethod.GET, RequestMethod.OPTIONS, RequestMethod.POST},
        produces = "charset/utf8")
public String post(HttpServletRequest request, HttpServletResponse response,
        @RequestParam(value = "email", required = false) String email,
        @RequestParam(value = "nick", required = false) String nick) throws IOException {
    String agent = request.getHeader(HttpHeaders.USER_AGENT);
    if (StringUtils.isEmpty(agent) || !agent.contains("WebKit")) {
        response.sendError(403, " illegal agent ");
        return null;
    }
    return "success email=" + email + "&nick=" + URLEncoder.encode(nick, "UTF-8") + "&status=success";
}
複製程式碼

簡單的使用姿勢如下

@Test
public void testPostHeader() {
    String url = "http://localhost:8080/post";
    String email = "test@hhui.top";
    String nick = "一灰灰Blog";

    MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
    params.add("email", email);
    params.add("nick", nick);

    HttpHeaders headers = new HttpHeaders();
    headers.add(HttpHeaders.USER_AGENT, "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " +
            "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36");
    HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(params, headers);

    RestTemplate restTemplate = new RestTemplate();
    ResponseEntity<String> response = restTemplate.postForEntity(url, request, String.class);
    System.out.println(response.getStatusCode() + " | " + response.getBody());
}
複製程式碼

從上面程式碼可以看出,具體的使用姿勢相比於不新增請求頭時,只是多了一個封裝

  • 具體的header資訊分裝到 HttpHeaders 物件中
  • 請求引數依然封裝到 MultiValueMap
  • 然後根據請求頭 + 請求引數,構建 HttpEntity 物件,將這個作為post的請求request引數傳入

2D203A36995BE818CECBD936F535875F.jpg

當然作為對比,當不加入headers時,看下返回什麼鬼, 406異常,但是我們後端定義的是403,為什麼會返回406呢?

B6C5FDF670E826D8040F6B5EDBB21F30.jpg

3. exchange 方式

另外還會關注到RestTemplate還提供了一個exchange方法,這個相當於一個公共的請求模板,使用姿勢和get/post沒有什麼區別,只是可以由呼叫發自己來選擇具體的請求方法

使用exchange對上面的post請求進行簡單的替換如下, 基本上除了多一個引數之外沒有什麼區別了

@Test
public void testPostHeader() {
    String url = "http://localhost:8080/post";
    String email = "test@hhui.top";
    String nick = "一灰灰Blog";

    MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
    params.add("email", email);
    params.add("nick", nick);

    HttpHeaders headers = new HttpHeaders();
    headers.add(HttpHeaders.USER_AGENT, "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " +
            "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36");
    HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(params, headers);

    RestTemplate restTemplate = new RestTemplate();
    ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, request, String.class);
    System.out.println(response.getStatusCode() + " | " + response.getBody());
}
複製程式碼

那麼問題來了,為什麼要有這個東西?或者說這個介面的提供可以帶來什麼好處?

  • 當你寫一個公共的Rest工具類時,就比較方便了,底層統一,具體的方法由上層業務方選擇即可
  • get可以通過這種方式直接新增請求頭(也就是不需要第一種case中的自定義攔截器來塞入header,顯然更加靈活)

II. Post引數提交

前面的post引數提交,其實預設採用的是 application/x-www-form-urlencoded 方式,即是我們最常見的表單提交方式,在瀏覽器中的表現形式如下

[站外圖片上傳中...(image-74df6f-1534334982345)]

此外,還有一種直接提交json串的方式,在前文 《180730-Spring之RequestBody的使用姿勢小結》中有說明,具體瀏覽器中表現形式為

[站外圖片上傳中...(image-92a91d-1534334982346)]

所以接下來的問題就是,RestTemplate要怎麼處理呢?

1. json串提交

建議在看下面的內容之前,先看一下上面的那篇博文,理解下RequestBody是什麼東西

首先搭建一個後端

@Data
@NoArgsConstructor
@AllArgsConstructor
public static class Req {
    private String key;
    private Integer size;
}

@ResponseBody
@RequestMapping(value = "/body", method = {RequestMethod.POST, RequestMethod.GET, RequestMethod.OPTIONS})
public String body(@RequestBody Req req) {
    return req.toString();
}
複製程式碼

然後使用方式,無非就是在請求頭中新增下Content-Type為Application/json

@Test
public void testPostRequestBody() {
    String url = "http://localhost:8080/body";
    String email = "test@hhui.top";
    String nick = "一灰灰Blog";

    Map<String, String> params = new HashMap<>();
    params.put("email", email);
    params.put("nick", nick);

    HttpHeaders headers = new HttpHeaders();
    headers.add(HttpHeaders.CONTENT_TYPE, "application/json");
    HttpEntity<Map<String, String>> request = new HttpEntity<>(params, headers);

    RestTemplate restTemplate = new RestTemplate();
    ResponseEntity<String> response = restTemplate.postForEntity(url, request, String.class);
    System.out.println(response.getStatusCode() + " | " + response.getBody());
}
複製程式碼

注意下post引數,是放在Map容器中,而不是之前的MultiValueMap

執行時截圖如下

D5E1ABAFBD02418EE63B38C5061685FF.jpg

2. 檔案上傳

post除了傳表單資料(json串)之外,還有一個常見的就是上傳檔案了,實際上使用RestTemplate來實現檔案上傳,算是比較簡單的了,和前面的使用基本上也沒有什麼差別,只是將檔案作為params引數而已

首先搭建一個Controller後端服務,簡單的獲取檔案內容,並返回

@ResponseBody
@PostMapping(path = "file")
public String file(MultipartHttpServletRequest request) throws IOException {
    MultipartFile file = request.getFile("file");
    if (file == null) {
        return "no file!";
    }

    BufferedReader reader = new BufferedReader(new InputStreamReader(file.getInputStream()));
    StringBuilder builder = new StringBuilder();
    String line = reader.readLine();
    while (line  != null) {
        builder.append(line);
        line = reader.readLine();
    }
    reader.close();
    return builder.toString();
}
複製程式碼

然後就是實際的測試用例,將檔案包裝在FileSystemResource物件中,然後塞入MultiValueMap中,注意下面的使用並沒有顯示新增請求頭,而這種時候,content-type 通過斷點檢視實際為 content-type = multipart/form-data;

@Test
public void testPostFile() {
    String url = "http://localhost:8080/file";
    FileSystemResource resource = new FileSystemResource(new File("/tmp/test.txt"));
    MultiValueMap<String, Object> params = new LinkedMultiValueMap<>();
    params.add("file", resource);
    params.add("fileName", "test.txt");

    RestTemplate restTemplate = new RestTemplate();
    ResponseEntity<String> response = restTemplate.postForEntity(url, params, String.class);
    System.out.println(response);
}
複製程式碼

14D0A3473CDF5DE85CB27FBD4DB3277F.jpg

III. 小結

本篇主要介紹如何給RestTemplate發起的請求,新增請求頭,以及完成某些特定的請求,下面小結一下使用姿勢

1. 設定header

兩種方式

  • 一個是設定Interceptors,在攔截器中主動新增上對應的請求頭即可,適用於為所有的請求新增統一的請求頭的場景
    • 這種方式不僅僅能用來設定請求頭,還可以在其中做很多其他的事情
  • 另外一種方式針對 postForXXXexchange 兩種請求方式而言,同樣自己設定請求頭HttpHeader,然後將請求頭和params封裝到HttpEntity,作為request引數提交即可

2. 特殊的請求方式

json串的提交

  • 設定請求頭的content-type為 Applicaiton/json
  • 將請求的資料封裝到map容器內(或者直接定義一個請求引數的DTO物件也可以)
  • 然後將header和引數封裝到 HttpEntity 中,發起請求即可

檔案上傳

  • 將資原始檔塞入到MultiValueMap中即可,和普通的請求方式沒有什麼區別

3. 其他

初級篇介紹瞭如何使用RestTemplate發起簡單的GET/POST請求;

中級篇則介紹請求的過程中新增設定請求頭,以及某些特殊的請求可以怎麼處理

顯然還會有高階篇,除了上面的東西,我們還需要知道些什麼呢?

  • 請求超時的設定比較實用,有必要了解下
  • 在訪問某些特殊的網站時,代理的設定也避不開
  • 請求有身份鑑權的情況下,如何安全的攜帶自己的身份呢?
  • RestTemplate底層使用的是什麼網路庫做的網路訪問?可以用其他的進行替換麼?(答案肯定是可以,不然這個命名就標準的名不副實了)

關於高階篇,坐等更新

IV. 其他

0. 相關博文

1. 一灰灰Blogliuyueyi.github.io/hexblog

一灰灰的個人部落格,記錄所有學習和工作中的博文,歡迎大家前去逛逛

2. 宣告

盡信書則不如,已上內容,純屬一家之言,因個人能力有限,難免有疏漏和錯誤之處,如發現bug或者有更好的建議,歡迎批評指正,不吝感激

3. 掃描關注

小灰灰Blog&公眾號

QrCode

知識星球

zhishi

相關文章