Suppose I have a controller that serves GET
request and returns bean to be serialized to JSON and also provides an exception handler for IllegalArgumentEx
There are several aspects relating to the problem:
StringHttpMessageConverter
adds catch-all mime type */*
to the list of supported media types, while MappingJackson2HttpMessageConverter
is bound to application/json
only.@RequestMapping
is providing produces = ...
, this value is stored in HttpServletRequest
(see RequestMappingInfoHandlerMapping.handleMatch()
) and when the error handler is called, this mime type is automatically inherited and used.The solution in simple case would be to put StringHttpMessageConverter
first in the list:
<mvc:annotation-driven>
<mvc:message-converters>
<bean class="org.springframework.http.converter.StringHttpMessageConverter">
<property name="supportedMediaTypes">
<array>
<util:constant static-field="org.springframework.http.MediaType.TEXT_PLAIN" />
</array>
</property>
</bean>
<bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter" />
</mvc:message-converters>
</mvc:annotation-driven>
and also remove produces
from @RequestMapping
annotation:
@RequestMapping(value = "/meta/{itemId}", method = RequestMethod.GET)
@ResponseBody
public MetaInformation getMetaInformation(@PathVariable int itemId) {
return myService.getMetaInformation(itemId);
}
Now:
StringHttpMessageConverter
will discard all types, which only MappingJackson2HttpMessageConverter
can handle (MetaInformation
, java.util.Collection
, etc) allowing them to be passed further.StringHttpMessageConverter
will take the precedence.So far so good, but unfortunately things get more complicated with ObjectToStringHttpMessageConverter. For handler return type java.util.Collection<MetaInformation>
this message convertor will report that it can convert this type to java.lang.String
. The limitation comes from the fact that collection element types are erased and AbstractHttpMessageConverter.canWrite(Class<?> clazz, MediaType mediaType)
method gets java.util.Collection<?>
class for check, however when it comes to conversion step ObjectToStringHttpMessageConverter
fails. To solve the problem we keep produces
for @RequestMapping
annotation where JSON convertor should be used, but to force correct content type for exception handler, we will erase HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE
attribute from HttpServletRequest
:
@ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(value = HttpStatus.BAD_REQUEST)
@ResponseBody
public String handleIllegalArgumentException(HttpServletRequest request, IllegalArgumentException ex) {
request.removeAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
return ExceptionUtils.getStackTrace(ex);
}
@RequestMapping(value = "/meta", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public Collection<MetaInformation> getMetaInformations() {
return myService.getMetaInformations();
}
Context stays the same as it was originally:
<mvc:annotation-driven>
<mvc:message-converters>
<bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter" />
<bean class="org.springframework.http.converter.ObjectToStringHttpMessageConverter">
<property name="conversionService">
<bean class="org.springframework.context.support.ConversionServiceFactoryBean" />
</property>
<property name="supportedMediaTypes">
<array>
<util:constant static-field="org.springframework.http.MediaType.TEXT_PLAIN" />
</array>
</property>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
Now scenarios (1,2,3,4) are handled correctly because of content-type negotiation, and scenarios (5,6) are processed in exception handler.
Alternatively one can replace collection return type with arrays, then solution #1 is applicable again:
@RequestMapping(value = "/meta", method = RequestMethod.GET)
@ResponseBody
public MetaInformation[] getMetaInformations() {
return myService.getMetaInformations().toArray();
}
For discussion:
I think that AbstractMessageConverterMethodProcessor.writeWithMessageConverters()
should not inherit class from value, but rather from method signature:
Type returnValueType = returnType.getGenericParameterType();
and HttpMessageConverter.canWrite(Class<?> clazz, MediaType mediaType)
should be changed to:
canWrite(Type returnType, MediaType mediaType)
or (in case it is too limiting potential class-based convertors) to
canWrite(Class<?> valueClazz, Type returnType, MediaType mediaType)
Then parametrized types could be handled correctly and solution #1 would be applicable again.
I think removing the produces = MediaType.APPLICATION_JSON_VALUE
from @RequestMapping
of the getMetaInformation
will give you the desired result.
The response-type will be negotiated according to the content-type value in the Accept header.
edit
As this does not cover scenario 3,4 here is a solution working with ResponseEntity.class
directly:
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleIllegalArgumentException(Exception ex) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.TEXT_PLAIN);
return new ResponseEntity<String>(ex.getMessage(), headers, HttpStatus.BAD_REQUEST);
}