RestTemplate遠程服務調用


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 來發送請求,可以直接定義請求頭,而不需要使用攔截器。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM