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

Viv*_*ath 5 java rest spring media-type spring-boot

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

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

但是,以下工作确实有效,基本上只是模仿构造函数实际上做了什么:

setSupportedMediaTypes(Collections.singletonList(
    MediaType.valueOf("application??/vnd.myservice+json")
));
Run Code Online (Sandbox Code Playgroud)

所以,我这样做是为了我的课,然后通过下面的Spring引导的文档添加的转换器我现有的转换器的列表在这里.我的代码基本上是这样的:

//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());
    }
}
Run Code Online (Sandbox Code Playgroud)

根据文档,这应该工作.但我注意到的是,它具有替换现有的MappingJackson2HttpMessageCoverter,处理application/json;charset=UTF-8和处理的效果application/*+json;charset=UTF-8.

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

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

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

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

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

我无法理解的是,为什么添加此媒体类型应该替换MappingJackson2HttpMessageConverter处理常规JSON 的现有实例.这有什么奇怪的魔法吗?

目前我有一个解决方法,但我不太喜欢它,因为它不是那么优雅,它涉及已经存在的代码重复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);
        }
    }

    ...

}
Run Code Online (Sandbox Code Playgroud)

然后我使用这个类如下:

public class TracksMediaTypeConverter extends ExtensibleMappingJackson2HttpMessageConverter<Tracks> {
    public TracksMediaTypeConverter() {
        super(new MediaType("application", "application/vnd.tracks.v1.hal+json"));
    }
}
Run Code Online (Sandbox Code Playgroud)

转换器在配置类中的注册与以前相同.通过这些更改,现有的实例MappingJackson2HttpMessageConverter 不会被覆盖,一切都按照我的预期运行.

所以要把所有东西都烧掉,我有两个问题:

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

Viv*_*ath 7

已在最新版本中修复

不知道什么时候这个问题已经修复,但是1.1.8.RELEASE由于它正在使用,这个问题不再存在ClassUtils.isAssignableValue.在此留下原始答案仅供参考.


这里似乎有多个问题,所以我将总结我的发现作为答案.我仍然没有为我正在尝试做的事情提供解决方案,但是我将与Spring Boot的人员讨论是否有意发生了什么.

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

这适用于1.1.4.RELEASESpring Boot的版本; 我还没有检查过其他版本.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);
}
Run Code Online (Sandbox Code Playgroud)

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;
}
Run Code Online (Sandbox Code Playgroud)

由于我的类扩展MappingJackson2HttpMessageConverterif语句返回true.这意味着在构造函数中,我们有一个有效的索引.Spring Boot然后新的实例替换现有的实例,这正是我所看到的.

这是理想的行为吗?

我不知道.它不似乎是,似乎很奇怪,我.

这是在任何地方的Spring Boot文档中明确调出的吗?

有点.看到这里.它说:

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

但是,仅仅因为它是现有转换器的子类型而重写转换器似乎不是有用的行为.

Spring HATEOAS如何解决Spring Boot问题?

Spring HATEOAS的生命周期与Spring Boot是分开的.Spring HATEOAS application/hal+jsonHyperMediaSupportBeanDefinitionRegistrar类中为media-type 注册其处理程序.相关方法是:

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;
}
Run Code Online (Sandbox Code Playgroud)

converters参数通过此片段从postProcessBeforeInitialization同一个类的方法传入.相关代码段是:

if (bean instanceof RequestMappingHandlerAdapter) {
    RequestMappingHandlerAdapter adapter = (RequestMappingHandlerAdapter) bean;
    adapter.setMessageConverters(potentiallyRegisterModule(adapter.getMessageConverters()));
}
Run Code Online (Sandbox Code Playgroud)

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

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