Rest訪問(RestTemplate)
在實際的項目中,往往需要發送一個Get/Post請求到其他的系統(Rest API),比如向人員管理部門請求,然后解析返回信息獲取該用戶的基本信息等。JDK傳統的HttpURLConnection、Apache HttpClient、Netty 4和OkHttp等可以實現訪問請求。不過spring的RestTemplate封裝了這些操作庫,使之更容易使用。
一. Rest的具體使用
1.1 get方式
RestTemplate get方式中有兩個方法: getForObject和getForEntity
getForObject可以指定返回類型,getForEntity同一返回ResponseEntity
1.1.1 getForObject
有三種重載方式
(1) T getForObject(URI url, Class responseType)
(2) T getForObject(String url, Class responseType, MapString< String, ?> urlVariables)
(3) T getForObject(String url, Class responseType, Object… urlVariables)
其中url為請求url,可用通配符表示請求參數,responseType為請求返回的對象類,自動封裝成對象該對象形式(如String.class 或User.class), Map< String, ?> urlVariables表示請求參數,與通配符對應即可, Object… urlVariables為請求的參數數組形式,按順序一一匹配url中內.
下面舉一個例子:
定義一個返回類型:User
public class User { private String name; private Integer age; } 省略get和set方法
發送方
private RestTemplate restTemplate = new RestTemplate(); private String name = "xiaoming"; private Integer age = 18; #重載方式3 String url = "http://localhost:8080/getUser?name={name}&age={age}"; Object[] arr = new Object[]{name, age}; User u = restTemplate.getForObject(url, User.class, arr); #重載方式2 String url = "http://localhost:8080/getUser?name={name}&age={age}"; Map<String, Object> map = new HashMap<>(); map.put("name", name); map.put("age", age); User u = restTemplate.getForObject(url, User.class, map); #重載方式1 #沒有參數的get方式直接調用重載方式1。如 String url = "http://localhost:8080/findAllUser"; User u = restTemplate.getForObject(url, User.class); #實際生產中常用的是 String url = "http://localhost:8080/getUser?name=${name}&age=${age}"; url.replace("${name}", name) .replace("${age}", age); User u = restTemplate.getForObject(url, User.class);
接受方controller
public User getUser(@RequestParam String name, @RequestParam Integer age)
1.1.2 getForEntity
getForEntity與getForObject請求參數基本一樣,只是返回內容不一樣 .getForEntity返回ResponseEntity,里面包含返回消息內容和http headers,http 狀態碼。
在實際的生產環境中get請求幾乎全部getForEntity,因為需要判斷狀態碼判斷接口是否調用成功(status == 10000)。返回類型往往是Json格式的字符串,然后通過轉換為Json獲取對應的信息。
發送方
String url = "http://localhost:8080/getUser?name={name}&age={age}"; Map<String, Object> map = new HashMap<>(); map.put("name", name); map.put("age", age); ResponseEntity<User> res = restTemplate.getForEntity(url, User.class, map); User u = res.getBody(); HttpHeaders headers = res.getHeaders(); HttpStatus status = res.getStatusCode();
1.2 Post方式
post方法主要有3種方法:postForObject , postForEntity和postForLocation
1.2.1 postForObject
(1) T postForObject(URI url, Object request, Class responseType )
(2) T postForObject(String url, Object request, Class responseType, MapString< String, ?> urlVariables)
(3) T postForObject(String url, Object request, Class responseType, Object… urlVariables)
形式和getForObject類似。
發送方
String url = "http://localhost:8080/getUser"; LinkedMultiValueMap<String, Object> map = new LinkedMultiValueMap<>(); map.add("name", name); map.add("age", age); User u = restTemplate.postForObject(url, map, User.class);
更常見的為:
String url = "http://localhost:8080/getUser"; LinkedMultiValueMap<String, Object> map = new LinkedMultiValueMap<>(); map.add("name", name); map.add("age", age); HttpEntity<LinkedMultiValueMap<String, Object>> httpEntity = new HttpEntity<>(map); ##還可以設置http頭 HttpHeaders headers = new HttpHeaders(); headers.add("msg", "head msg test"); HttpEntity<LinkedMultiValueMap<String, Object>> httpEntity = new HttpEntity<>(map, headers); User u = restTemplate.postForObject(url, httpEntity, User.class);
接受方
@RequestMapping(value = "/getUser", method = RequestMethod.POST) public User get1(@RequestParam String name, @RequestParam Integer age, @RequestHeader(required = false) String msg)
1.2.2 postForEntity
postForEntity返回ResponseEntity,里面包含返回消息內容和http headers,http 狀態碼。在實際中,也是往往使用PostForEntity.
1.2.3 postForLocation
postForLocation返回URI,返回的是response header中的location信息,一般用於資源定位。
1.3 其他方式
http請求共有8種方式,Post和Get是最常見的兩類方式。
方法 | 描述 |
GET | 請求指定的頁面信息,並返回實體主體。 |
HEAD | 類似於get請求,只不過返回的響應中沒有具體的內容,用於獲取報頭 |
POST | 向指定資源提交數據進行處理請求(例如提交表單或者上傳文件)。數據被包含在請求體中。POST請求可能會導致新的資源的建立和/或已有資源的修改。 |
PUT | 從客戶端向服務器傳送的數據取代指定的文檔的內容。 |
DELETE | 請求服務器刪除指定的頁面。 |
CONNECT | HTTP/1.1協議中預留給能夠將連接改為管道方式的代理服務器。 |
OPTIONS | 允許客戶端查看服務器的性能。 |
TRACE | 回顯服務器收到的請求,主要用於測試或診斷。 |
RestTemplate支持以下6種主要的方式。
另外,exchange和execute方法提供了以上方法的通用版本,用來支持額外的、不常用的組合(如:HTTP PATCH,帶有消息體的HTTP PUT,等等)。注意,無論怎樣使用底層HTTP庫,都必須支持必要的組合。
1.4 配置
1.4.1 設置超時時間
spring boot 中
@Configuration public class AppConfig { @Bean @ConfigurationProperties(prefix = "custom.rest.connection") public HttpComponentsClientHttpRequestFactory customHttpRequestFactory() { return new HttpComponentsClientHttpRequestFactory(); } @Bean public RestTemplate customRestTemplate() { return new RestTemplate(customHttpRequestFactory()); } }
然后在配置文件指定
custom.rest.connection.connection-request-timeout=3000
custom.rest.connection.connect-timeout=3000
custom.rest.connection.read-timeout=3000
1.4.2 上傳文件
public void testUpload() throws Exception { String url = "http://127.0.0.1:8080/test/upload.do"; String filePath = "C:\\Users\\MikanMu\\Desktop\\test.txt"; RestTemplate rest = new RestTemplate(); FileSystemResource resource = new FileSystemResource(new File(filePath)); MultiValueMap<String, Object> param = new LinkedMultiValueMap<>(); param.add("jarFile", resource); param.add("fileName", "test.txt"); //設置頭為上傳 HttpHeaders header = new HttpHeaders(); header.setContentType(MediaType.MULTIPART_FORM_DATA); HttpEntity<LinkedMultiValueMap<String, Object>> httpEntity = new HttpEntity<>(param, headers); String string = rest.postForObject(url, httpEntity, String.class); }
1.4.3 下載文件
// 小文件 RequestEntity requestEntity = RequestEntity.get(uri).build(); ResponseEntity<byte[]> responseEntity = restTemplate.exchange(requestEntity, byte[].class); byte[] downloadContent = responseEntity.getBody(); // 大文件 ResponseExtractor<ResponseEntity<File>> responseExtractor = new ResponseExtractor<ResponseEntity<File>>() { @Override public ResponseEntity<File> extractData(ClientHttpResponse response) throws IOException { File rcvFile = File.createTempFile("rcvFile", "zip"); FileCopyUtils.copy(response.getBody(), new FileOutputStream(rcvFile)); return ResponseEntity.status(response.getStatusCode()).headers(response.getHeaders()).body(rcvFile); } }; File getFile = this.restTemplate.execute(targetUri, HttpMethod.GET, null, responseExtractor);
二. RestTemplate的原理(轉)
RestTemplate包含以下幾個部分:
- HttpMessageConverter 對象轉換器
- ClientHttpRequestFactory 默認是JDK的HttpURLConnection
- ResponseErrorHandler 異常處理
- ClientHttpRequestInterceptor 請求攔截器
2.1 RestTemplate類圖
2.2 postForEntity 處理過程
以postForEntity方法作為切入點,來梳理一下請求是如何執行的,以下概要流程圖。(灰色方框內為doExcute方法的內部處理。)
postForEntity方法中,創建了兩個內部類對象requestCallback和responseExtractor並傳遞給execute方法,分別用於請求和響應的關鍵處理。
總結了一下,不管是請求還是響應,這里的關鍵處理就是明確資源的媒體類型(也就是要明確請求端和響應端交換的信息的格式),
根據媒體類型選擇適合的解析器,將消息寫入輸出流或者從輸入流讀入。
2.3 requestCallback.doWithRequest 處理過程
——內部類AcceptHeaderRequestCallback.doWithRequest的處理。
發送請求時,Http頭部需要設置Accept字段,該字段表明了發送請求的這方接受的媒體類型(消息格式),也是響應端要返回的信息的媒體類型(消息格式)。
根據postForEntity方法的第三個參數responseType,程序將選擇適合的解析器XXXConverter,並依據該解析器找出所有支持的媒體類型。
——內部類HttpEntityRequestCallback.doWithRequest的處理。
如果是POST請求並且消息體存在時,除了設置Accept字段,還可能需要設置Content-Type字段,該字段表明了所發送請求的媒體類型(消息格式),也是響應端接受的媒體類型(消息格式)。
根據postForEntity方法的第二個參數request,程序將選擇適合的解析器XXXConverter,將請求消息寫入輸出流。
2.4 responseExtractor.extractData 處理過程
與請求消息體的處理過程相似。
雖然,postForEntity方法中responseExtractor對象的類型為ResponseEntityResponseExtractor,但是實際執行處理過程是HttpMessageConverterExtractor的對象實例。
在postForObject方法中,則是直接使用了HttpMessageConverterExtractor創建對象。 下圖畫出的也是HttpMessageConverterExtractor類中的extractData方法的處理過程。
2.5 關於GenericHttpMessageConverter
在以上幾個方法的梳理過程中,我注意到每次消息解析轉換都要作GenericHttpMessageConverter分支判斷,為什么呢?
GenericHttpMessageConverter接口繼承自HttpMessageConverter接口,二者都是在org.springframework.http.converter路徑下。
此包中還有其他幾種Converter實現類,看名字就可以猜到主要功能。唯獨GenericHttpMessageConverter沒猜出來。
於是,我在eclipse中使用Ctrl+Shift+G快捷鍵搜索了一下它的實現類AbstractGenericHttpMessageConverter。
看到AbstractJackson2HttpMessageConverter類的時候,我好像明白了。
GenericHttpMessageConverter是其他轉換器派生類的接口,用於解析特殊格式的資源,比如json,xml等。
2.6 關於RestTemplate 中的轉換器列表
轉換器列表messageConverters是final類型的,由RestTemplate的構造函數賦值。一旦創建了RestTemplate對象,該對象也就同時擁有了一個當前系統支持的轉換器列表。
那么,對於需要引用jar包的轉換器,RestTemplate是怎么添加轉換器實例的呢?
在聲明messageConverters列表之前,定義了幾個布爾型靜態常量,該常量是對某一個特殊類是否可以被加載的判斷結果。
在RestTemplate的構造函數中,根據該常量值來判斷是否將某個轉換器的實例加入到列表中。
private static boolean romePresent = ClassUtils.isPresent("com.rometools.rome.feed.WireFeed", RestTemplate.class.getClassLoader()); private static final boolean jaxb2Present = ClassUtils.isPresent("javax.xml.bind.Binder", RestTemplate.class.getClassLoader()); private static final boolean jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", RestTemplate.class.getClassLoader()) && ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", RestTemplate.class.getClassLoader()); private static final boolean jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", RestTemplate.class.getClassLoader()); private static final boolean gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", RestTemplate.class.getClassLoader()); private final List<HttpMessageConverter<?>> messageConverters = new ArrayList<>();
由此可知,RestTemplate的初始化順序:
創建(new)一個RestTemplate實例時,首先裝載RestTemplate類,然后按照出現的順序轉載靜態變量或代碼。
裝載完成之后,進行實例化。首先實例化成員變量,然后執行構造函數。
那么,外部引用的類是否可以被加載具體是怎么判斷的?
通過ClassUtils.isPresent(String className, ClassLoader classLoader)方法。——感覺ClassUtils可以單獨寫一篇orz
ClassUtils 類在 spring-core 工程的 org.springframework.util 路徑下。
簡單來說,isPresent實際上是返回了ClassUtils.forName方法的處理結果,當forName方法正常執行,則鑒定的類被加載,返回true;若拋出異常(注意,此處異常是Throwable)則返回false。
forName方法的處理是:
首先,根據類名的長度(<=8)來確定是否是原始類型,若是原始類型則返回類對象Class