Suppose I have a controller that serves GET
request and returns bean to be serialized to JSON and also provides an exception handler for IllegalArgumentException
that can be raised in service:
@RequestMapping(value = "/meta/{itemId}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public MetaInformation getMetaInformation(@PathVariable int itemId) {
return myService.getMetaInformation(itemId);
}
@ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(value = HttpStatus.BAD_REQUEST)
@ResponseBody
public String handleIllegalArgumentException(IllegalArgumentException ex) {
return ExceptionUtils.getStackTrace(ex);
}
Message convertors are:
<mvc:annotation-driven>
<mvc:message-converters>
<bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter" />
<bean class="org.springframework.http.converter.StringHttpMessageConverter" />
</mvc:message-converters>
</mvc:annotation-driven>
Now when I request the given URL in browser I see the correct JSON reply. However if exception is raised, the stringified exception is converted into JSON as well, but I would love it to be processed by StringHttpMessageConverter
(resulting text/plain
mime type). How can I go it?
To make the picture more complete (and complicated), suppose I also have the following handler:
@RequestMapping(value = "/version", method = RequestMethod.GET)
@ResponseBody
public String getApplicationVersion() {
return "1.0.12";
}
This handler allows the return string to be serialized by both MappingJackson2HttpMessageConverter
and StringHttpMessageConverter
depending in passed Accept-type
by the client. The return types and values should be as following:
+----+---------------------+-----------------------+------------------+-------------------------------------+ | NN | URL | Accept-type | Content-type | Message converter | | | | request header | response header | | +----+---------------------+-----------------------+------------------+-------------------------------------+ | 1. | /version | text/html; */* | text/plain | StringHttpMessageConverter | | 2. | /version | application/json; */* | application/json | MappingJackson2HttpMessageConverter | | 3. | /meta/1 | text/html; */* | application/json | MappingJackson2HttpMessageConverter | | 4. | /meta/1 | application/json; */* | application/json | MappingJackson2HttpMessageConverter | | 5. | /meta/0 (exception) | text/html; */* | text/plain | StringHttpMessageConverter | | 6. | /meta/0 (exception) | application/json; */* | text/plain | StringHttpMessageConverter | +----+---------------------+-----------------------+------------------+-------------------------------------+
Spring MVC provides exception handling for your web application to make sure you are sending your own exception page instead of the server-generated exception to the user. The @ExceptionHandler annotation is used to detect certain runtime exceptions and send responses according to the exception.
In the Design tab part of the Ribbon, select New > Global Handler. The New Global Handler window opens. Type in a Name for the handler and save it in the project path. Click Create, a Global Exception Handler is added to the automation project.
The @ExceptionHandler is an annotation used to handle the specific exceptions and sending the custom responses to the client. Define a class that extends the RuntimeException class. You can define the @ExceptionHandler method to handle the exceptions as shown.
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); }
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With