Невозможно выполнить HAL + JSON Level 3 RESTful API с помощью Spring HATEOAS из-за отсутствия ясности вокруг медиа-типа HAL + JSON
Уровень 3 API RESTful использует специальные типы мультимедиа, например application/vnd.service.entity.v1+json
. В моем случае я использую HAL для предоставления ссылок между связанными ресурсами в моем JSON.
Я не совсем понял правильный формат для специального медиа-типа, который использует HAL + JSON. То, что у меня сейчас, выглядит как application/vnd.service.entity.v1.hal+json
. Сначала я пошел с application/vnd.service.entity.v1+hal+json
, но суффикс +hal
не зарегистрирован и поэтому нарушает раздел 4.2.8 RFC6838.
Теперь Spring HATEOAS поддерживает ссылки в JSON из коробки, но специально для HAL-JSON вам нужно использовать @EnableHypermediaSupport(type=EnableHypermediaSupport.HypermediaType.HAL)
. В моем случае, поскольку я использую Spring Boot, я присоединяю это к моему классу инициализатора (т.е. К тому, который расширяет SpringBootServletInitializer
). Но Spring Boot не будет распознавать мои собственные медиа-типы из коробки. Поэтому для этого мне нужно было выяснить, как сообщить ему, что ему нужно использовать объект-сопоставление HAL для медиа-типов формы application/vnd.service.entity.v1.hal+json
.
Для моей первой попытки я добавил следующее в мой инициализатор загрузки Spring:
@Bean
public HttpMessageConverters customConverters() {
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
converter.setSupportedMediaTypes(Arrays.asList(
new MediaType("application", "json", Charset.defaultCharset()),
new MediaType("application", "*+json", Charset.defaultCharset()),
new MediaType("application", "hal+json"),
new MediaType("application", "*hal+json")
));
CurieProvider curieProvider = getCurieProvider(beanFactory);
RelProvider relProvider = beanFactory.getBean(DELEGATING_REL_PROVIDER_BEAN_NAME, RelProvider.class);
ObjectMapper halObjectMapper = beanFactory.getBean(HAL_OBJECT_MAPPER_BEAN_NAME, ObjectMapper.class);
halObjectMapper.registerModule(new Jackson2HalModule());
halObjectMapper.setHandlerInstantiator(new Jackson2HalModule.HalHandlerInstantiator(relProvider, curieProvider));
converter.setObjectMapper(halObjectMapper);
return new HttpMessageConverters(converter);
}
Это сработало, и я возвращал ссылки в правильном формате HAL. Однако это было совпадение. Это связано с тем, что фактический тип носителя, который заканчивается сообщением как "совместимый" с application/vnd.service.entity.v1.hal+json
, равен *+json
; он не распознает его против application/*hal+json
(см. ниже для объяснения). Мне не понравилось это решение, поскольку оно загрязняло существующий JSON-конвертер проблемами HAL. Итак, я сделал другое решение:
@Configuration
public class ApplicationConfiguration {
private static final String HAL_OBJECT_MAPPER_BEAN_NAME = "_halObjectMapper";
@Autowired
private BeanFactory beanFactory;
@Bean
public HttpMessageConverters customConverters() {
return new HttpMessageConverters(new HalMappingJackson2HttpMessageConverter());
}
private class HalMappingJackson2HttpMessageConverter extends MappingJackson2HttpMessageConverter {
public HalMappingJackson2HttpMessageConverter() {
setSupportedMediaTypes(Arrays.asList(
new MediaType("application", "hal+json"),
new MediaType("application", "*hal+json")
));
ObjectMapper halObjectMapper = beanFactory.getBean(HAL_OBJECT_MAPPER_BEAN_NAME, ObjectMapper.class);
setObjectMapper(halObjectMapper);
}
}
}
Это решение не работает; Я получаю ссылки в моем JSON, которые не соответствуют HAL. Это связано с тем, что application/vnd.service.entity.v1.hal+json
не распознается application/*hal+json
. Причина этого в том, что MimeType
, который проверяет совместимость медиа-типа, распознает только типы медиа, которые начинаются с *+
как допустимые типы медиафайлов wild-card для подтипов (например, application/*+json
). Вот почему первое решение сработало (по совпадению).
Итак, здесь есть две проблемы:
-
MimeType
будет никогда распознавать типы медиафайлов HAL формы application/vnd.service.entity.v1.hal+json
для application/*hal+json
.
-
MimeType
будет распознавать типы медиафайлов HAL, специфичные для поставщика формы application/vnd.service.entity.v1+hal+json
по сравнению с application/*+hal+json
, однако, это недопустимые типы mimetypes по раздел 4.2.8 RFC6838.
Похоже, что единственный правильный способ был бы, если +hal
распознается как действительный суффикс, и в этом случае второй вариант выше будет в порядке. В противном случае какой-либо другой вид медиа-типа wild-card не может распознавать специфические для HAL медиа-типы конкретного поставщика. Единственным вариантом было бы переопределить существующий конвертер сообщений JSON с проблемами HAL (см. Первое решение).
Другим обходным решением в настоящее время будет указание каждого пользовательского типа мультимедиа, который вы используете, при создании списка поддерживаемых типов медиа для конвертера сообщений. То есть:
@Configuration
public class ApplicationConfiguration {
private static final String HAL_OBJECT_MAPPER_BEAN_NAME = "_halObjectMapper";
@Autowired
private BeanFactory beanFactory;
@Bean
public HttpMessageConverters customConverters() {
return new HttpMessageConverters(new HalMappingJackson2HttpMessageConverter());
}
private class HalMappingJackson2HttpMessageConverter extends MappingJackson2HttpMessageConverter {
public HalMappingJackson2HttpMessageConverter() {
setSupportedMediaTypes(Arrays.asList(
new MediaType("application", "hal+json"),
new MediaType("application", "vnd.service.entity.v1.hal+json"),
new MediaType("application", "vnd.service.another-entity.v1.hal+json"),
new MediaType("application", "vnd.service.one-more-entity.v1.hal+json")
));
ObjectMapper halObjectMapper = beanFactory.getBean(HAL_OBJECT_MAPPER_BEAN_NAME, ObjectMapper.class);
setObjectMapper(halObjectMapper);
}
}
}
Это имеет смысл не загрязнять существующий конвертер JSON, но кажется менее элегантным. Кто-нибудь знает правильное решение для этого? Неужели я об этом совершенно не так?
Ответы
Ответ 1
Хотя этот вопрос немного старый, я недавно наткнулся на ту же проблему, поэтому я хотел дать свои 2 цента этой теме.
Я думаю, что проблема здесь в понимании HAL относительно JSON. Как вы уже указали здесь, все HAL - JSON, но не все JSON - HAL. Различие между ними заключается в том, что HAL определяет некоторые условные обозначения для семантики/структуры, например, говоря вам, что за атрибутом типа _links
вы найдете некоторые ссылки, тогда как JSON просто определяет формат, например key: [value]
( как уже упоминалось @zeroflagL)
В этом причина, почему тип носителя называется application/hal+json
. В основном это говорит о стиле/семантике HAL в формате JSON. Это также является причиной того, что существует тип носителя application/hal+xml
(источник).
Теперь с конкретным типом носителя поставщика вы определяете свою собственную семантику и, следовательно, заменяете hal
на application/hal+json
и не расширяете ее.
Если вы правильно поняли, вы в основном хотите сказать, что у вас есть собственный тип мультимедиа, который использует стиль HAL для форматирования JSON. (Таким образом, клиент может использовать некоторую библиотеку HAL для простого анализа вашего JSON.)
Итак, в конце я думаю, вы в основном должны решить, хотите ли вы различать JSON и HAL-based JSON, и ваш API должен предоставить один из этих или обоих.
Если вы хотите предоставить оба варианта, вам нужно будет определить два разных типа носителя vnd.service.entity.v1.hal+json
AND vnd.service.entity.v1+json
. Для типа носителя vnd.service.entity.v1.hal+json
вам необходимо добавить свой настраиваемый MappingJackson2HttpMessageConverter
, который использует _halObjectMapper
, чтобы вернуть JSON на основе HAL, тогда как тип носителя +json
поддерживается по умолчанию, возвращая ваш ресурс в старом добром JSON.
Если вы всегда хотите предоставить JSON на базе HAL, вы должны включить HAL в качестве типа JSON-Media по умолчанию (например, добавив настраиваемый MappingJackson2HttpMessageConverter
, который поддерживает тип носителя +json
и использует _halObjectMapper
, упомянутый выше), поэтому каждый запрос application/vnd.service.entity.v1+json
обрабатывается этим конвертером, возвращающим JSON на основе HAL.
С моей точки зрения, я считаю, что правильный путь состоит в том, чтобы различать только JSON и другие форматы, такие как XML и документацию вашего типа мультимедиа, которые вы сказали бы, что ваш JSON вдохновлен HAL таким образом, что клиенты могут использовать библиотеки HAL для проанализируйте ответы.
EDIT:
Чтобы обойти проблему, которую вам придется добавлять отдельно для каждого типа носителя, вы можете переопределить метод isCompatibleWith тип носителя, который вы добавляете в свой собственный MappingJackson2HttpMessageConverter
converter.setSupportedMediaTypes(Arrays.asList(
new MediaType("application", "doesntmatter") {
@Override
public boolean isCompatibleWith(final MediaType other) {
if (other == null) {
return false;
}
else if (other.getSubtype().startsWith("vnd.") && other.getSubtype().endsWith("+json")) {
return true;
}
return super.isCompatibleWith(other);
}
}
));