一尘不染

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

spring-boot

我正在尝试为的自定义媒体类型创建转换器application/vnd.custom.hal+json。我在这里看到了这个答案,但由于您无权访问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引导的文档添加的转换器我现有的转换器的列表在这里。我的代码基本上如下所示:

//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-8和处理的效果application/*+json;charset=UTF-8

我通过将调试器附加到应用程序并逐步遍历Spring
AbstractMessageCoverterMethodProcessor.java类中的断点来验证了这一点。在此,专用字段messageConverters包含已注册的转换器的列表。通常,即,如果我不尝试添加转换器,则会看到以下掩盖内容:

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

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

  • MappingJackson2HttpMessageConverterapplication/hal+json(我假设这是我正在使用的Spring HATEOAS添加的)
  • ByteArrayHttpMessageConverter
  • StringHttpMessageConverter
  • ResourceHttpMessageConverter
  • SourceHttpMessageConverter
  • AllEncompassingFormHttpMessageConverter
  • MappingJackson2HttpMessageConverterapplication/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);
        }
    }

    ...

}

然后,按如下方式使用此类:

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

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

因此,将所有内容归结为两个问题:

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

阅读 363

收藏
2020-05-30

共1个答案

一尘不染

已在最新版本中修复

不确定何时解决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);
}

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

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

这是理想的行为吗?

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

在Spring Boot文档中的任何地方都明确指出了这一点吗?

有点。看这里。它说:

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

但是,仅仅因为转换器是现有转换器的子类型而对其进行覆盖似乎并不有用。

Spring HATEOAS如何解决这个Spring Boot问题?

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

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项目中打开一个问题,并从中获得一些反馈。然后,我将使用任何新信息为答案进行更新。

2020-05-30