【问题标题】:Producing / Consuming Symmetrical JSON with Spring MVC 3.0使用 Spring MVC 3.0 生产/消费对称 JSON
【发布时间】:2011-02-23 08:13:48
【问题描述】:

我正在通过 Spring 配置一个 RESTful Web 服务,具有各种表示形式,包括 JSON。我希望接口是对称的,这意味着通过 GET 序列化为 JSON 的对象的格式也是 POST/PUT 可以接受的格式。不幸的是,我只能让 GET 工作。

这是我发送和接收 JSON 的配置,它由 JSON 消息转换器和视图组成:

<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
    <property name="messageConverters">
        <util:list>
            <bean class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter"/>
        </util:list>
    </property>
</bean>

<bean id="contentNegotiatingViewResolver" class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
    <property name="mediaTypes">
        <util:map>
            <entry key="json" value="application/json"/>
        </util:map>
    </property>
    <property name="defaultViews">
        <util:list>
            <bean class="org.springframework.web.servlet.view.json.MappingJacksonJsonView"/>
        </util:list>
    </property>
</bean>

当我使用 GET 访问控制器以返回对象(例如 Book)时,它会输出类似这样的内容。

{"book":{"isbn":"1234","author":"Leo Tolstoy","title":"War and Peace"}}

如果我转身通过 POST 或 PUT 重新提交一些类似的 JSON,Spring 无法使用它,抱怨 Unrecognized field "book" (Class com.mycompany.Book), not marked as ignorable。此外,如果我去掉“书”包装元素(我宁愿不这样做,只是想看看会发生什么),我会收到 400 BAD REQUEST。无论哪种情况,我的控制器代码都不会被命中。

这是我的控制器 - 我宁愿在这里没有任何特定于 JSON 的代码(或者我的类上的注释被编组/解组),因为它们将有多种表示形式 - 我想使用 Spring 的解耦 MVC 基础架构来推动那种东西(编组/查看解析/等)到配置文件中:

@RequestMapping(method=PUT, value="/books/{isbn}")
@ResponseStatus(NO_CONTENT)
public void saveBook(@RequestBody Book book, @PathVariable String isbn) {
    book.setIsbn(isbn);
    bookService.saveBook(book)
}

@RequestMapping(method=GET, value="/books/{isbn}")
public ModelAndView getBook(@PathVariable String isbn) {
    return new ModelAndView("books/show", "book", bookService.getBook(isbn));
}

【问题讨论】:

    标签: java json spring spring-mvc marshalling


    【解决方案1】:

    尽管很尴尬,但我正在为后代回答我自己的问题:-)

    事实证明,我发布的这个示例方法代表了我真实代码中的等效控制器方法:

    void saveBook(@RequestBody Book book, @PathVariable String isbn)
    

    其实看起来更像这样(注:LongviceString):

    void saveBook(@RequestBody Book book, @PathVariable Long isbn)
    

    并且传递的值不能转换为 Long(它是字母数字)。所以……我搞砸了! :-)

    然而,Spring 对此并不好,干脆吐出400 Bad Request。我附加了一个调试器来发现这一点。

    ModelAndView 的使用仍然会生成一个外部包装器元素,我必须以某种方式处理它(因为我想使用 ModelAndView 来支持 JSP 视图等)。我可能必须为此提供一个自定义视图。


    包装元素的更新:

    事实证明,它是由 Spring 编组代表模型的对象的 Map 创建的。该地图有一个名为“book”的键(我想是从类名生成的,因为即使我只是返回一本书,它也在那里)。在我找到更好的方法之前,这里有一个 hackish 方法:

    /**
     * When using a Spring Controller that is ignorant of media types, the resulting model
     * objects end up in a map as values. The MappingJacksonJsonView then converts this map
     * to JSON, which (possibly) incorrectly wraps the single model object in the map
     * entry's key. This class eliminates this wrapper element if there is only one model
     * object.
     */
    public class SimpleJacksonJsonView extends MappingJacksonJsonView {
    
        @Override
        @SuppressWarnings("unchecked")
        protected Object filterModel(Map<String, Object> model) {
            Map<String, Object> filteredModel = (Map<String, Object>) super.filterModel(model);
            if(filteredModel.size() != 1) return filteredModel;
            return filteredModel.entrySet().iterator().next().getValue();
        }
    }
    

    【讨论】:

    • MappingJackson2JsonView 有一个属性 extractValueFromSingleKeyModel。当它设置为 true 时,它​​的行为与您的 SimpleJacksonJsonView 完全相同。
    • 在提出这个问题时,我认为该属性不存在。谢谢!
    【解决方案2】:

    请注意,您指定在控制器方法上使用 GET,因此您可以配置特定的 POST 方法以接收 GET 和 POST 形式的 RESTFull 方法,如下所示:

    @RequestMapping(method=GET, value="/books/{isbn}")
    public ModelAndView getBook(@PathVariable String isbn) {
        return new ModelAndView("books/show", "book", bookService.getBook(isbn));
    }
    
    @RequestMapping(method=POST, value="/books/{isbn}")
    public ModelAndView getByPostBook(@PathVariable String isbn) {
        return getBook(isbn);
    }
    

    【讨论】:

    • 谢谢。我不想“通过邮件获取”,我想将 JSON 对象 PUT 和/或 POST 到服务器(我正在制作 RESTful 服务)。我认为问题不在于我如何定义我的请求映射,而在于我如何为传入的 JSON 配置消息转换器。请参阅我标记为“PUT”的示例控制器方法 - 这是由于消息解组错误而无法调用的接口。
    • 对不起@SingleShot,我不明白你的问题。好吧,如果您的问题与消息转换器有关,我认为您将不得不自定义自己的。实际上,我不喜欢杰克逊在处理 jason 对象时所做的事情。所以我找到了一种方法来隐藏它,方法是构建一个自定义 Gson 消息转换器see if it helps you get a new idea
    • 谢谢。尽管你没有回答我的确切问题,但你让我了解了 Gson :-)
    【解决方案3】:

    @SingleShot, {"isbn":"1234","author":"Leo Tolstoy","title":"War and Peace"} 没有包装 book 是 JSON 中书籍实例的正确表示,添加书籍包装器是因为当您在 GET 方法中返回 ModelAndView 时拥有的 modelName“book”,并且 MappingJacksonJSONView 附加在书籍包装器上。

    更好的模式是在 Get 中简单地返回 book 对象并使用 @ResponseBody 注释该方法

        @RequestMapping(method=RequestMethod.GET, value="/books/{isbn}")
    public @ResponseBody Book getBook(@PathVariable String isbn) {
        return new Book("isbn","title","author");
    }
    

    关于您的 PUT 没有解析正确的 Controller 方法,您能否确认您的请求内容类型为 application/json。

    【讨论】:

    • 谢谢。我更喜欢您的@ResponseBody 建议,但发现需要使用 ModelAndView 才能让我的控制器在需要 HTML 时解析为 JSP(除了 JSON 之外,我还生成 HTML 和 XML)。我确实将 Content-Type 设置为 application/json。不过,我会探讨您的建议。
    • 我测试了您建议的样式 - 结果相同:“书”包装元素。
    • 我对包装元素有一个骇人听闻的解决方案。看我的回答。
    猜你喜欢
    • 2019-06-21
    • 2020-11-11
    • 1970-01-01
    • 2020-08-13
    • 1970-01-01
    • 1970-01-01
    • 2017-07-13
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多