在Spring Boot中,通过扩展MappingJackson2HttpMessageConverter添加自定义转换器似乎覆盖了现有的转换器 [英] In Spring Boot, adding a custom converter by extending MappingJackson2HttpMessageConverter seems to overwrite the existing converter

查看:6729
本文介绍了在Spring Boot中,通过扩展MappingJackson2HttpMessageConverter添加自定义转换器似乎覆盖了现有的转换器的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试为自定义媒体类型创建转换器,例如 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 Boot的文档这里。我的代码基本上是这样的:

  //定义转换器; media-type只是一个自定义媒体类型,
//仍然是application / hal + json,即JSON在顶部
//上有一些额外的语义// HAL已经添加到JSON
public class TracksMediaTypeConverter extends MappingJackson2HttpMessageConverter {
public TracksMediaTypeConverter(){
setSupportedMediaTypes(Collections.singletonList(
new MediaType(application,vnd.tracks.v1.hal + json) )
));
}
}

//添加消息转换器
@Configuration
@EnableSwagger
公共类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 包含已注册的转换器列表。通常,即,如果我不尝试添加我的转换器,我会看到以下转换器:




  • MappingJackson2HttpMessageCoverter for application / hal + json (我假设这是由我正在使用的Spring HATEOAS添加的)

  • ByteArrayHttpMessageConverter

  • StringHttpMessageConverter

  • ResourceHttpMessageConverter

  • SourceHttpMessageConverter

  • AllEncompassingFormHttpMessageConverter

  • MappingJackson2HttpMessageConverter for application / json; charset = UTF-8 application / * + json; charset = UTF-8

  • Jaxb2RootElementHttpMessageConverter



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




  • MappingJackson2HttpMessageConverter for application / hal + json (我假设这是由我正在使用的Spring HATEOAS添加的)

  • ByteArrayHttpMessageConverter

  • StringHttpMessageConverter

  • ResourceHttpMessageConverter

  • SourceHttpMessageConverter

  • AllEncompassingFormHttpMessageConverter

  • MappingJackson2HttpMessageConverter for application / vnd.tracks.v1.hal + json (现有的已被替换)

  • Jaxb2RootElementHttpMessageConverter



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



我无法理解为什么添加此媒体类型替换处理常规JSON的现有 MappingJackson2HttpMessageConverter 实例。是否有一些奇怪的魔法正在发生呢?



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



我创建了以下类(仅从常规<$ c更改)显示$ c> MappingJackson2HttpMessageConverter :

 公共抽象类ExtensibleMappingJackson2HttpMessageConverter< T>扩展AbstractHttpMessageConverter< T>实现GenericHttpMessageConverter< T> {

//这些构造函数在`MappingJackson2HttpMessageConverter`中不可用,所以
//我在这里提供它们只是为了方便起见。

/ **
*构造一个没有支持的媒体类型的{@code AbstractHttpMessageConverter}。
* @see #setSupportedMediaTypes
* /
protected ExtensibleMappingJackson2HttpMessageConverter(){
}

/ **
*构造一个{@code ExtensibleMappingJackson2HttpMessageConverter}具有一种支持的媒体类型。
* @param supportedMediaType支持的媒体类型
* /
protected ExtensibleMappingJackson2HttpMessageConverter(MediaType supportedMediaType){
setSupportedMediaTypes(Collections.singletonList(supportedMediaType));
}

/ **
*使用多种支持的媒体类型构建{@code ExtensibleMappingJackson2HttpMessageConverter}。
* @param supportedMediaTypes支持的媒体类型
* /
protected ExtensibleMappingJackson2HttpMessageConverter(MediaType ... supportedMediaTypes){
setSupportedMediaTypes(Arrays.asList(supportedMediaTypes));
}

...

//这些返回MappingJackson2HttpMessageConverter中的Object,因为它扩展了
// AbstractHttpMessageConverter< Object>。现在这些只返回
//泛型的实例。

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

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

@Override
public T read(类型类型,类<?> contextClass,HttpInputMessage inputMessage)
抛出IOException,HttpMessageNotReadableException {

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

private T readJavaType(JavaType javaType,HttpInputMessage inputMessage){
try {
return this.objectMapper.readValue(inputMessage.getBody(),javaType);
}
catch(IOException ex){
抛出新的HttpMessageNotReadableException(无法读取JSON:+ ex.getMessage(),ex);
}
}

...

}

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

 公共类TracksMediaTypeConverter扩展ExtensibleMappingJackson2HttpMessageConverter< Tracks> {
public TracksMediaTypeConverter(){
super(new MediaType(application,application / vnd.tracks.v1.hal + json));
}
}

配置类中转换器的注册是和之前一样。通过这些更改, MappingJackson2HttpMessageConverter 的现有实例不会被覆盖,一切都按预期工作。



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




  • 当我扩展<$>为什么现有的转换器会被覆盖c $ c> MappingJackson2HttpMessageConverter ?

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


解决方案<进行序列化和反序列化/ div>

已在最新版本中修复



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






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



当我扩展 MappingJackson2HttpMessageConverter

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

  / ** 
*使用指定的额外
*转换器创建一个新的{@link HttpMessageConverters}实例。
* @param additionalConverters要添加的其他转换器。新的转换器将
*添加到列表的前面,覆盖将替换现有的项目而不用
*更改订单。 {@link #getConverters()}方法可用于进一步的
*转换器操作。
* /
public HttpMessageConverters(Collection< HttpMessageConverter<?> additionalConverters){
List< HttpMessageConverter<?>> converters = new ArrayList< HttpMessageConverter<?>>();
列表< 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(converter);
}

环。请注意,它通过调用 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)){
回归我;
}
}
返回-1;
}

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



是这个理想的行为?



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



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



排序。请参见此处。它说:


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


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



Spring HATEOAS如何解决Spring Boot问题?



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

  private List< HttpMessageConverter<?>> potentialRegisterModule(List< HttpMessageConverter<?>>转换器){

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

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只是application / hal + json
halConverter.setObjectMapper(halObjectMapper)的MediaType实例;

列表< HttpMessageConverter<?>> result = new ArrayList< HttpMessageConverter<?>>(converters.size());
result.add(halConverter);
result.addAll(converter);
返回结果;
}

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

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



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



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


I'm trying to create a converter for a custom media-type like application/vnd.custom.hal+json. I saw this answer here, but it won't work since you don't have access to the protected constructor of AbstractHttpMessageConverter<T> (super class of MappingJackson2HttpMessageConverter). Which means that the following code does not work:

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

However, the following does work and basically just mimics what the constructor actually does anyway:

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

So I did this for my class, and then added the converter to my existing list of converters by following Spring Boot's documentation here. My code basically looks like this:

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

As per the documentation, this should work. But what I noticed is that this has the effect of replacing the existing MappingJackson2HttpMessageCoverter, which handles application/json;charset=UTF-8 and application/*+json;charset=UTF-8.

I verified this by attaching a debugger to my app and stepping through breakpoints inside Spring's AbstractMessageCoverterMethodProcessor.java class. There, the private field messageConverters contains the list of converters that have been registered. Normally, i.e., if I do not try to add my converter, I see the following coverters:

  • MappingJackson2HttpMessageCoverter for application/hal+json (I'm assuming this is added by Spring HATEOAS, which I am using)
  • ByteArrayHttpMessageConverter
  • StringHttpMessageConverter
  • ResourceHttpMessageConverter
  • SourceHttpMessageConverter
  • AllEncompassingFormHttpMessageConverter
  • MappingJackson2HttpMessageConverter for application/json;charset=UTF-8 and application/*+json;charset=UTF-8
  • Jaxb2RootElementHttpMessageConverter

When I add my custom media type, the second instance of MappingJackson2HttpMessageConverter gets replaced. That is, the list now looks like this:

  • MappingJackson2HttpMessageConverter for application/hal+json (I'm assuming this is added by Spring HATEOAS, which I am using)
  • ByteArrayHttpMessageConverter
  • StringHttpMessageConverter
  • ResourceHttpMessageConverter
  • SourceHttpMessageConverter
  • AllEncompassingFormHttpMessageConverter
  • MappingJackson2HttpMessageConverter for application/vnd.tracks.v1.hal+json (the existing one has been replaced)
  • Jaxb2RootElementHttpMessageConverter

I'm not entirely sure why this is happening. I stepped through the code and the only thing that really happens is that the no-args constructor of MappingJackson2HttpMessageConverter is called (as it should be), which initially sets the supported media-types to application/json;charset=UTF-8 and application/*+json;charset=UTF-8. After that, the list gets overwritten with the media-type that I provide.

What I cannot understand is why adding this media type should replace the existing instance of MappingJackson2HttpMessageConverter that handles regular JSON. Is there some strange magic that is going on that does this?

Currently I have a workaround, but I don't like it very much since it's not that elegant and it involves duplication of code already in MappingJackson2HttpMessageConverter.

I created the following class (only changes from the regular MappingJackson2HttpMessageConverter are shown):

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

    ...

}

I then use this class as follows:

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

The registration of the converter in the configuration class is the same as before. With these changes, the existing instance of MappingJackson2HttpMessageConverter is not overwritten and everything works as I would expect.

So to boil everything down, I have two questions:

  • Why is the existing converter being overwritten when I extend MappingJackson2HttpMessageConverter?
  • What is the right way to create a custom media-type converter that represents a semantic media-type that is still basically JSON (and therefore can be serialized and deserialized by MappingJackson2HttpMessageConverter?

解决方案

Fixed in the latest version

Not sure when this was fixed, but as of 1.1.8.RELEASE, this problem no-longer exists since it is using ClassUtils.isAssignableValue. Leaving the original answer here just for information.


There seem to be multiple issues at play here, so I'm going to summarize my findings as the answer. I still don't really have a solution for what I'm trying to do, but I'm going to talk to the Spring Boot folks to see if what's happening is intended or not.

Why is the existing converter being overwritten when I extend MappingJackson2HttpMessageConverter?

This applies to version 1.1.4.RELEASE of Spring Boot; I haven't checked other versions. The constructor of the HttpMessageConverters class is as follows:

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

Inside the for loop. Notice that it determines an index in the list by calling the indexOfItemClass method. That method looks like this:

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

Since my class extends MappingJackson2HttpMessageConverter the if statement returns true. This means that in the constructor, we have a valid index. Spring Boot then replaces the existing instance with the new one, which is exactly what I am seeing.

Is this desirable behavior?

I don't know. It doesn't seem to be and seems very strange to me.

Is this called out explicitly in Spring Boot documentation anywhere?

Sort of. See here. It says:

Any HttpMessageConverter bean that is present in the context will be added to the list of converters. You can also override default converters that way.

However, overriding a converter simply because it is a subtype of an existing one doesn't seem like helpful behavior.

How does Spring HATEOAS get around this Spring Boot issue?

Spring HATEOAS' lifecycle is separate from Spring Boot. Spring HATEOAS registers its handler for the application/hal+json media-type in the HyperMediaSupportBeanDefinitionRegistrar class. The relevant method is:

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

The converters argument is passed-in via this snippet from the postProcessBeforeInitialization method from the same class. Relevant snippet is:

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

What is the right way to create a custom media-type converter that represents a semantic media-type that is still basically JSON (and therefore can be serialized and deserialized by MappingJackson2HttpMessageConverter?

I'm not sure. Sub-classing ExtensibleMappingJackson2HttpMessageConverter<T> (shown in the question) works for the time being. Another option would perhaps be to create a private instance of MappingJackson2HttpMessageConverter inside your custom converter, and simply delegate to that. Either way, I am going to open an issue with the Spring Boot project and get some feedback from them. I'll then update with answer with any new information.

这篇关于在Spring Boot中,通过扩展MappingJackson2HttpMessageConverter添加自定义转换器似乎覆盖了现有的转换器的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆