在〈聊聊 Spring HATEOAS〉中大致談過,如何使用 Spring HATEOAS,讓應用程式的 REST 模型可以支援 HATEOAS,現在的問題是,若要使用 RestTemplate
來處理傳回的 HAL 該怎麼做?
就結論而言,必須為 RestTemplate
設置 HAL 轉換器,而這個轉換器的建立方式是:
private MappingJackson2HttpMessageConverter getHalMessageConverter() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.registerModule(new Jackson2HalModule());
MappingJackson2HttpMessageConverter halConverter = new TypeConstrainedMappingJackson2HttpMessageConverter(
ResourceSupport.class);
halConverter.setSupportedMediaTypes(Arrays.asList(MediaTypes.HAL_JSON));
halConverter.setObjectMapper(objectMapper);
return halConverter;
}
有點麻煩對吧!稍後會談到一個有點 HACK 的方式,總之,有了這個轉換器後,可以在建立 RestTemplate
時設定給 RestTemplate
實例:
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
List<HttpMessageConverter<?>> existingConverters = restTemplate.getMessageConverters();
List<HttpMessageConverter<?>> newConverters = new ArrayList<>();
newConverters.add(getHalMessageConverter());
newConverters.addAll(existingConverters);
restTemplate.setMessageConverters(newConverters);
return restTemplate;
}
有了這個 RestTemplate
實例,對於底下這類 HAL JSON 回應:
{
"text": "msg1",
"_links": {
"self": {
"href": "http://localhost:8080/messages/1"
}
}
}
可以如下將之轉為 ResponseEntity<Resource<Message>>
:
@Test
public void show() {
RequestEntity<Void> request = RequestEntity
.get(URI.create(String.format("http://localhost:8080/messages/%s", "1")))
.build();
ResponseEntity<Resource<Message>> response =
restTemplate.exchange(request, new ParameterizedTypeReference<Resource<Message>>(){});
assertNotNull(response.getBody().getContent().getText());
}
回應的本體會是 Resource<Message>
,因此可以透過 getContent
取得 Message
實例。至於底下之類的 HAL JSON 格式:
{
"_embedded": {
"messageList": [
{
"text": "msg1",
"_links": {
"self": {
"href": "http://localhost:8080/messages/1"
}
}
},
{
"text": "msg2",
"_links": {
"self": {
"href": "http://localhost:8080/messages/2"
}
}
}
]
},
"_links": {
"self": {
"href": "http://localhost:8080/messages"
}
}
}
可以透過底下方式取得:
@Test
public void index() {
RequestEntity<Void> request = RequestEntity
.get(URI.create("http://localhost:8080/messages/"))
.build();
ResponseEntity<Resources<Message>> response =
restTemplate.exchange(request, new TypeReferences.ResourcesType<Message>() {});
assertTrue(response.getBody().getContent().size() > 0);
}
有興趣看完整範例專案的話,可以在 HATEOAS 取得。
那麼,怎麼簡單地取得 HAL 轉換器呢?既然 Spring Data Rest 透過 Spring HATEOAS 來轉換出 HAL JSON,那是不是意謂著,它內部就有 HAL 轉換器了?是的!你可以在專案的 build.gradle 中包含 Spring Data Rest:
implementation('org.springframework.boot:spring-boot-starter-data-rest')
這麼一來,Spring Boot 就會生成 HAL 轉換器,而 Bean 的名稱為 "halJacksonHttpMessageConverter"
,例如,在 XYZ 這個範例專案中,就以 @Qualifier
指定 "halJacksonHttpMessageConverter"
,直接取得了這個轉換器,基於 RestTemplate
來請求 @RepositoryRestResource 中範例專案產生的 HAL JSON,建立了一個簡單的訊息查看頁面:
package cc.openhome;
...略
@SpringBootApplication
@Controller
public class XyzApplication {
public static void main(String[] args) {
SpringApplication.run(XyzApplication.class, args);
}
@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://localhost:8080/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";
}
@GetMapping("{username}/messages")
public String userMessages(@PathVariable("username") String username, Model model) {
RequestEntity<Void> request = RequestEntity
.get(URI.create(String.format("http://localhost:8080/messages/search/messagesBy?username=%s", username)))
.build();
ResponseEntity<Resources<Message>> response =
restTemplate.exchange(request, new TypeReferences.ResourcesType<Message>() {});
model.addAttribute("title", String.format("%s 的訊息", username));
model.addAttribute("messages", new ArrayList<>(response.getBody().getContent()));
return "show";
}
@Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
List<HttpMessageConverter<?>> existingConverters = restTemplate.getMessageConverters();
List<HttpMessageConverter<?>> newConverters = new ArrayList<>();
newConverters.add(getHalMessageConverter());
newConverters.addAll(existingConverters);
restTemplate.setMessageConverters(newConverters);
return restTemplate;
}
@Autowired
@Qualifier("halJacksonHttpMessageConverter")
private TypeConstrainedMappingJackson2HttpMessageConverter halConverter;
public MappingJackson2HttpMessageConverter getHalMessageConverter() {
return halConverter;
}
}