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

我正在尝试为自定义媒体类型创建转换器,如application/vnd.custom.hal+json 。 我在这里看到了这个答案,但由于您无法访问AbstractHttpMessageConverterMappingJackson2HttpMessageConverter超类)的受保护构造函数,因此无法正常工作。 这意味着以下代码不起作用:

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

但是,以下方法确实有效,并且基本上只是模仿构造函数实际执行的操作:

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

所以我为我的class级做了这个,然后按照Spring Boot的文档将转换器添加到我现有的转换器列表中。 我的代码基本上是这样的:

 //Defining the converter; the media-type is simply a custom media-type that is //still application/hal+json, ie, 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-8application/*+json;charset=UTF-8

我通过将调试器附加到我的应用程序并在Spring的AbstractMessageCoverterMethodProcessor.java类中逐步执行断点来validation这一点。 在那里,私有字段messageConverters包含已注册的转换器列表。 通常,即如果我不尝试添加转换器,我会看到以下转换器:

  • MappingJackson2HttpMessageCoverter for application/hal+json (我假设这是由Spring HATEOAS添加的,我正在使用)
  • ByteArrayHttpMessageConverter
  • StringHttpMessageConverter
  • ResourceHttpMessageConverter
  • SourceHttpMessageConverter
  • AllEncompassingFormHttpMessageConverter
  • MappingJackson2HttpMessageConverter for application/json;charset=UTF-8 and 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就是调用MappingJackson2HttpMessageConverter的no-args构造函数(应该是这样),它最初将支持的媒体类型设置为application/json;charset=UTF-8application/*+json;charset=UTF-8 。 之后,列表将被我提供的媒体类型覆盖。

我无法理解的是,为什么添加此媒体类型应该替换处理常规JSON的现有MappingJackson2HttpMessageConverter实例。 这有什么奇怪的魔法吗?

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

我创建了以下类(仅显示常规MappingJackson2HttpMessageConverter更改):

 public abstract class ExtensibleMappingJackson2HttpMessageConverter extends AbstractHttpMessageConverter implements GenericHttpMessageConverter { //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. Now these simply return an instance of //the generic type. @Override protected T readInternal(Class 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 { public TracksMediaTypeConverter() { super(new MediaType("application", "application/vnd.tracks.v1.hal+json")); } } 

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

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

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

已在最新版本中修复

不确定何时修复,但从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> additionalConverters) { List> converters = new ArrayList>(); List> 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  int indexOfItemClass(List list, E item) { Class 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在HyperMediaSupportBeanDefinitionRegistrar类中为application/hal+json media-type注册其处理程序。 相关方法是:

 private List> potentiallyRegisterModule(List> 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> result = new ArrayList>(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 (在问题中显示)暂时有效。 另一种选择可能是在自定义转换器中创建MappingJackson2HttpMessageConverter的私有实例,并简单地委托给它。 无论哪种方式,我将打开Spring Boot项目的问题并从中获得一些反馈。 然后,我将使用任何新信息更新答案。

Spring启动文档明确指出添加自定义MappingJackson2HttpMessageConverter替换默认值。

来自docs :

最后,如果您提供任何类型为@BeansMappingJackson2HttpMessageConverter那么它们将替换MVC配置中的默认值。 此外,提供类型为HttpMessageConverters的便捷bean(如果使用默认的MVC配置,则始终可用),它具有访问默认和用户增强的消息转换器的一些有用方法。