Невозможно выполнить 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);
                }
            }
));