Я пытаюсь создать конвертер для пользовательского типа мультимедиа, такого как 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
?