有 Java 编程相关的问题?

你可以在下面搜索框中键入要查询的问题!

由于HAL+JSON mediatype不够清晰,java无法使用Spring HATEOAS实现HAL+JSON级别3 RESTful API

Level 3RESTful API的特性是定制媒体类型,例如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后缀没有注册,因此违反了section 4.2.8 of RFC6838

现在Spring HATEOAS支持JSON开箱即用的链接,但对于HAL-JSON,您需要使用@EnableHypermediaSupport(type=EnableHypermediaSupport.HypermediaType.HAL)。在我的例子中,因为我使用的是Spring Boot,所以我将其附加到我的初始值设定项类(即扩展SpringBootServletInitializer的类)。但是Spring Boot无法识别我的定制媒体类型。因此,我必须弄清楚如何让它知道,它需要对application/vnd.service.entity.v1.hal+json形式的媒体类型使用HAL对象映射器

在我的第一次尝试中,我在Spring Boot初始值设定项中添加了以下内容:

@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相反(请参阅后面的解释)。我不喜欢这个解决方案,因为它会因为HAL问题而污染现有的JSON转换器。所以,我提出了一个不同的解决方案:

@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检查媒体类型兼容性,只会将以*+开头的媒体类型识别为子类型(例如application/*+json)的有效通配符媒体类型。这就是第一个解决方案起作用的原因(巧合)

所以这里有两个问题:

  • MimeType永远不会根据{}识别{}形式的特定于供应商的HAL媒体类型
  • MimeType将{}形式的供应商特定HAL媒体类型识别为application/*+hal+json然而根据section 4.2.8 of RFC6838这些是无效的模拟类型

似乎只有+hal被识别为有效后缀时才是正确的,在这种情况下,上面的第二个选项就可以了。否则,任何其他类型的通配符介质类型都无法专门识别特定于供应商的HAL介质类型。唯一的选择是用HAL问题覆盖现有的JSON消息转换器(参见第一个解决方案)

目前的另一个解决方法是,在为消息转换器创建支持的媒体类型列表时,指定您正在使用的每个自定义媒体类型。也就是说:

@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) 个答案

  1. # 1 楼答案

    虽然这个问题有点老了,但我最近偶然发现了同样的问题,所以我想在这个话题上花2美分

    我认为这里的问题在于HAL对JSON的理解。正如您已经指出的here,所有的HAL都是JSON,但并非所有的JSON都是HAL。根据我的理解,两者之间的区别在于,HAL定义了语义/结构的一些约定,比如告诉你,在像_links这样的属性后面,你会找到一些链接,而JSON只定义了像key: [value]这样的格式(正如@zeroplagl已经提到的)

    这就是为什么媒体类型被称为application/hal+json的原因。它基本上说是JSON格式的HAL风格/语义。这也是存在媒体类型application/hal+xmlsource )的原因

    现在,使用特定于供应商的媒体类型,您可以定义自己的语义,从而替换application/hal+json中的hal,而不扩展它

    如果我没弄错的话,你基本上想说,你有一个自定义的媒体类型,它使用HAL样式进行JSON格式化。(这样,客户机可以使用一些HAL库轻松解析JSON。)

    所以,最后,我认为你基本上必须决定你是否想要区分JSON和基于HAL的JSON,以及你的API是否应该提供其中一个或两者

    如果想同时提供这两种类型,就必须定义两种不同的媒体类型vnd.service.entity.v1.hal+jsonvnd.service.entity.v1+json。对于vnd.service.entity.v1.hal+json媒体类型,您必须添加自定义的MappingJackson2HttpMessageConverter,它使用_halObjectMapper返回基于HAL的JSON,而默认情况下,支持+json媒体类型以旧的JSON返回资源

    如果您总是想提供基于HAL的JSON,那么必须启用HAL作为默认的JSON媒体类型(例如,通过添加一个自定义的MappingJackson2HttpMessageConverter来支持+json媒体类型并使用前面提到的_halObjectMapper),因此对application/vnd.service.entity.v1+json的每个请求都由返回基于HAL的JSON的转换器处理

    从我的观点来看,我认为正确的方法是只区分JSON和其他格式,比如XML,在您的媒体类型文档中,您会说,您的JSON受HAL启发,客户可以使用HAL LIB解析响应。


    编辑:

    要绕过必须分别添加每个特定于供应商的介质类型的问题,可以覆盖要添加到自定义MappingJackson2HttpMessageConverter的介质类型的isCompatibleWith方法

    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);
                    }
                }
    ));