我正在尝试为application/vnd.custom.hal+json
等自定义媒体类型创建转换器。我看到了这个答案here,但由于您无权访问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的文档here将转换器添加到我现有的转换器列表中。我的代码基本上是这样的:
//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
的no-args构造函数(应该是这样),它最初将支持的媒体类型设置为application/json;charset=UTF-8
和{ {1}}。之后,列表会被我提供的媒体类型覆盖。
我无法理解为什么添加此媒体类型应替换处理常规JSON的现有application/*+json;charset=UTF-8
实例。是否有一些奇怪的魔法正在发生呢?
目前我有一个解决方法,但我不太喜欢它,因为它不那么优雅,它涉及MappingJackson2HttpMessageConverter
中已有的代码重复。
我创建了以下类(仅显示常规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
进行序列化和反序列化?答案 0 :(得分:7)
不确定何时修复此问题,但自1.1.8.RELEASE
起,此问题不再存在,因为它使用ClassUtils.isAssignableValue
。在此留下原始答案仅供参考。
这里似乎存在多个问题,因此我将总结我的发现作为答案。我仍然没有真正解决我想要做的事情,但我会和Spring Boot的人谈谈,看看是否有意。
MappingJackson2HttpMessageConverter
时会覆盖现有转换器? 这适用于Spring Boot的1.1.4.RELEASE
版本;我还没有检查过其他版本。 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然后用新的实例替换现有实例,完全我所看到的。
我不知道。它似乎似乎并且对我来说似乎很奇怪。
排序。见here。它说:
上下文中存在的任何
HttpMessageConverter bean
都将添加到转换器列表中。您也可以通过这种方式覆盖默认转换器。
但是,仅仅因为它是现有转换器的子类型而覆盖转换器并不是一种有用的行为。
Spring HATEOAS&#39;生命周期与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()));
}
MappingJackson2HttpMessageConverter
进行序列化和反序列化?我不确定。子类ExtensibleMappingJackson2HttpMessageConverter<T>
(在问题中显示)暂时有效。另一种选择可能是在自定义转换器中创建MappingJackson2HttpMessageConverter
的私有实例,并简单地委托给它。无论哪种方式,我将打开Spring Boot项目的问题并从他们那里获得一些反馈。然后,我会使用任何新信息更新答案。
答案 1 :(得分:1)
Spring启动文档明确指出添加自定义MappingJackson2HttpMessageConverter
会替换默认值。
来自docs:
最后,如果您提供类型
@Beans
的任何MappingJackson2HttpMessageConverter
,那么它们将替换MVC配置中的默认值。此外,提供类型为HttpMessageConverters
的便捷bean(如果使用默认的MVC配置,则始终可用),它具有一些有用的方法来访问默认和用户增强的消息转换器。