RestTemplate 與 Ribbon/Feign


在〈註冊服務實例〉中,示範了如何透過 DiscoveryClient 來查找服務實例,不過實際上並不需要直接使用 DiscoveryClient(除非你想要獲取全部服務實例來做些什麼),你只要在建構 RestTemplate@Bean 方法上加註 @LoadBalancedRestTemplate 實例會被裝飾,擁有自動查找服務以及負載平衡的效果。

來實際將〈HAL 與 RestTemplate〉中的 XYZ 專案改造一下,看看怎麼套用以上的設定。

首先,build.gradle 同樣要〈註冊服務實例〉中的說明修改一下,並加入:

implementation('org.springframework.cloud:spring-cloud-starter-netflix-eureka-client')

然後,bootstrap.properties 中設定服務註冊伺服器的資訊,若這個 XYZ 不提供 REST 服務,可不用向伺服器註冊服務,只要取得服務實例註冊表就可以了,因此可以將 eureka.client.registerWithEureka 設為 false

server.port=80
eureka.client.registerWithEureka=false
spring.application.name=xyz
eureka.client.serviceUrl.defaultZone=http://localhost:8761/eureka/

然後,在產生 RestTemplate 的方法上加註 @LoadBalanced,並修改一下 exchange 對象的 URI:

package cc.openhome;

...略

@SpringBootApplication
@Controller
public class XyzApplication {
    ...略

    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("messages/{id}")
    public String user(@PathVariable("id") String id, Model model) {
        RequestEntity<Void> request = RequestEntity
                .get(URI.create(String.format("http://msg-service/messages/%s", id)))
                .build();

         ResponseEntity<Resource<Message>> response = 
                 restTemplate.exchange(request, new ParameterizedTypeReference<Resource<Message>>(){});

        model.addAttribute("title", String.format("第 %s 筆訊息", id));
        model.addAttribute("messages", Arrays.asList(response.getBody().getContent()));
        return "show";
    }

    ...略

    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

RestTemplate 實例會被裝飾以支援客戶端負載平衡器 Ribbon,客戶端會快取註冊表,msg-service 會被自動替換為查找到的服務實例位置,若有多個服務實例,每個請求都會自動分配到不同的服務實例以平衡負擔,而且對於 HAL 的處理,也不用自行處理轉換器的設定,不過 build.gradle 中,必須有 org.springframework.boot:spring-boot-starter-data-rest 的相依。

另一個銜接服務實例的方式是基於 Feign 客戶端,為了更明白它大致的原理,可以先來重構一下專案,將 RestTemplate 銜接服務的細節,封裝到 MessageService 之中:

package cc.openhome;

...略 

@Component
public class MessageService {
    @Autowired
    private RestTemplate restTemplate;

    public Resource<Message> messageById(String id) {
        RequestEntity<Void> request = RequestEntity
                .get(URI.create(String.format("http://msg-service/messages/%s", id)))
                .build();

         ResponseEntity<Resource<Message>> response = 
                 restTemplate.exchange(request, new ParameterizedTypeReference<Resource<Message>>(){});

         return response.getBody();
    }

    public Resources<Message> messagesByUsername(String username) {
        RequestEntity<Void> request = RequestEntity
                .get(URI.create(String.format("http://msg-service/messages/search/messagesBy?username=%s", username)))
                .build();

        ResponseEntity<Resources<Message>> response = 
                restTemplate.exchange(request, new TypeReferences.ResourcesType<Message>() {});

        return response.getBody();
    }
}

這麼一來,XyzApplication 就可以重構為:

package cc.openhome;

...略

@SpringBootApplication
@Controller
public class XyzApplication {

    public static void main(String[] args) {
        SpringApplication.run(XyzApplication.class, args);
    }

    @Autowired
    private MessageService messageService;

    @GetMapping("messages/{id}")
    public String user(@PathVariable("id") String id, Model model) {
        model.addAttribute("title", String.format("第 %s 筆訊息", id));
        model.addAttribute("messages", Arrays.asList(messageService.messageById(id).getContent()));
        return "show";
    }

    @GetMapping("{username}/messages")
    public String userMessages(@PathVariable("username") String username, Model model) {
        model.addAttribute("title", String.format("%s 的訊息", username));
        model.addAttribute("messages", new ArrayList<>(messageService.messagesByUsername(username).getContent()));
        return "user";
    }
}

當然,應用程式的功能並沒有變化,現在來看看方才重構出來的 MessageService,實際上建構請求實體的過程是固定的流程,轉換為 ResourceResources 的流程其實也可以通用化,而這可以由 Feign 來處理,為了能使用 Feign,必須在 build.gradle 中加入:

implementation('org.springframework.cloud:spring-cloud-starter-openfeign')

然後,在 XyzApplication 上加註 @EnableFeignClients 以啟用 Feign 功能:

@SpringBootApplication
@EnableFeignClients
@Controller
public class XyzApplication {
    ...略
}

接著就是神奇的地方了,將 MessageService 改為以下宣告式的風格:

package cc.openhome;

...略

@FeignClient("msg-service")
public interface MessageService {
    @GetMapping(value = "messages/{id}")
    Resource<Message> messageById(@PathVariable("id") String id);

    @GetMapping(value = "messages/search/messagesBy?username={username}")
    Resources<Message> messagesByUsername(@PathVariable("username") String username);
}

@FeignClient("msg-service") 設定了服務實例的名稱,@XXXMapping 用來設定請求的方式以及目標 URI,參數的部份透過 @PathVariable 來對應。

你可以在 XYZ 找到以上的範例專案。