В 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 здесь. Мой код в основном выглядит так:

//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 содержит список зарегистрированных конвертеров. Нормально, т.е. если я не пытаюсь добавить свой конвертер, то вижу следующие конвертеры:

  • MappingJackson2HttpMessageCoverter для application/hal+json (я предполагаю, что это добавлено Spring HATEOAS, который я использую)
  • ByteArrayHttpMessageConverter
  • StringHttpMessageConverter
  • ResourceHttpMessageConverter
  • SourceHttpMessageConverter
  • AllEncompassingFormHttpMessageConverter
  • MappingJackson2HttpMessageConverter для application/json;charset=UTF-8 и application/*+json;charset=UTF-8
  • Jaxb2RootElementHttpMessageConverter

Когда я добавляю свой пользовательский тип мультимедиа, второй экземпляр MappingJackson2HttpMessageConverter заменяется. То есть список теперь выглядит так:

  • MappingJackson2HttpMessageConverter для application/hal+json (я предполагаю, что это добавлено Spring HATEOAS, который я использую)
  • ByteArrayHttpMessageConverter
  • StringHttpMessageConverter
  • ResourceHttpMessageConverter
  • SourceHttpMessageConverter
  • AllEncompassingFormHttpMessageConverter
  • MappingJackson2HttpMessageConverter на application/vnd.tracks.v1.hal+json (существующий заменен)
  • Jaxb2RootElementHttpMessageConverter

Я не совсем понимаю, почему это происходит. Я прошел через код, и единственное, что действительно происходит, это то, что вызывается конструктор без аргументов MappingJackson2HttpMessageConverter (как и должно быть), который изначально устанавливает поддерживаемые типы мультимедиа в application/json;charset=UTF-8 и application/*+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?

person Vivin Paliath    schedule 05.11.2014    source источник


Ответы (2)


Исправлено в последней версии

Не уверен, когда это было исправлено, но с 1.1.8.RELEASE этой проблемы больше не существует, так как она использует ClassUtils.isAssignableValue. Оставив исходный ответ здесь только для информации.


Кажется, здесь есть несколько проблем, поэтому я собираюсь обобщить свои выводы в качестве ответа. У меня до сих пор нет решения для того, что я пытаюсь сделать, но я собираюсь поговорить с людьми из Spring Boot, чтобы узнать, задумано ли то, что происходит, или нет.

Почему существующий преобразователь перезаписывается при расширении MappingJackson2HttpMessageConverter?

Это относится к версии 1.1.4.RELEASE Spring 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+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 и получить от них отзывы. Затем я обновлю ответ с любой новой информацией.

person Vivin Paliath    schedule 05.11.2014