在Spring Boot中,通过扩展MappingJackson2HttpMessageConverter添加自定义转换器似乎覆盖了现有的转换器 [英] In Spring Boot, adding a custom converter by extending MappingJackson2HttpMessageConverter seems to overwrite the existing converter
问题描述
我正在尝试为自定义媒体类型创建转换器,例如 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
forapplication / hal + json
(我假设这是由我正在使用的Spring HATEOAS添加的) -
ByteArrayHttpMessageConverter
-
StringHttpMessageConverter
-
ResourceHttpMessageConverter
-
SourceHttpMessageConverter
-
AllEncompassingFormHttpMessageConverter
-
MappingJackson2HttpMessageConverter
forapplication / json; charset = UTF-8
和application / * + json; charset = UTF-8
-
Jaxb2RootElementHttpMessageConverter
当我添加自定义媒体类型时,第二个实例 MappingJackson2HttpMessageConverter
被替换。也就是说,列表现在看起来像这样:
-
MappingJackson2HttpMessageConverter
forapplication / hal + json
(我假设这是由我正在使用的Spring HATEOAS添加的) -
ByteArrayHttpMessageConverter
-
StringHttpMessageConverter
-
ResourceHttpMessageConverter
-
SourceHttpMessageConverter
-
AllEncompassingFormHttpMessageConverter
-
MappingJackson2HttpMessageConverter
forapplication / 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
?
已在最新版本中修复
不确定何时修复,但从 1.1.8.RELEASE $开始c $ c>,此问题不再存在,因为它使用
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
forapplication/hal+json
(I'm assuming this is added by Spring HATEOAS, which I am using)ByteArrayHttpMessageConverter
StringHttpMessageConverter
ResourceHttpMessageConverter
SourceHttpMessageConverter
AllEncompassingFormHttpMessageConverter
MappingJackson2HttpMessageConverter
forapplication/json;charset=UTF-8
andapplication/*+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
forapplication/hal+json
(I'm assuming this is added by Spring HATEOAS, which I am using)ByteArrayHttpMessageConverter
StringHttpMessageConverter
ResourceHttpMessageConverter
SourceHttpMessageConverter
AllEncompassingFormHttpMessageConverter
MappingJackson2HttpMessageConverter
forapplication/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屋!