Подтвердить что ты не робот

Как изменить тип содержимого в обработчике исключений

Предположим, у меня есть контроллер, который обслуживает запрос 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          |
+----+---------------------+-----------------------+------------------+-------------------------------------+
4b9b3361

Ответ 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 будет применимо снова.