RestTemplate 是從 Spring3.0 開始支持的一個 HTTP 請求工具,它提供了常見的REST請求方案的模版,例如 GET 請求、POST 請求、PUT 請求、DELETE 請求以及一些通用的請求執行方法 exchange 以及 execute。RestTemplate 繼承自 InterceptingHttpAccessor 並且實現了 RestOperations 接口,其中 RestOperations 接口定義了基本的 RESTful 操作,這些操作在 RestTemplate 中都得到了實現。
准備工作:我們創建一個普通的maven工程、然后分別創建eureka、provider、consumer三個SpringBoot子工程項目、然后分別在application.properties或者application.yml進行配置。
一、GET請求
在RestTemplate中GET請求有這幾個方法:
這些請求中有兩類方法,分別是getForEntity和getForObject,每一個類都有三個重載方法。
getForEntity
我們要了解RestTemplate 發送的是Http請求,那么響應數據一定是有響應頭的,如果需要獲取響應頭的信息,那么就可以使用getForEntity來發送Http請求,此時返回的就是一個ResponseEntity的實例,這個實例包含着響應頭和響應的數據,例如下面的這個接口是來自provider 中的:
@RestController public class SayHelloController { @GetMapping("/hello") public String sayHello(String name) { return "hello " + name + " !"; } }
我們需要在consumer 啟動類中注入@RestTemplate
@SpringBootApplication public class ConsumerApplication { public static void main(String[] args) { SpringApplication.run(ConsumerApplication.class, args); } @Bean RestTemplate restTemplate() { return new RestTemplate(); } }
然后在consumer中添加一個SayUserHelloController ,並且提供一個/hello 來消費我們provider提供的服務
@RestController public class SayUseHelloController { @Autowired RestTemplate restTemplate; @Autowired DiscoveryClient discoveryClient; //org.springframework.cloud.client.discovery.DiscoveryClient @GetMapping("/hello") public String hello(String name) { //拿到服務提供商 List<ServiceInstance> list = discoveryClient.getInstances("provider"); //拿到第一個實例 ServiceInstance instance = list.get(0); //得到主機號 String host = instance.getHost(); //得到端口號 int port = instance.getPort(); //拼接完整的請求url String url = "http://" + host + ":" + port + "/hello?name={1}"; //restTemple 實際返回的是一個ResponseEntity 的實例 ResponseEntity<String> responseEntity = restTemplate.getForEntity(url, String.class, name); StrBuilder sb = new StrBuilder(); //拿到狀態碼 HttpStatus statusCode = responseEntity.getStatusCode(); //拿到響應體 String body= responseEntity.getBody(); sb.append("statusCode:") .append(statusCode) .append("<br/>") .append("body:") .append(body) .append("<br/>"); //拿到響應頭header HttpHeaders headers = responseEntity.getHeaders(); Set<String> keySet = headers.keySet(); for (String s : keySet) { sb.append(s) .append(":") .append(headers.get(s)) .append("</br>"); } return sb.toString(); } }
主要來看 getForEntity 方法。第一個參數是 url ,url 中有一個占位符 {1} ,如果有多個占位符分別用 {2} 、 {3} … 去表示,第二個參數是接口返回的數據類型,最后是一個可變長度的參數,用來給占位符填值。在返回的 ResponseEntity 中,可以獲取響應頭中的信息,其中 getStatusCode 方法用來獲取響應狀態碼, getBody 方法用來獲取響應數據, getHeaders 方法用來獲取響應頭,在瀏覽器中訪問該接口,結果如下:
除了上面這一種方法外,還有兩種方法也就是getForEntity另外兩個重載方法,使用map
Map<String,Object> map = new HashMap<>();
map.put("name",name); ResponseEntity<String> responseEntity = restTemplate.getForEntity(url, String.class, map);
還有一種就是使用Uri對象
String url = "http://" + host + ":" + port + "/hello?name=" + URLEncoder.encode(name, "UTF-8"); URI uri = URI.create(url); ResponseEntity<String> responseEntity = restTemplate.getForEntity(uri,String.class);
注意這里使用URL如果是帶了中文字符串那么需要使用UTF-8。
getForObject
getForObject 方法和 getForEntity 方法類似,getForObject 方法也有三個重載的方法,參數和 getForEntity 一樣。
getForObject 和 getForEntity 的差異主要體現在返回值的差異上, getForObject 的返回值就是服務提供者返回的數據,使用 getForObject 無法獲取到響應頭。
例如,還是上面的請求,利用 getForObject 來發送 HTTP 請求,結果如下:
String url = "http://" + host + ":" + port + "/hello?name=" + URLEncoder.encode(name, "UTF-8"); URI uri = URI.create(url); String s = restTemplate.getForObject(uri, String.class);
注意,這里返回的 s 就是 provider 的返回值,如果開發者只關心 provider 的返回值,並不關系 HTTP 請求的響應頭,那么可以使用該方法。
二、POST請求
和 GET 請求相比,RestTemplate 中的 POST 請求多了一個類型的方法,如下:
可以看到,post 請求的方法類型除了 postForEntity 和 postForObject 之外,還有一個 postForLocation。這里的方法類型雖然有三種,但是這三種方法重載的參數基本是一樣的。
postForEntity
在 POST 請求中,參數的傳遞可以是 key/value 的形式,也可以是 JSON 數據,分別來看:
傳遞 key/value 形式的參數
首先在 provider 的 SayHelloController 類中再添加一個 POST 請求的接口,如下:
@PostMapping("/hello2") public String sayHello2(String name) { return "Hello " + name + " !"; }
然后在consumer 中去調用provider 中提供的/hello2
@GetMapping("/hello5") public String hello5(String name) { List<ServiceInstance> list = discoveryClient.getInstances("provider"); ServiceInstance instance = list.get(0); String host = instance.getHost(); int port = instance.getPort(); String url = "http://" + host + ":" + port + "/hello2"; MultiValueMap map = new LinkedMultiValueMap(); map.add("name", name); ResponseEntity<String> responseEntity = restTemplate.postForEntity(url, map, String.class); return responseEntity.getBody(); }
postForEntity 方法第一個參數是請求地址,第二個參數 map 對象中存放着請求參數 key/value,第三個參數則是返回的數據類型。當然這里的第一個參數 url 地址也可以換成一個 Uri 對象,效果是一樣的。這種方式傳遞的參數是以 key/value 形式傳遞的,在 post 請求中,也可以按照 get 請求的方式去傳遞 key/value 形式的參數,傳遞方式和 get 請求的傳參方式基本一致,例如下面這樣:
@GetMapping("/hello6") public String hello6(String name) { List<ServiceInstance> list = discoveryClient.getInstances("provider"); ServiceInstance instance = list.get(0); String host = instance.getHost(); int port = instance.getPort(); String url = "http://" + host + ":" + port + "/hello2?name={1}"; ResponseEntity<String> responseEntity = restTemplate.postForEntity(url, null, String.class,name); return responseEntity.getBody(); }
此時第二個參數可以直接傳一個 null。
如何傳遞JSON數據
上面介紹的是 post 請求傳遞 key/value 形式的參數,post 請求也可以直接傳遞 json 數據,在 post 請求中,可以自動將一個對象轉換成 json 進行傳輸,數據到達 provider 之后,再被轉換為一個對象。具體操作步驟如下:
(1)定義一個公共的模塊common,創建一個 User 對象
public class UserDTO { private String nickname; private String address; }
(2)分別在 provider 和 consumer 的 pom.xml 文件中添加對 commons 模塊的依賴,這樣在 provider 和 consumer 中就都能使用 UserDTO 對象了。
在 provider 中創建一個添加用戶的接口,如下:
@Controller @ResponseBody public class UserController { @PostMapping("/user") public ResponseEntity<Object> userDTO(@RequestBody UserDTO userDTO) { return new ResponseEntity<Object>(userDTO, HttpStatus.OK); } }
這里的接口很簡單,只需要將用戶傳來的 User 對象再原封不動地返回去就行了,然后在 consumer 中添加一個接口來測試這個接口,如下:
@PostMapping("/hello7") public ResponseEntity<UserDTO> hello7(@RequestBody UserDTO userDTO) { List<ServiceInstance> list = discoveryClient.getInstances("provider"); ServiceInstance instance = list.get(0); String host = instance.getHost(); int port = instance.getPort(); String url = "http://" + host + ":" + port + "/user"; UserDTO u = new UserDTO(); u.setNickname(userDTO.getNickname()); u.setAddress(userDTO.getAddress()); ResponseEntity<UserDTO> responseEntity = restTemplate.postForEntity(url, u, UserDTO.class); return new ResponseEntity<UserDTO>(responseEntity.getBody(),HttpStatus.OK); }
我們訪問localhost:9911/hello7並且傳入如下參數:
{ "nickname": "技術無止境", "address": "四川成都" }
看到這段代碼有人要問了,這不和前面的一樣嗎?是的,唯一的區別就是第二個參數的類型不同,這個參數如果是一個 MultiValueMap 的實例,則以 key/value 的形式發送,如果是一個普通對象,則會被轉成 json 發送。
postForObject
postForObject 和 postForEntity 基本一致,就是返回類型不同而已。
postForLocation
postForLocation 方法的返回值是一個 Uri 對象,因為 POST 請求一般用來添加數據,有的時候需要將剛剛添加成功的數據的 URL 返回來,此時就可以使用這個方法,一個常見的使用場景如用戶注冊功能,用戶注冊成功之后,可能就自動跳轉到登錄頁面了,此時就可以使用該方法。例如在 provider 中提供一個用戶注冊接口,再提供一個用戶登錄接口,如下:
@RequestMapping("/register") public String register(UserDTO userDTO) throws UnsupportedEncodingException { return "redirect:/loginPage?username=" + URLEncoder.encode(userDTO.getNickname(),"UTF-8") + "&address=" + URLEncoder.encode(userDTO.getAddress(),"UTF-8"); } @GetMapping("/loginPage") @ResponseBody public String loginPage(UserDTO userDTO) { return "loginPage:" + userDTO.getNickname() + ":" + userDTO.getAddress(); }
注意:postForLocation 方法返回的 Uri 實際上是指響應頭的 Location 字段,所以,provider 中 register 接口的響應頭必須要有 Location 字段(即請求的接口實際上是一個重定向的接口),否則 postForLocation 方法的返回值為null。
三、PUT請求
只要將 GET 請求和 POST 請求搞定了,接下來 PUT 請求就會容易很多了,PUT 請求本身方法也比較少,只有三個,如下:
這三個重載的方法其參數其實和 POST 是一樣的,可以用 key/value 的形式傳參,也可以用 JSON 的形式傳參,無論哪種方式,都是沒有返回值的。
首先在 provider 的 UserController 中添加如下兩個數據更新接口:
@PutMapping("/user/name") @ResponseBody public void updateUserByUsername(User User) { System.out.println(User); } @PutMapping("/user/address") @ResponseBody public void updateUserByAddress(@RequestBody User User) { System.out.println(User); }
這里兩個接口,一個接收 key/value 形式的參數,另一個接收 JSON 參數。因為這里沒有返回值,我直接把數據打印出來就行了。接下來在 consumer 中添加接口調用這里的服務,如下:
@GetMapping("/hello9") public void hello9() { List<ServiceInstance> list = discoveryClient.getInstances("provider"); ServiceInstance instance = list.get(0); String host = instance.getHost(); int port = instance.getPort(); String url1 = "http://" + host + ":" + port + "/user/name"; String url2 = "http://" + host + ":" + port + "/user/address"; MultiValueMap map = new LinkedMultiValueMap(); map.add("nickname","技術無止境"); map.add("address", "四川成都"); restTemplate.put(url1, map); UserDTO u = new UserDTO(); u.setNickname("獨孤劍聖"); u.setAddress("北上廣"); restTemplate.put(url2, u); }
四、Delete請求
和 PUT 請求一樣,DELETE 請求也是比較簡單的,只有三個方法,如下:
不同於 POST 和 PUT ,DELETE 請求的參數只能在地址欄傳送,可以是直接放在路徑中,也可以用 key/value 的形式傳遞,當然,這里也是沒有返回值的。
首先在 provider 的 UserController 中添加兩個接口,如下:
@DeleteMapping("/user/{id}") @ResponseBody public void deleteUserById(@PathVariable Integer id) { System.out.println(id); } @DeleteMapping("/user/") @ResponseBody public void deleteUserByUsername(String username) { System.out.println(username); }
兩個接口,一個的參數在路徑中,另一個的參數以 key/value 的形式傳遞,然后在 consumer 中,添加一個方法調用這兩個接口,如下:
@GetMapping("/hello10") public void hello10() { List<ServiceInstance> list = discoveryClient.getInstances("provider"); ServiceInstance instance = list.get(0); String host = instance.getHost(); int port = instance.getPort(); String url1 = "http://" + host + ":" + port + "/user/{1}"; String url2 = "http://" + host + ":" + port + "/user/?nickname={nickname}"; Map<String,String> map = new HashMap<>(); map.put("nickname","技術無止境"); restTemplate.delete(url1, 99); restTemplate.delete(url2, map); }
五、如何設置請求頭
有的時候我們會有一些特殊的需求,例如模擬 cookie ,此時就需要我們自定義請求頭了。自定義請求頭可以通過攔截器的方式來實現。定義攔截器、自動修改請求數據、一些身份認證信息等,都可以在攔截器中來統一處理。具體操作步驟如下:
首先在 provider 中定義一個接口,在接口中獲取客戶端傳來的 cookie 數據,如下:
@GetMapping("diyheader") public String diyHeader(HttpServletRequest request) { return request.getHeader("cookie"); }
這里簡單處理,將客戶端傳來的 cookie 拿出來后再返回給客戶端,然后在 consumer 中添加如下接口來測試:
@GetMapping("/hello11") public void hello11() { List<ServiceInstance> list = discoveryClient.getInstances("provider"); ServiceInstance instance = list.get(0); String host = instance.getHost(); int port = instance.getPort(); String url = "http://" + host + ":" + port + "/diyheader"; restTemplate.setInterceptors(Collections.singletonList(new ClientHttpRequestInterceptor() { @Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException, IOException { HttpHeaders headers = request.getHeaders(); headers.add("cookie","i love java"); return execution.execute(request,body); } })); String s = restTemplate.getForObject(url, String.class); System.out.println(s); }
這里通過調用 RestTemplate 的 setInterceptors 方法來給它設置攔截器,攔截器也可以有多個,我這里只有一個。在攔截器中,將請求拿出來,給它設置 cookie ,然后調用 execute 方法讓請求繼續執行。此時,在 /diyheader 接口中就獲取到cookie了。
六、通用方法 exchange
在 RestTemplate 中還有一個通用的方法 exchange。為什么說它通用呢?因為這個方法需要你在調用的時候去指定請求類型,即它既能做 GET 請求,也能做 POST 請求,也能做其它各種類型的請求。如果開發者需要對請求進行封裝,使用它再合適不過了。
@GetMapping("/hello12") public void hello12() { List<ServiceInstance> list = discoveryClient.getInstances("provider"); ServiceInstance instance = list.get(0); String host = instance.getHost(); int port = instance.getPort(); String url = "http://" + host + ":" + port + "/diyheader"; HttpHeaders headers = new HttpHeaders(); headers.add("cookie","i love gir"); HttpEntity<MultiValueMap<String,String>> request = new HttpEntity<>(null,headers); ResponseEntity<String> responseEntity = restTemplate.exchange(url, HttpMethod.GET, request, String.class); System.out.println(responseEntity.getBody()); }
這里的參數和前面的也都差不多,注意就是多了一個請求類型的參數,然后創建一個 HttpEntity 作為參數來傳遞。 HttpEntity 在創建時候需要傳遞兩個參數,第一個上文給了一個 null ,這個參數實際上就相當於 POST/PUT 請求中的第二個參數,有需要可以自行定義。HttpEntity 創建時的第二個參數就是請求頭了,也就是說,如果使用 exchange 來發送請求,可以直接定義請求頭,而不需要使用攔截器。