Spring之RestTemplate詳解
1 RestTemplate
1.1 引言
現如今的 IT
專案,由服務端向外發起網路請求的場景,基本上處處可見!
傳統情況下,在服務端程式碼裡訪問 http
服務時,一般會使用 JDK
的 HttpURLConnection
Apache
的 HttpClient
,不過這種方法使用起來太過繁瑣,而且 api
使用起來非常的複雜,還得操心資源回收。
以下載檔案為例,通過 Apache
的 HttpClient
方式進行下載檔案,會很複雜
其實Spring
已經為我們提供了一種簡單便捷的模板類來進行操作,它就是RestTemplate
RestTemplate
是一個執行HTTP
請求的同步阻塞式工具類,它僅僅只是在 HTTP
客戶端庫(例如 JDK HttpURLConnection
,Apache HttpComponents
,okHttp
等)基礎上,封裝了更加簡單易用的模板方法 API,方便程式設計師利用已提供的模板方法發起網路請求和處理,能很大程度上提升我們的開發效率
1.2 環境配置
1.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);
}
1.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 {
@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);
}
1.3 API 實踐
RestTemplate
最大的特色就是對各種網路請求方式做了包裝,能極大的簡化開發人員的工作量,下面我們以GET、POST、PUT、DELETE、檔案上傳與下載
為例,分別介紹各個API的使用方式
1.3.1 GET請求
通過RestTemplate
傳送HTTP GET
協議請求,經常使用到的方法有兩個:
-
getForObject()
:返回值是HTTP
協議的響應體 -
getForEntity()
:返回的是ResponseEntity
,ResponseEntity
是對HTTP
響應的封裝,除了包含響應體,還包含HTTP
狀態碼、contentType、contentLength、Header
等資訊
在Spring Boot
環境下寫一個單元測試用例,首先建立一個Api
介面,然後編寫單元測試進行服務測試。
1.3.1.1 不帶參請求
不帶參的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;
省去getset方法
}
@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());
}
1.3.1.2 帶參的get請求(使用佔位符號傳參)
@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());
}
1.3.1.3 帶參的get請求(restful風格)
@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());
}
1.3.1.4 getForEntity使用示例
上面的所有的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);
}
1.3.2 POST請求
其實POST
請求方法和GET
請求方法上大同小異,RestTemplate
的POST
請求也包含兩個主要方法:
-
postForObject()
:返回body
物件 -
postForEntity()
:返回全部的資訊
1.3.2.1 模擬表單請求
模擬表單請求,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());
}
1.3.2.2 模擬表單請求(傳遞物件)
模擬表單請求,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;
省去getset方法
}
@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());
}
1.3.2.3 模擬JSON請求
模擬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());
}
1.3.2.4 模擬頁面重定向
模擬頁面重定向,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
1.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);
}
1.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);
}
1.3.5 通用請求方法exchange方法
如果以上方法還不滿足你的要求。在RestTemplate
工具類裡面,還有一個exchange
通用協議請求方法,它可以傳送GET、POST、DELETE、PUT、OPTIONS、PATCH等等HTTP
方法請求。
開啟原始碼,我們可以很清晰的看到這一點。
採用exchange
方法,可以滿足各種場景下的請求操作
1.3.6 檔案上傳與下載
除了經常用到的get
和post
請求以外,還有一個經常會碰到的場景,那就是檔案的上傳與下載,如果採用RestTemplate
,該怎麼使用呢?
案例如下,具體實現細節參考程式碼註釋!
1.3.6.1 檔案上傳
@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());
}
1.3.6.2 檔案下載
@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(), "未獲取到下載檔案"));
}
這種下載方法實際上是將下載檔案一次性載入到客戶端本地記憶體,然後從記憶體將檔案寫入磁碟。這種方式對於小檔案的下載還比較適合,如果檔案比較大或者檔案下載併發量比較大,容易造成記憶體的大量佔用,從而降低應用的執行效率
1.3.6.3 大檔案下載
@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等等檔案,特別管用,
轉載於:https://mp.weixin.qq.com/s/FEn7w12Cvuy-Or6qiZxYBg
1.4 核心講解
1.4.1 excute
所有的get、post、delete、put、options、head、exchange(一部分)
方法最終呼叫的都是excute
方法
public T execute(String url, HttpMethod method, @Nullable RequestCallback requestCallback,
@Nullable ResponseExtractor responseExtractor, Object… uriVariables)
從上面的Excute
方法中我們可以看出Excute
方法只是將String
格式的URI
轉成了java.net.URI
,之後呼叫了doExecute
方法。
整個呼叫過程如下
doExecute方法如下:doExecute
方法並沒有暴露出來,只能通過繼承呼叫
這裡需要了解兩個類: RequestCallback & ResponseExtractor
1.4.2 RequestCallback
RequestCallback
用於在ClientHttpRequest
上操作的程式碼的回撥介面。允許操作請求頭,並寫入請求主體。
RequestCallback
有兩個實現類,都是內部類:
-
AcceptHeaderRequestCallback
:只處理請求頭,用於restTemplate.getXXX()
方法 -
HttpEntityRequestCallback
:繼承於AcceptHeaderRequestCallback
可以處理請求頭
和body
,用於restTemplate.putXXX()、restTemplate.postXXX()和restTemplate.exchange()
方法
1.4.3 ResponseExtractor
restTemplate
對此介面ResponseExtractor
的檢索方法實現,使用的通用回撥介面執行,從clienthttpresponse
提取資料的實際工作(解析HTTP
響應的資料),但不需要擔心異常處理或關閉資源
RequestCallback
有三個實現類:
-
HeadersExtractor
:用於提取請求頭 -
HttpMessageConverterExtractor
:用於提取響應body
-
ResponseEntityResponseExtractor
:使用HttpMessageConverterExtractor
提取body
(委託模式),然後將body和響應頭、狀態封裝成ResponseEntity
物件。