Как изменить тип содержимого в обработчике исключений
Предположим, у меня есть контроллер, который обслуживает запрос GET
и возвращает bean для сериализации в JSON, а также предоставляет обработчик исключений для IllegalArgumentException
, который может быть поднят в службе:
@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);
}
Преобразователи сообщений:
<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>
Теперь, когда я запрашиваю данный URL-адрес в браузере, я вижу правильный ответ JSON. Однако, если возникает исключение, строковое исключение также преобразуется в JSON, но мне бы хотелось, чтобы он обрабатывался с помощью StringHttpMessageConverter
(в результате text/plain
mime type). Как я могу это сделать?
Чтобы сделать изображение более полным (и сложным), предположим, что у меня также есть следующий обработчик:
@RequestMapping(value = "/version", method = RequestMethod.GET)
@ResponseBody
public String getApplicationVersion() {
return "1.0.12";
}
Этот обработчик позволяет сериализовать возвращаемую строку как с помощью MappingJackson2HttpMessageConverter
, так и StringHttpMessageConverter
, в зависимости от переданного Accept-type
клиентом. Типы и значения возврата должны быть следующими:
+----+---------------------+-----------------------+------------------+-------------------------------------+
| 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 |
+----+---------------------+-----------------------+------------------+-------------------------------------+
Ответы
Ответ 1
Я думаю, что удаление produces = MediaType.APPLICATION_JSON_VALUE
из @RequestMapping
из getMetaInformation
даст вам желаемый результат.
Тип ответа будет согласован в соответствии со значением типа содержимого в заголовке Accept.
изменить
Так как это не охватывает сценарий 3,4, то это решение, непосредственно работающее с ResponseEntity.class
:
@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);
}
Ответ 2
Существует несколько аспектов проблемы:
-
StringHttpMessageConverter
добавляет тип MIME всех типов */*
в список поддерживаемых типов носителей, а MappingJackson2HttpMessageConverter
привязан только к application/json
.
- Когда
@RequestMapping
предоставляет produces = ...
, это значение сохраняется в HttpServletRequest
(см. RequestMappingInfoHandlerMapping.handleMatch()
), и когда вызывается обработчик ошибок, этот тип mime автоматически наследуется и используется.
Решение в простом случае заключалось бы в том, чтобы сначала положить StringHttpMessageConverter
в список:
<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>
а также удалить produces
из @RequestMapping
аннотации:
@RequestMapping(value = "/meta/{itemId}", method = RequestMethod.GET)
@ResponseBody
public MetaInformation getMetaInformation(@PathVariable int itemId) {
return myService.getMetaInformation(itemId);
}
Сейчас:
-
StringHttpMessageConverter
будет отбрасывать все типы, которые могут обрабатывать только MappingJackson2HttpMessageConverter
(MetaInformation
, java.util.Collection
и т.д.), что позволяет им передавать дальше.
- В случае исключения в сценарии (5, 6)
StringHttpMessageConverter
будет приоритет.
Пока все хорошо, но, к сожалению, все сложнее с ObjectToStringHttpMessageConverter
. Для типа возврата обработчика java.util.Collection<MetaInformation>
этот преобразователь сообщений сообщит, что он может преобразовать этот тип в java.lang.String
. Ограничение исходит из того факта, что типы элементов коллекции стираются, а метод AbstractHttpMessageConverter.canWrite(Class<?> clazz, MediaType mediaType)
получает класс java.util.Collection<?>
для проверки, однако, когда дело доходит до этапа преобразования ObjectToStringHttpMessageConverter
, происходит сбой. Чтобы решить эту проблему, мы сохраняем produces
для аннотации @RequestMapping
, где должен использоваться конвертер JSON, но для принудительного правильного типа содержимого для обработчика исключений мы удалим атрибут HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE
из 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();
}
Контекст остается таким же, как и изначально:
<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>
Теперь сценарии (1,2,3,4) обрабатываются правильно из-за согласования типа контента, а сценарии (5, 6) обрабатываются в обработчике исключений.
В качестве альтернативы можно заменить тип возвращаемого типа массивами массивами, тогда решение # 1 снова применимо:
@RequestMapping(value = "/meta", method = RequestMethod.GET)
@ResponseBody
public MetaInformation[] getMetaInformations() {
return myService.getMetaInformations().toArray();
}
Для обсуждения:
Я думаю, что AbstractMessageConverterMethodProcessor.writeWithMessageConverters()
не должен наследовать класс от значения, а скорее из сигнатуры метода:
Type returnValueType = returnType.getGenericParameterType();
и HttpMessageConverter.canWrite(Class<?> clazz, MediaType mediaType)
следует изменить на:
canWrite(Type returnType, MediaType mediaType)
или (в случае, если это слишком ограничивает потенциальные конвертеры на основе классов) до
canWrite(Class<?> valueClazz, Type returnType, MediaType mediaType)
Тогда параметризованные типы могут быть обработаны правильно, и решение # 1 будет применимо снова.