在〈註冊服務實例〉中,示範了如何透過 DiscoveryClient
來查找服務實例,不過實際上並不需要直接使用 DiscoveryClient
(除非你想要獲取全部服務實例來做些什麼),你只要在建構 RestTemplate
的 @Bean
方法上加註 @LoadBalanced
,RestTemplate
實例會被裝飾,擁有自動查找服務以及負載平衡的效果。
來實際將〈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
,實際上建構請求實體的過程是固定的流程,轉換為 Resource
、 Resources
的流程其實也可以通用化,而這可以由 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 找到以上的範例專案。