Spring 3.0.5 — добавление @ModelAttribute в сигнатуру метода обработчика приводит к JsonMappingException

Я не уверен, является ли это неправильной конфигурацией с моей стороны, непониманием того, что можно сделать с помощью @ModelAttribute и автоматического преобразования содержимого JSON, или ошибкой в ​​Spring или Jackson. Если выяснится, что последнее, конечно, я напишу вопрос соответствующим людям.

Я столкнулся с проблемой добавления @ModelAttribute в метод обработчика контроллера. Целью метода является предоставление компонента, который был заполнен из формы или предыдущей отправки, но я могу воспроизвести проблему, фактически не отправляя данные в компонент.

Я использую образец Spring mvc-showcase. В настоящее время он использует Spring 3.1, но я впервые столкнулся и могу воспроизвести эту проблему в своей установке 3.0.5. Образец mvc-showcase использует довольно стандартный servlet-context.xml:

servlet-context.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/mvc"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:beans="http://www.springframework.org/schema/beans"
    xsi:schemaLocation="
        http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.1.xsd
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd">

    <!-- DispatcherServlet Context: defines this servlet's request-processing infrastructure -->

    <!-- Enables the Spring MVC @Controller programming model -->
    <annotation-driven conversion-service="conversionService">
        <argument-resolvers>
            <beans:bean class="org.springframework.samples.mvc.data.custom.CustomArgumentResolver"/>
        </argument-resolvers>
    </annotation-driven>

    <!-- Handles HTTP GET requests for /resources/** by efficiently serving up static resources in the ${webappRoot}/resources/ directory -->
    <resources mapping="/resources/**" location="/resources/" />

    <!-- Resolves views selected for rendering by @Controllers to .jsp resources in the /WEB-INF/views directory -->
    <beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <beans:property name="prefix" value="/WEB-INF/views/" />
        <beans:property name="suffix" value=".jsp" />
    </beans:bean>

    <!-- Imports user-defined @Controller beans that process client requests -->
    <beans:import resource="controllers.xml" />

    <!-- Only needed because we install custom converters to support the examples in the org.springframewok.samples.mvc.convert package -->
    <beans:bean id="conversionService" class="org.springframework.samples.mvc.convert.CustomConversionServiceFactoryBean" />

    <!-- Only needed because we require fileupload in the org.springframework.samples.mvc.fileupload package -->
    <beans:bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver" />

</beans:beans>

controllers.xml, указанный в файле, просто устанавливает соответствующее сканирование компонента и контроллер представления для корневого пути. Соответствующий фрагмент приведен ниже.

контроллеры.xml

<!-- Maps '/' requests to the 'home' view -->
<mvc:view-controller path="/" view-name="home"/>

<context:component-scan base-package="org.springframework.samples.mvc" />

Тестовый компонент, который я пытаюсь доставить, представляет собой очень простой POJO.

TestBean.java

package org.springframework.samples.mvc.test;

public class TestBean {
    private String testField = "[email protected]";

    public String getTestField() {
        return testField;
    }

    public void setTestField(String testField) {
        this.testField = testField;
    }

}

И, наконец, контроллер, тоже простой.

TestController.java

package org.springframework.samples.mvc.test;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequestMapping("test/*")
public class TestController {

    @ModelAttribute("testBean")
    public TestBean getTestBean() {
        return new TestBean();
    }

    @RequestMapping(value = "beanOnly", method = RequestMethod.POST)
    public @ResponseBody
    TestBean testBean(@ModelAttribute("testBean") TestBean bean) {
        return bean;
    }

    @RequestMapping(value = "withoutModel", method = RequestMethod.POST)
    public @ResponseBody
    Model testWithoutModel(Model model) {
        model.addAttribute("result", "success");
        return model;
    }

    @RequestMapping(value = "withModel", method = RequestMethod.POST)
    public @ResponseBody
    Model testWithModel(Model model, @ModelAttribute("testBean") TestBean bean) {
        bean.setTestField("This is the new value of testField");
        model.addAttribute("result", "success");
        return model;
    }

}

Если я вызову контроллер через сопоставленный путь /mvc-showcase/test/beanOnly, я получу JSON-представление bean-компонента, как и ожидалось. Вызов обработчика withoutModel предоставляет JSON-представление объекта Spring Model, связанного с вызовом. Он включает неявный @ModelAttribute из начального объявления в возвращаемое значение, но бин недоступен для метода. Если я хочу, например, обработать результаты отправки формы и вернуть ответное сообщение JSON, мне нужен этот атрибут.

Последний метод добавляет @ModelAttribute, и здесь возникает проблема. Вызов /mvc-showcase/test/withModel вызывает исключение.

В моей установке 3.0.5 я получаю исключение JsonMappingException, вызванное отсутствием сериализатора для FormattingConversionService. В образце 3.1.0 исключение вызвано отсутствием сериализатора для DefaultConversionService. Я включу сюда исключение 3.1; кажется, что у него та же основная причина, даже если путь немного отличается.

3.1 org.codehaus.jackson.map.JsonMappingException

org.codehaus.jackson.map.JsonMappingException: No serializer found for class org.springframework.format.support.DefaultFormattingConversionService and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationConfig.Feature.FAIL_ON_EMPTY_BEANS) ) (through reference chain: org.springframework.validation.support.BindingAwareModelMap["org.springframework.validation.BindingResult.testBean"]->org.springframework.validation.BeanPropertyBindingResult["propertyAccessor"]->org.springframework.beans.BeanWrapperImpl["conversionService"])
    at org.codehaus.jackson.map.ser.StdSerializerProvider$1.failForEmpty(StdSerializerProvider.java:89)
    at org.codehaus.jackson.map.ser.StdSerializerProvider$1.serialize(StdSerializerProvider.java:62)
    at org.codehaus.jackson.map.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:272)
    at org.codehaus.jackson.map.ser.BeanSerializer.serializeFields(BeanSerializer.java:175)
    at org.codehaus.jackson.map.ser.BeanSerializer.serialize(BeanSerializer.java:147)
    at org.codehaus.jackson.map.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:272)
    at org.codehaus.jackson.map.ser.BeanSerializer.serializeFields(BeanSerializer.java:175)
    at org.codehaus.jackson.map.ser.BeanSerializer.serialize(BeanSerializer.java:147)
    at org.codehaus.jackson.map.ser.MapSerializer.serializeFields(MapSerializer.java:207)
    at org.codehaus.jackson.map.ser.MapSerializer.serialize(MapSerializer.java:140)
    at org.codehaus.jackson.map.ser.MapSerializer.serialize(MapSerializer.java:22)
    at org.codehaus.jackson.map.ser.StdSerializerProvider._serializeValue(StdSerializerProvider.java:315)
    at org.codehaus.jackson.map.ser.StdSerializerProvider.serializeValue(StdSerializerProvider.java:242)
    at org.codehaus.jackson.map.ObjectMapper.writeValue(ObjectMapper.java:1030)
    at org.springframework.http.converter.json.MappingJacksonHttpMessageConverter.writeInternal(MappingJacksonHttpMessageConverter.java:153)
    at org.springframework.http.converter.AbstractHttpMessageConverter.write(AbstractHttpMessageConverter.java:181)
    at org.springframework.web.servlet.mvc.method.annotation.support.AbstractMessageConverterMethodProcessor.writeWithMessageConverters(AbstractMessageConverterMethodProcessor.java:121)
    at org.springframework.web.servlet.mvc.method.annotation.support.AbstractMessageConverterMethodProcessor.writeWithMessageConverters(AbstractMessageConverterMethodProcessor.java:101)
    at org.springframework.web.servlet.mvc.method.annotation.support.RequestResponseBodyMethodProcessor.handleReturnValue(RequestResponseBodyMethodProcessor.java:81)
    at org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite.handleReturnValue(HandlerMethodReturnValueHandlerComposite.java:64)
    at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:114)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMethodAdapter.invokeHandlerMethod(RequestMappingHandlerMethodAdapter.java:505)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMethodAdapter.handleInternal(RequestMappingHandlerMethodAdapter.java:468)
    at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:80)
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:790)
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:719)
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:644)
    at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:560)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:710)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:803)
    at 
...

Итак, есть ли какая-то конфигурация, которую мне не хватает, которая должна позволить конвертеру Джексона правильно обрабатывать ответ, полученный от обработчика с @ModelAttribute в сигнатуре метода? Если нет, есть ли какие-либо мысли о том, является ли это скорее ошибкой Spring или ошибкой Джексона? На данный момент я склоняюсь к весне.


person Palpatim    schedule 22.04.2011    source источник


Ответы (1)


Это похоже на проблему конфигурации Spring: при сериализации в JSON DefaultFormattingConversionService пуст, и Джексон (по умолчанию) выдает исключение, если bean-компонент пуст, см. FAIL_ON_EMPTY_BEANS в документация по функциям. Но мне не ясно, почему бин пустой.

Это должно работать, если вы установите FAIL_ON_EMPTY_BEANS в false, но все равно не объясняет, почему это происходит в первую очередь.

DefaultFormattingConversionService является новым для 3.1 — он расширяет FormattingConversionService, который объясняет различные исключения между 3.0.5 и 3.1.

Я не думаю, что это проблема Джексона, хотя новая версия Джексона (1.8. 0) был выпущен всего 3 дня назад, так что вы тоже можете попробовать его.

Я попытаюсь воспроизвести это локально.

person andyb    schedule 23.04.2011
comment
Я тоже к этому склоняюсь — подозреваю, что либо ошибка во взаимодействии, либо какая-то конфигурация, которую мне не хватает где-то в цепочке. Я открыл вопрос о весне; посмотрим, принесет ли это какие-либо полезные результаты jira.springsource.org/browse/SPR-8272< /а> - person Palpatim; 23.04.2011
comment
Дополнение: я отключил функцию FAIL_ON_EMPTY_BEANS и теперь вижу ошибку циклической ссылки. Я продолжу копаться в этом, но сейчас я, вероятно, склонюсь к использованию пользовательского компонента вместо возврата возвращаемого значения на основе Model в тех методах, где мне требуется, чтобы @ModelAttribute передавалось обработчику. - person Palpatim; 25.04.2011
comment
Заключительное слово: объяснение см. в комментариях к выпуску Spring: jira.springsource.org/browse/ SPR-8272 . По сути, модель автоматически оснащается ссылками BindingResults, которые вызывают ошибку циклической ссылки. Правильная парадигма состоит в том, чтобы сделать то, что я в конце концов сделал: предоставить требуемые данные и значения bindingResults в объекте, который мы создали сами. Я бы хотел, чтобы это было встроено в Spring, но, наверное, именно поэтому он с открытым исходным кодом. :) - person Palpatim; 04.05.2011
comment
@Palpatim спасибо за публикацию продолжения. Я предполагаю, что большинство приложений, которые я написал, используют настраиваемые сериализаторы либо для обслуживания циклических ссылок, либо для предоставления оптимизированного представления модели. - person andyb; 05.05.2011