天天看點

真不是吹,Spring 裡這款牛逼的網絡工具庫你可能沒用過

作者:馬士兵教育

一、簡介

現如今的 IT 項目,由服務端向外發起網絡請求的場景,基本上處處可見!

傳統情況下,在服務端代碼裡通路 http 服務時,我們一般會使用 JDK 的 HttpURLConnection 或者 Apache 的 HttpClient,不過這種方法使用起來太過繁瑣,而且 api 使用起來非常的複雜,還得操心資源回收。

以下載下傳檔案為例,通過 Apache 的 HttpClient方式進行下載下傳檔案,下面這個是我之前封裝的代碼邏輯,看看有多複雜!

真不是吹,Spring 裡這款牛逼的網絡工具庫你可能沒用過

其實Spring已經為我們提供了一種簡單便捷的模闆類來進行操作,它就是RestTemplate。

RestTemplate是一個執行HTTP請求的同步阻塞式工具類,它僅僅隻是在 HTTP 用戶端庫(例如 JDK HttpURLConnection,Apache HttpComponents,okHttp 等)基礎上,封裝了更加簡單易用的模闆方法 API,友善程式員利用已提供的模闆方法發起網絡請求和處理,能很大程度上提升我們的開發效率。

好了,不多 BB 了,代碼撸起來!

二、環境配置

2.1、非 Spring 環境下使用 RestTemplate

如果目前項目不是Spring項目,加入spring-web包,即可引入RestTemplate類

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-web</artifactId>
  <version>5.2.6.RELEASE</version>
</dependency>
           

編寫一個單元測試類,使用RestTemplate發送一個GET請求,看看程式運作是否正常

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

2.2、Spring 環境下使用 RestTemplate

如果目前項目是SpringBoot,添加如下依賴接口!

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
           

同時,将RestTemplate配置初始化為一個Bean。

@Configuration
public class RestTemplateConfig {

    /**
     * 沒有執行個體化RestTemplate時,初始化RestTemplate
     * @return
     */
    @ConditionalOnMissingBean(RestTemplate.class)
    @Bean
    public RestTemplate restTemplate(){
        RestTemplate restTemplate = new RestTemplate();
        return restTemplate;
    }
}

           

注意,這種初始化方法,是使用了JDK自帶的HttpURLConnection作為底層HTTP用戶端實作。

當然,我們還可以修改RestTemplate預設的用戶端,例如将其改成HttpClient用戶端,方式如下:

@Configuration
public class RestTemplateConfig {


    /**
     * 沒有執行個體化RestTemplate時,初始化RestTemplate
     * @return
     */
    @ConditionalOnMissingBean(RestTemplate.class)
    @Bean
    public RestTemplate restTemplate(){
        RestTemplate restTemplate = new RestTemplate(getClientHttpRequestFactory());
        return restTemplate;
    }

    /**
     * 使用HttpClient作為底層用戶端
     * @return
     */
    private ClientHttpRequestFactory getClientHttpRequestFactory() {
        int timeout = 5000;
        RequestConfig config = RequestConfig.custom()
                .setConnectTimeout(timeout)
                .setConnectionRequestTimeout(timeout)
                .setSocketTimeout(timeout)
                .build();
        CloseableHttpClient client = HttpClientBuilder
                .create()
                .setDefaultRequestConfig(config)
                .build();
        return new HttpComponentsClientHttpRequestFactory(client);
    }

}
           

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

@Autowired
private RestTemplate restTemplate;
           

從開發人員的回報,和網上的各種HTTP用戶端性能以及易用程度評測來看,OkHttp 優于 Apache的HttpClient、Apache的HttpClient優于HttpURLConnection。

是以,我們還可以通過如下方式,将底層的http用戶端換成OkHttp!

/**
 * 使用OkHttpClient作為底層用戶端
 * @return
 */
private ClientHttpRequestFactory getClientHttpRequestFactory(){
    OkHttpClient okHttpClient = new OkHttpClient.Builder()
            .connectTimeout(5, TimeUnit.SECONDS)
            .writeTimeout(5, TimeUnit.SECONDS)
            .readTimeout(5, TimeUnit.SECONDS)
            .build();
    return new OkHttp3ClientHttpRequestFactory(okHttpClient);
}
           

三、API 實踐

RestTemplate最大的特色就是對各種網絡請求方式做了包裝,能極大的簡化開發人員的工作量,下面我們以GET、POST、PUT、DELETE、檔案上傳與下載下傳為例,分别介紹各個API的使用方式!

3.1、GET 請求

通過RestTemplate發送HTTP GET協定請求,經常使用到的方法有兩個:

  • getForObject()
  • getForEntity()

二者的主要差別在于,getForObject()傳回值是HTTP協定的響應體。

getForEntity()傳回的是ResponseEntity,ResponseEntity是對HTTP響應的封裝,除了包含響應體,還包含HTTP狀态碼、contentType、contentLength、Header等資訊。

在Spring Boot環境下寫一個單元測試用例,首先建立一個Api接口,然後編寫單元測試進行服務測試。

  • 不帶參的get請求
@RestController
public class TestController {

    /**
     * 不帶參的get請求
     * @return
     */
    @RequestMapping(value = "testGet", method = RequestMethod.GET)
    public ResponseBean testGet(){
        ResponseBean result = new ResponseBean();
        result.setCode("200");
        result.setMsg("請求成功,方法:testGet");
        return result;
    }
}
           
public class ResponseBean {

    private String code;

    private String msg;

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    @Override
    public String toString() {
        return "ResponseBean{" +
                "code='" + code + '\'' +
                ", msg='" + msg + '\'' +
                '}';
    }
}
           
@Autowired
private RestTemplate restTemplate;

/**
 * 單元測試(不帶參的get請求)
 */
@Test
public void testGet(){
    //請求位址
    String url = "http://localhost:8080/testGet";

    //發起請求,直接傳回對象
    ResponseBean responseBean = restTemplate.getForObject(url, ResponseBean.class);
    System.out.println(responseBean.toString());
}
           
  • 帶參的get請求(restful風格)
@RestController
public class TestController {

    /**
     * 帶參的get請求(restful風格)
     * @return
     */
    @RequestMapping(value = "testGetByRestFul/{id}/{name}", method = RequestMethod.GET)
    public ResponseBean testGetByRestFul(@PathVariable(value = "id") String id, @PathVariable(value = "name") String name){
        ResponseBean result = new ResponseBean();
        result.setCode("200");
        result.setMsg("請求成功,方法:testGetByRestFul,請求參數id:" +  id + "請求參數name:" + name);
        return result;
    }
}
           
@Autowired
private RestTemplate restTemplate;


 /**
 * 單元測試(帶參的get請求)
 */
@Test
public void testGetByRestFul(){
    //請求位址
    String url = "http://localhost:8080/testGetByRestFul/{1}/{2}";

    //發起請求,直接傳回對象(restful風格)
    ResponseBean responseBean = restTemplate.getForObject(url, ResponseBean.class, "001", "張三");
    System.out.println(responseBean.toString());
}
           
  • 帶參的get請求(使用占位符号傳參)
@RestController
public class TestController {

    /**
     * 帶參的get請求(使用占位符号傳參)
     * @return
     */
    @RequestMapping(value = "testGetByParam", method = RequestMethod.GET)
    public ResponseBean testGetByParam(@RequestParam("userName") String userName,
                                             @RequestParam("userPwd") String userPwd){
        ResponseBean result = new ResponseBean();
        result.setCode("200");
        result.setMsg("請求成功,方法:testGetByParam,請求參數userName:" +  userName + ",userPwd:" + userPwd);
        return result;
    }
}
           
@Autowired
private RestTemplate restTemplate;

 /**
 * 單元測試(帶參的get請求)
 */
@Test
public void testGetByParam(){
    //請求位址
    String url = "http://localhost:8080/testGetByParam?userName={userName}&userPwd={userPwd}";

    //請求參數
    Map<String, String> uriVariables = new HashMap<>();
    uriVariables.put("userName", "唐三藏");
    uriVariables.put("userPwd", "123456");

    //發起請求,直接傳回對象(帶參數請求)
    ResponseBean responseBean = restTemplate.getForObject(url, ResponseBean.class, uriVariables);
    System.out.println(responseBean.toString());
}
           

上面的所有的getForObject請求傳參方法,getForEntity都可以使用,使用方法上也幾乎是一緻的,隻是在傳回結果接收的時候略有差别。

使用ResponseEntity<T> responseEntity來接收響應結果。用responseEntity.getBody()擷取響應體。

/**
 * 單元測試
 */
@Test
public void testAllGet(){
    //請求位址
    String url = "http://localhost:8080/testGet";

    //發起請求,傳回全部資訊
    ResponseEntity<ResponseBean> response = restTemplate.getForEntity(url, ResponseBean.class);

    // 擷取響應體
    System.out.println("HTTP 響應body:" + response.getBody().toString());

    // 以下是getForEntity比getForObject多出來的内容
    HttpStatus statusCode = response.getStatusCode();
    int statusCodeValue = response.getStatusCodeValue();
    HttpHeaders headers = response.getHeaders();

    System.out.println("HTTP 響應狀态:" + statusCode);
    System.out.println("HTTP 響應狀态碼:" + statusCodeValue);
    System.out.println("HTTP Headers資訊:" + headers);
}
           

3.2、POST 請求

其實POST請求方法和GET請求方法上大同小異,RestTemplate的POST請求也包含兩個主要方法:

  • postForObject()
  • postForEntity()

postForEntity()傳回全部的資訊,postForObject()方法傳回body對象,具體使用方法如下!

  • 模拟表單請求,post方法測試
@RestController
public class TestController {

    /**
     * 模拟表單請求,post方法測試
     * @return
     */
    @RequestMapping(value = "testPostByForm", method = RequestMethod.POST)
    public ResponseBean testPostByForm(@RequestParam("userName") String userName,
                                        @RequestParam("userPwd") String userPwd){
        ResponseBean result = new ResponseBean();
        result.setCode("200");
        result.setMsg("請求成功,方法:testPostByForm,請求參數userName:" + userName + ",userPwd:" + userPwd);
        return result;
    }
}
           
@Autowired
private RestTemplate restTemplate;

/**
 * 模拟表單送出,post請求
 */
@Test
public void testPostByForm(){
    //請求位址
    String url = "http://localhost:8080/testPostByForm";

    // 請求頭設定,x-www-form-urlencoded格式的資料
    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

    //送出參數設定
    MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
    map.add("userName", "唐三藏");
    map.add("userPwd", "123456");

    // 組裝請求體
    HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(map, headers);

    //發起請求
    ResponseBean responseBean = restTemplate.postForObject(url, request, ResponseBean.class);
    System.out.println(responseBean.toString());
}
           
  • 模拟表單請求,post方法測試(對象接受)
@RestController
public class TestController {

    /**
     * 模拟表單請求,post方法測試
     * @param request
     * @return
     */
    @RequestMapping(value = "testPostByFormAndObj", method = RequestMethod.POST)
    public ResponseBean testPostByForm(RequestBean request){
        ResponseBean result = new ResponseBean();
        result.setCode("200");
        result.setMsg("請求成功,方法:testPostByFormAndObj,請求參數:" + JSON.toJSONString(request));
        return result;
    }
}
           
public class RequestBean {


    private String userName;


    private String userPwd;


    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getUserPwd() {
        return userPwd;
    }

    public void setUserPwd(String userPwd) {
        this.userPwd = userPwd;
    }
}
           
@Autowired
private RestTemplate restTemplate;

/**
 * 模拟表單送出,post請求
 */
@Test
public void testPostByForm(){
    //請求位址
    String url = "http://localhost:8080/testPostByFormAndObj";

    // 請求頭設定,x-www-form-urlencoded格式的資料
    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

    //送出參數設定
    MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
    map.add("userName", "唐三藏");
    map.add("userPwd", "123456");

    // 組裝請求體
    HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(map, headers);

    //發起請求
    ResponseBean responseBean = restTemplate.postForObject(url, request, ResponseBean.class);
    System.out.println(responseBean.toString());
}
           
  • 模拟 JSON 請求,post 方法測試
@RestController
public class TestController {

    /**
     * 模拟JSON請求,post方法測試
     * @param request
     * @return
     */
    @RequestMapping(value = "testPostByJson", method = RequestMethod.POST)
    public ResponseBean testPostByJson(@RequestBody RequestBean request){
        ResponseBean result = new ResponseBean();
        result.setCode("200");
        result.setMsg("請求成功,方法:testPostByJson,請求參數:" + JSON.toJSONString(request));
        return result;
    }
}
           
@Autowired
private RestTemplate restTemplate;

/**
 * 模拟JSON送出,post請求
 */
@Test
public void testPostByJson(){
    //請求位址
    String url = "http://localhost:8080/testPostByJson";

    //入參
    RequestBean request = new RequestBean();
    request.setUserName("唐三藏");
    request.setUserPwd("123456789");

    //發送post請求,并列印結果,以String類型接收響應結果JSON字元串
    ResponseBean responseBean = restTemplate.postForObject(url, request, ResponseBean.class);
    System.out.println(responseBean.toString());
}
           
  • 模拟頁面重定向,post請求
@Controller
public class LoginController {

    /**
     * 重定向
     * @param request
     * @return
     */
    @RequestMapping(value = "testPostByLocation", method = RequestMethod.POST)
    public String testPostByLocation(@RequestBody RequestBean request){
        return "redirect:index.html";
    }
}

           
@Autowired
private RestTemplate restTemplate;

/**
 * 重定向,post請求
 */
@Test
public void testPostByLocation(){
    //請求位址
    String url = "http://localhost:8080/testPostByLocation";

    //入參
    RequestBean request = new RequestBean();
    request.setUserName("唐三藏");
    request.setUserPwd("123456789");

    //用于送出完成資料之後的頁面跳轉,傳回跳轉url
    URI uri = restTemplate.postForLocation(url, request);
    System.out.println(uri.toString());
}
           

輸出結果如下:

http://localhost:8080/index.html
           

3.3、PUT 請求

put請求方法,可能很多人都沒用過,它指的是修改一個已經存在的資源或者插入資源,該方法會向URL代表的資源發送一個HTTP PUT方法請求,示例如下!

@RestController
public class TestController {

    /**
     * 模拟JSON請求,put方法測試
     * @param request
     * @return
     */
    @RequestMapping(value = "testPutByJson", method = RequestMethod.PUT)
    public void testPutByJson(@RequestBody RequestBean request){
        System.out.println("請求成功,方法:testPutByJson,請求參數:" + JSON.toJSONString(request));
    }
}
           
@Autowired
private RestTemplate restTemplate;

/**
 * 模拟JSON送出,put請求
 */
@Test
public void testPutByJson(){
    //請求位址
    String url = "http://localhost:8080/testPutByJson";

    //入參
    RequestBean request = new RequestBean();
    request.setUserName("唐三藏");
    request.setUserPwd("123456789");

    //模拟JSON送出,put請求
    restTemplate.put(url, request);
}
           

3.4、DELETE 請求

與之對應的還有delete方法協定,表示删除一個已經存在的資源,該方法會向URL代表的資源發送一個HTTP DELETE方法請求。

@RestController
public class TestController {

    /**
     * 模拟JSON請求,delete方法測試
     * @return
     */
    @RequestMapping(value = "testDeleteByJson", method = RequestMethod.DELETE)
    public void testDeleteByJson(){
        System.out.println("請求成功,方法:testDeleteByJson");
    }
}
           
@Autowired
private RestTemplate restTemplate;

/**
 * 模拟JSON送出,delete請求
 */
@Test
public void testDeleteByJson(){
    //請求位址
    String url = "http://localhost:8080/testDeleteByJson";

    //模拟JSON送出,delete請求
    restTemplate.delete(url);
}
           

3.5、通用請求方法 exchange 方法

如果以上方法還不滿足你的要求。在RestTemplate工具類裡面,還有一個exchange通用協定請求方法,它可以發送GET、POST、DELETE、PUT、OPTIONS、PATCH等等HTTP方法請求。

打開源碼,我們可以很清晰的看到這一點。

真不是吹,Spring 裡這款牛逼的網絡工具庫你可能沒用過
真不是吹,Spring 裡這款牛逼的網絡工具庫你可能沒用過

采用exchange方法,可以滿足各種場景下的請求操作!

3.6、檔案上傳與下載下傳

除了經常用到的get和post請求以外,還有一個我們經常會碰到的場景,那就是檔案的上傳與下載下傳,如果采用RestTemplate,該怎麼使用呢?

案例如下,具體實作細節參考代碼注釋!

  • 檔案上傳
@RestController
public class FileUploadController {


    private static final String UPLOAD_PATH = "/springboot-frame-example/springboot-example-resttemplate/";

    /**
     * 檔案上傳
     * @param uploadFile
     * @return
     */
    @RequestMapping(value = "upload", method = RequestMethod.POST)
    public ResponseBean upload(@RequestParam("uploadFile") MultipartFile uploadFile,
                               @RequestParam("userName") String userName) {
        // 在 uploadPath 檔案夾中通過使用者名對上傳的檔案歸類儲存
        File folder = new File(UPLOAD_PATH + userName);
        if (!folder.isDirectory()) {
            folder.mkdirs();
        }

        // 對上傳的檔案重命名,避免檔案重名
        String oldName = uploadFile.getOriginalFilename();
        String newName = UUID.randomUUID().toString() + oldName.substring(oldName.lastIndexOf("."));

        //定義傳回視圖
        ResponseBean result = new ResponseBean();
        try {
            // 檔案儲存
            uploadFile.transferTo(new File(folder, newName));
            result.setCode("200");
            result.setMsg("檔案上傳成功,方法:upload,檔案名:" + newName);
        } catch (IOException e) {
            e.printStackTrace();
            result.setCode("500");
            result.setMsg("檔案上傳失敗,方法:upload,請求檔案:" + oldName);
        }
        return result;
    }
}

           
@Autowired
private RestTemplate restTemplate;

/**
 * 檔案上傳,post請求
 */
@Test
public void upload(){
    //需要上傳的檔案
    String filePath = "/Users/panzhi/Desktop/Jietu20220205-194655.jpg";

    //請求位址
    String url = "http://localhost:8080/upload";

    // 請求頭設定,multipart/form-data格式的資料
    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.MULTIPART_FORM_DATA);

    //送出參數設定
    MultiValueMap<String, Object> param = new LinkedMultiValueMap<>();
    param.add("uploadFile", new FileSystemResource(new File(filePath)));
    //服務端如果接受額外參數,可以傳遞
    param.add("userName", "張三");

    // 組裝請求體
    HttpEntity<MultiValueMap<String, Object>> request = new HttpEntity<>(param, headers);

    //發起請求
    ResponseBean responseBean = restTemplate.postForObject(url, request, ResponseBean.class);
    System.out.println(responseBean.toString());
}
           
  • 檔案下載下傳
@RestController
public class FileUploadController {


    private static final String UPLOAD_PATH = "springboot-frame-example/springboot-example-resttemplate/";


    /**
     * 帶參的get請求(restful風格)
     * @return
     */
    @RequestMapping(value = "downloadFile/{userName}/{fileName}", method = RequestMethod.GET)
    public void downloadFile(@PathVariable(value = "userName") String userName,
                             @PathVariable(value = "fileName") String fileName,
                             HttpServletRequest request,
                             HttpServletResponse response) throws Exception {

        File file = new File(UPLOAD_PATH + userName + File.separator + fileName);
        if (file.exists()) {
            //擷取檔案流
            FileInputStream fis = new FileInputStream(file);
            //擷取檔案字尾(.png)
            String extendFileName = fileName.substring(fileName.lastIndexOf('.'));
            //動态設定響應類型,根據前台傳遞檔案類型設定響應類型
            response.setContentType(request.getSession().getServletContext().getMimeType(extendFileName));
            //設定響應頭,attachment表示以附件的形式下載下傳,inline表示線上打開
            response.setHeader("content-disposition","attachment;fileName=" + URLEncoder.encode(fileName,"UTF-8"));
            //擷取輸出流對象(用于寫檔案)
            OutputStream os = response.getOutputStream();
            //下載下傳檔案,使用spring架構中的FileCopyUtils工具
            FileCopyUtils.copy(fis,os);
        }
    }
}

           
@Autowired
private RestTemplate restTemplate;

/**
 * 小檔案下載下傳
 * @throws IOException
 */
@Test
public void downloadFile() throws IOException {
    String userName = "張三";
    String fileName = "c98b677c-0948-46ef-84d2-3742a2b821b0.jpg";
    //請求位址
    String url = "http://localhost:8080/downloadFile/{1}/{2}";

    //發起請求,直接傳回對象(restful風格)
    ResponseEntity<byte[]> rsp = restTemplate.getForEntity(url, byte[].class, userName,fileName);
    System.out.println("檔案下載下傳請求結果狀态碼:" + rsp.getStatusCode());

    // 将下載下傳下來的檔案内容儲存到本地
    String targetPath = "/Users/panzhi/Desktop/"  + fileName;
    Files.write(Paths.get(targetPath), Objects.requireNonNull(rsp.getBody(), "未擷取到下載下傳檔案"));
}
           

這種下載下傳方法實際上是将下載下傳檔案一次性加載到用戶端本地記憶體,然後從記憶體将檔案寫入磁盤。這種方式對于小檔案的下載下傳還比較适合,如果檔案比較大或者檔案下載下傳并發量比較大,容易造成記憶體的大量占用,進而降低應用的運作效率。

  • 大檔案下載下傳
@Autowired
private RestTemplate restTemplate;

/**
 * 大檔案下載下傳
 * @throws IOException
 */
@Test
public void downloadBigFile() throws IOException {
    String userName = "張三";
    String fileName = "c98b677c-0948-46ef-84d2-3742a2b821b0.jpg";
    //請求位址
    String url = "http://localhost:8080/downloadFile/{1}/{2}";

    //定義請求頭的接收類型
    RequestCallback requestCallback = request -> request.getHeaders()
            .setAccept(Arrays.asList(MediaType.APPLICATION_OCTET_STREAM, MediaType.ALL));

    //對響應進行流式處理而不是将其全部加載到記憶體中
    String targetPath = "/Users/panzhi/Desktop/"  + fileName;
    restTemplate.execute(url, HttpMethod.GET, requestCallback, clientHttpResponse -> {
        Files.copy(clientHttpResponse.getBody(), Paths.get(targetPath));
        return null;
    }, userName, fileName);
}
           

這種下載下傳方式的差別在于:

  • 設定了請求頭APPLICATION_OCTET_STREAM,表示以流的形式進行資料加載
  • RequestCallback結合File.copy保證了接收到一部分檔案内容,就向磁盤寫入一部分内容。而不是全部加載到記憶體,最後再寫入磁盤檔案。

在下載下傳大檔案時,例如excel、pdf、zip等等檔案,特别管用,

四、小結

通過本章的講解,想必讀者初步的了解了如何使用RestTemplate友善快捷的通路restful接口。其實RestTemplate的功能非常強大,作者也僅僅學了點皮毛。