在Spring Boot中,通过扩展MappingJackson2HttpMessageConverter添加自定义转换器似乎会覆盖现有的转换器

时间:2014-11-05 16:12:14

标签: java spring rest spring-boot media-type

我正在尝试为application/vnd.custom.hal+json等自定义媒体类型创建转换器。我看到了这个答案here,但由于您无权访问AbstractHttpMessageConverter<T>(超级MappingJackson2HttpMessageConverter)的受保护构造函数,因此无法工作。这意味着以下代码不起作用:

class MyCustomVndConverter extends MappingJacksonHttpMessageConverter {
    public MyCustomVndConverter (){
        super(MediaType.valueOf("application/vnd.myservice+json"));
    }
}

但是,以下方法确实有效,并且基本上只是模仿构造函数实际执行的操作:

setSupportedMediaTypes(Collections.singletonList(
    MediaType.valueOf("application‌​/vnd.myservice+json")
));

所以我为我的班级做了这个,然后按照Spring Boot的文档here将转换器添加到我现有的转换器列表中。我的代码基本上是这样的:

//Defining the converter; the media-type is simply a custom media-type that is 
//still application/hal+json, i.e., JSON with some additional semantics on top 
//of what HAL already adds to JSON
public class TracksMediaTypeConverter extends MappingJackson2HttpMessageConverter {
    public TracksMediaTypeConverter() {
        setSupportedMediaTypes(Collections.singletonList(
            new MediaType("application‌​", "vnd.tracks.v1.hal+json")
        ));
    }
}

//Adding the message converter
@Configuration
@EnableSwagger
public class MyApplicationConfiguration {

    ...    
    @Bean
    public HttpMessageConverters customConverters() {
        return new HttpMessageConverters(new TracksMediaTypeConverter());
    }
}

根据文档,这应该有效。但我注意到,这会影响替换现有MappingJackson2HttpMessageCoverter,后者处理application/json;charset=UTF-8application/*+json;charset=UTF-8

我通过将调试器附加到我的应用程序并在Spring的AbstractMessageCoverterMethodProcessor.java类中逐步执行断点来验证这一点。在那里,私有字段messageConverters包含已注册的转换器列表。通常情况下,即如果我不尝试添加转换器,我会看到以下转换器:

    {li> MappingJackson2HttpMessageCoverter application/hal+json(我假设这是由我正在使用的Spring HATEOAS添加的)
  • ByteArrayHttpMessageConverter
  • StringHttpMessageConverter
  • ResourceHttpMessageConverter
  • SourceHttpMessageConverter
  • AllEncompassingFormHttpMessageConverter
  • MappingJackson2HttpMessageConverter代表application/json;charset=UTF-8application/*+json;charset=UTF-8
  • Jaxb2RootElementHttpMessageConverter

当我添加自定义媒体类型时,MappingJackson2HttpMessageConverter的第二个实例会被替换。也就是说,列表现在看起来像这样:

    {li> MappingJackson2HttpMessageConverter application/hal+json(我假设这是由我正在使用的Spring HATEOAS添加的)
  • ByteArrayHttpMessageConverter
  • StringHttpMessageConverter
  • ResourceHttpMessageConverter
  • SourceHttpMessageConverter
  • AllEncompassingFormHttpMessageConverter
  • {li> MappingJackson2HttpMessageConverter application/vnd.tracks.v1.hal+json(现有的已被替换)
  • Jaxb2RootElementHttpMessageConverter

我不完全确定为什么这种情况正在发生。我逐步完成了代码,唯一真正发生的事情就是调用MappingJackson2HttpMessageConverter的no-args构造函数(应该是这样),它最初将支持的媒体类型设置为application/json;charset=UTF-8和{ {1}}。之后,列表会被我提供的媒体类型覆盖。

我无法理解为什么添加此媒体类型应替换处理常规JSON的现有application/*+json;charset=UTF-8实例。是否有一些奇怪的魔法正在发生呢?

目前我有一个解决方法,但我不太喜欢它,因为它不那么优雅,它涉及MappingJackson2HttpMessageConverter中已有的代码重复。

我创建了以下类(仅显示常规MappingJackson2HttpMessageConverter的更改):

MappingJackson2HttpMessageConverter

然后我按如下方式使用这个类:

public abstract class ExtensibleMappingJackson2HttpMessageConverter<T> extends AbstractHttpMessageConverter<T> implements GenericHttpMessageConverter<T> {

    //These constructors are not available in `MappingJackson2HttpMessageConverter`, so
    //I provided them here just for convenience.    

    /**
     * Construct an {@code AbstractHttpMessageConverter} with no supported media types.
     * @see #setSupportedMediaTypes
     */
    protected ExtensibleMappingJackson2HttpMessageConverter() {
    }

    /**
     * Construct an {@code ExtensibleMappingJackson2HttpMessageConverter} with one supported media type.
     * @param supportedMediaType the supported media type
     */
    protected ExtensibleMappingJackson2HttpMessageConverter(MediaType supportedMediaType) {
        setSupportedMediaTypes(Collections.singletonList(supportedMediaType));
    }

    /**
     * Construct an {@code ExtensibleMappingJackson2HttpMessageConverter} with multiple supported media type.
     * @param supportedMediaTypes the supported media types
     */
    protected ExtensibleMappingJackson2HttpMessageConverter(MediaType... supportedMediaTypes) {
        setSupportedMediaTypes(Arrays.asList(supportedMediaTypes));
    }

    ...

    //These return Object in MappingJackson2HttpMessageConverter because it extends
    //AbstractHttpMessageConverter<Object>. Now these simply return an instance of
    //the generic type. 

    @Override
    protected T readInternal(Class<? extends T> clazz, HttpInputMessage inputMessage)
            throws IOException, HttpMessageNotReadableException {

        JavaType javaType = getJavaType(clazz, null);
        return readJavaType(javaType, inputMessage);
    }

    @Override
    public T read(Type type, Class<?> contextClass, HttpInputMessage inputMessage)
            throws IOException, HttpMessageNotReadableException {

        JavaType javaType = getJavaType(type, contextClass);
        return readJavaType(javaType, inputMessage);
    }

    private T readJavaType(JavaType javaType, HttpInputMessage inputMessage) {
        try {
            return this.objectMapper.readValue(inputMessage.getBody(), javaType);
        }
        catch (IOException ex) {
            throw new HttpMessageNotReadableException("Could not read JSON: " + ex.getMessage(), ex);
        }
    }

    ...

}

配置类中转换器的注册与以前相同。通过这些更改,public class TracksMediaTypeConverter extends ExtensibleMappingJackson2HttpMessageConverter<Tracks> { public TracksMediaTypeConverter() { super(new MediaType("application", "application/vnd.tracks.v1.hal+json")); } } 的现有实例不会被覆盖,并且所有内容都可以正常运行。

所以为了把一切都烧掉,我有两个问题:

  • 为什么在扩展MappingJackson2HttpMessageConverter
  • 时会覆盖现有的转换器
  • 创建自定义媒体类型转换器的正确方法是什么,该转换器表示基本上仍然是JSON的语义媒体类型(因此可以通过MappingJackson2HttpMessageConverter进行序列化和反序列化?

2 个答案:

答案 0 :(得分:7)

已在最新版本

中修复

不确定何时修复此问题,但自1.1.8.RELEASE起,此问题不再存在,因为它使用ClassUtils.isAssignableValue。在此留下原始答案仅供参考。


这里似乎存在多个问题,因此我将总结我的发现作为答案。我仍然没有真正解决我想要做的事情,但我会和Spring Boot的人谈谈,看看是否有意。

为什么在扩展MappingJackson2HttpMessageConverter时会覆盖现有转换器?

这适用于Spring Boot的1.1.4.RELEASE版本;我还没有检查过其他版本。 HttpMessageConverters类的构造函数如下:

/**
 * Create a new {@link HttpMessageConverters} instance with the specified additional
 * converters.
 * @param additionalConverters additional converters to be added. New converters will
 * be added to the front of the list, overrides will replace existing items without
 * changing the order. The {@link #getConverters()} methods can be used for further
 * converter manipulation.
 */
public HttpMessageConverters(Collection<HttpMessageConverter<?>> additionalConverters) {
    List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();
    List<HttpMessageConverter<?>> defaultConverters = getDefaultConverters();
    for (HttpMessageConverter<?> converter : additionalConverters) {
        int defaultConverterIndex = indexOfItemClass(defaultConverters, converter);
        if (defaultConverterIndex == -1) {
            converters.add(converter);
        }
        else {
            defaultConverters.set(defaultConverterIndex, converter);
        }
    }
    converters.addAll(defaultConverters);
    this.converters = Collections.unmodifiableList(converters);
}

for循环内。请注意,它通过调用indexOfItemClass方法确定列表中的索引。该方法如下所示:

private <E> int indexOfItemClass(List<E> list, E item) {
    Class<? extends Object> itemClass = item.getClass();
    for (int i = 0; i < list.size(); i++) {
        if (list.get(i).getClass().isAssignableFrom(itemClass)) {
            return i;
        }
    }
    return -1;
}

由于我的课程扩展MappingJackson2HttpMessageConverterif语句会返回true。这意味着在构造函数中,我们有一个有效的索引。 Spring Boot然后用新的实例替换现有实例,完全我所看到的。

这是理想的行为吗?

我不知道。它似乎似乎并且对我来说似乎很奇怪。

是否在Spring Boot文档中明确地调用了它?

排序。见here。它说:

  

上下文中存在的任何HttpMessageConverter bean都将添加到转换器列表中。您也可以通过这种方式覆盖默认转换器。

但是,仅仅因为它是现有转换器的子类型而覆盖转换器并不是一种有用的行为。

Spring HATEOAS如何解决Spring Boot问题?

Spring HATEOAS&#39;生命周期与Spring Boot是分开的。 Spring HATEOAS在application/hal+json类中为HyperMediaSupportBeanDefinitionRegistrar媒体类型注册其处理程序。相关方法是:

private List<HttpMessageConverter<?>> potentiallyRegisterModule(List<HttpMessageConverter<?>> converters) {

    for (HttpMessageConverter<?> converter : converters) {
        if (converter instanceof MappingJackson2HttpMessageConverter) {
            MappingJackson2HttpMessageConverter halConverterCandidate = (MappingJackson2HttpMessageConverter) converter;
            ObjectMapper objectMapper = halConverterCandidate.getObjectMapper();
            if (Jackson2HalModule.isAlreadyRegisteredIn(objectMapper)) {
                return converters;
            }
        }
    }

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

    MappingJackson2HttpMessageConverter halConverter = new MappingJackson2HttpMessageConverter();
    halConverter.setSupportedMediaTypes(Arrays.asList(HAL_JSON)); //HAL_JSON is just a MediaType instance for application/hal+json
    halConverter.setObjectMapper(halObjectMapper);

    List<HttpMessageConverter<?>> result = new ArrayList<HttpMessageConverter<?>>(converters.size());
    result.add(halConverter);
    result.addAll(converters);
    return result;
}

converters参数通过此片段从同一个类的postProcessBeforeInitialization方法传入。相关摘录是:

if (bean instanceof RequestMappingHandlerAdapter) {
    RequestMappingHandlerAdapter adapter = (RequestMappingHandlerAdapter) bean;
    adapter.setMessageConverters(potentiallyRegisterModule(adapter.getMessageConverters()));
}

创建自定义媒体类型转换器的正确方法是什么,该转换器表示基本上仍然是JSON的语义媒体类型(因此可以通过MappingJackson2HttpMessageConverter进行序列化和反序列化?

我不确定。子类ExtensibleMappingJackson2HttpMessageConverter<T>(在问题中显示)暂时有效。另一种选择可能是在自定义转换器中创建MappingJackson2HttpMessageConverter的私有实例,并简单地委托给它。无论哪种方式,我将打开Spring Boot项目的问题并从他们那里获得一些反馈。然后,我会使用任何新信息更新答案。

答案 1 :(得分:1)

Spring启动文档明确指出添加自定义MappingJackson2HttpMessageConverter会替换默认值。

来自docs

  

最后,如果您提供类型@Beans的任何MappingJackson2HttpMessageConverter,那么它们将替换MVC配置中的默认值。此外,提供类型为HttpMessageConverters的便捷bean(如果使用默认的MVC配置,则始终可用),它具有一些有用的方法来访问默认和用户增强的消息转换器。