使用 JSON 将嵌套对象发布到 Spring MVC 控制器

Posted

技术标签:

【中文标题】使用 JSON 将嵌套对象发布到 Spring MVC 控制器【英文标题】:Post Nested Object to Spring MVC controller using JSON 【发布时间】:2011-08-19 12:58:12 【问题描述】:

我有一个控制器,其 POST 处理程序定义如下:

@RequestMapping(value="/ajax/saveVendor.do", method = RequestMethod.POST)
public @ResponseBody AjaxResponse saveVendor( @Valid UIVendor vendor,
                                              BindingResult result,
                                              Locale currentLocale )

当以 JSON 格式查看时,UIVendor 对象如下所示:

var vendor = 

  vendorId: 123,
  vendorName: "ABC Company",
  emails : [
              emailAddress: "abc123@abc.com", flags: 2 ,
              emailAddress: "xyz@abc.com", flags: 3 
           ]

UIVendor bean 有一个名为“Emails”的 ArrayList 类型的字段,带有适当的 setter 和 getter (getEmails/setEmails)。 NotificationEmail 对象也有适当的公共 setter/getter。

当我尝试使用以下代码发布对象时:

$.post("ajax/saveVendor.do", $.param(vendor), saveEntityCallback, "json" );

我在日志中收到此错误:

Invalid property 'emails[0][emailAddress]' of bean class [beans.UIVendor]: Property referenced in indexed property path 'emails[0][emailAddress]' is neither an array nor a List nor a Map; returned value was [abc123@abc.com]

如何正确地将这样的嵌套对象发布到 Spring 控制器并使其正确反序列化为适当的对象结构。

更新 根据 Bohzo 的要求,这里是 UIVendor 类的内容。这个类包装了一个 web 服务生成的 bean 类,将 VendorAttributes 暴露为单独的字段:

package com.mycompany.beans;

import java.util.*;
import org.apache.commons.lang.*;
import com.mycompany.domain.Vendor;
import com.mycompany.domain.VendorAttributes;
import org.apache.commons.logging.*;
import org.codehaus.jackson.annotate.JsonIgnore;

public class UIVendor

  private final Log logger = LogFactory.getLog( this.getClass() );
  private Vendor vendor;
  private boolean ftpFlag;
  private String ftpHost;
  private String ftpPath;
  private String ftpUser;
  private String ftpPassword; 
  private List<UINotificationEmail> emails = null;

  public UIVendor()  this( new Vendor() ); 
  public UIVendor( Vendor vendor )
  
    this.vendor = vendor;
    loadVendorAttributes();
  

  private void loadVendorAttributes()
  
    this.ftpFlag = false;
    this.ftpHost = this.ftpPassword = this.ftpPath = this.ftpUser = "";
    this.emails = null;

    for ( VendorAttributes a : this.vendor.getVendorAttributes() )
    
      String key = a.getVendorFakey();
      String value = a.getVendorFaValue();
      int flags = a.getFlags();

      if ( StringUtils.isBlank(key) || StringUtils.isBlank(value) ) continue;

      if ( key.equals( "ftpFlag" ) )
      
        this.ftpFlag = BooleanUtils.toBoolean( value );
      
      else if ( key.equals( "ftpHost" ) )
      
        this.ftpHost = value;
      
      else if ( key.equals("ftpPath") )
      
        this.ftpPath = value;
      
      else if ( key.equals("ftpUser") )
      
        this.ftpUser = value;
      
      else if ( key.equals("ftpPassword") )
      
        this.ftpPassword = value;
      
      else if ( key.equals("email") )
      
        UINotificationEmail email = new UINotificationEmail(value, flags);
        this.getEmails().add( email );
      
    
  

  private void saveVendorAttributes()
  
    int id = this.vendor.getVendorId();
    List<VendorAttributes> attrs = this.vendor.getVendorAttributes();
    attrs.clear();

    if ( this.ftpFlag )
          
      VendorAttributes flag = new VendorAttributes();
      flag.setVendorId( id );
      flag.setStatus( "A" );
      flag.setVendorFakey( "ftpFlag" );
      flag.setVendorFaValue( BooleanUtils.toStringTrueFalse( this.ftpFlag ) );
      attrs.add( flag );

      if ( StringUtils.isNotBlank( this.ftpHost ) )
      
        VendorAttributes host = new VendorAttributes();
        host.setVendorId( id );
        host.setStatus( "A" );
        host.setVendorFakey( "ftpHost" );
        host.setVendorFaValue( this.ftpHost );
        attrs.add( host );

        if ( StringUtils.isNotBlank( this.ftpPath ) )
        
          VendorAttributes path = new VendorAttributes();
          path.setVendorId( id );
          path.setStatus( "A" );
          path.setVendorFakey( "ftpPath" );
          path.setVendorFaValue( this.ftpPath );
          attrs.add( path );
        

        if ( StringUtils.isNotBlank( this.ftpUser ) )
        
          VendorAttributes user = new VendorAttributes();
          user.setVendorId( id );
          user.setStatus( "A" );
          user.setVendorFakey( "ftpUser" );
          user.setVendorFaValue( this.ftpUser );
          attrs.add( user );
        

        if ( StringUtils.isNotBlank( this.ftpPassword ) )
        
          VendorAttributes password = new VendorAttributes();
          password.setVendorId( id );
          password.setStatus( "A" );
          password.setVendorFakey( "ftpPassword" );
          password.setVendorFaValue( this.ftpPassword ); 
          attrs.add( password );
        
            
    

    for ( UINotificationEmail e : this.getEmails() )
    
      logger.debug("Adding email " + e );
      VendorAttributes email = new VendorAttributes();
      email.setStatus( "A" );
      email.setVendorFakey( "email" );
      email.setVendorFaValue( e.getEmailAddress() );
      email.setFlags( e.getFlags() );
      email.setVendorId( id );
      attrs.add( email );
    
  

  @JsonIgnore
  public Vendor getVendor()
  
    saveVendorAttributes();
    return this.vendor;
  

  public int getVendorId()
  
    return this.vendor.getVendorId();
  
  public void setVendorId( int vendorId )
  
    this.vendor.setVendorId( vendorId );
  

  public String getVendorType()
  
    return this.vendor.getVendorType();
  
  public void setVendorType( String vendorType )
  
    this.vendor.setVendorType( vendorType );
  

  public String getVendorName()
  
    return this.vendor.getVendorName();
  
  public void setVendorName( String vendorName )
  
    this.vendor.setVendorName( vendorName );
  

  public String getStatus()
  
    return this.vendor.getStatus();
  
  public void setStatus( String status )
  
    this.vendor.setStatus( status );
  

  public boolean isFtpFlag()
  
    return this.ftpFlag;
  
  public void setFtpFlag( boolean ftpFlag )
  
    this.ftpFlag = ftpFlag;
  

  public String getFtpHost()
  
    return this.ftpHost;
  
  public void setFtpHost( String ftpHost )
  
    this.ftpHost = ftpHost;
  

  public String getFtpPath()
  
    return this.ftpPath;
  
  public void setFtpPath( String ftpPath )
  
    this.ftpPath = ftpPath;
  

  public String getFtpUser()
  
    return this.ftpUser;
  
  public void setFtpUser( String ftpUser )
  
    this.ftpUser = ftpUser;
  

  public String getFtpPassword()
  
    return this.ftpPassword;
  
  public void setFtpPassword( String ftpPassword )
  
    this.ftpPassword = ftpPassword;
  

  public List<UINotificationEmail> getEmails()
  
    if ( this.emails == null )
    
      this.emails = new ArrayList<UINotificationEmail>();
    
    return emails;
  

  public void setEmails(List<UINotificationEmail> emails)
  
    this.emails = emails;
  

更新 2 这是 Jackson 的输出:


  "vendorName":"MAIL",
  "vendorId":45,
  "emails":
  [
    
      "emailAddress":"dfg",
      "success":false,
      "failure":false,
      "flags":0
    
  ],
  "vendorType":"DFG",
  "ftpFlag":true,
  "ftpHost":"kdsfjng",
  "ftpPath":"dsfg",
  "ftpUser":"sdfg",
  "ftpPassword":"sdfg",
  "status":"A"

这是我在 POST 中返回的对象的结构:


  "vendorId":"45",
  "vendorName":"MAIL",
  "vendorType":"DFG",
  "ftpFlag":true,
  "ftpHost":"kdsfjng",
  "ftpUser":"sdfg",
  "ftpPath":"dsfg",
  "ftpPassword":"sdfg",
  "status":"A",
  "emails": 
            [
              
                "success":"false",
                "failure":"false",
                "emailAddress":"dfg"
              ,
              
                "success":"true",
                "failure":"true",
                "emailAddress":"pfc@sj.org"
              
            ]

我也尝试过使用来自 www.json.org 的 JSON 库进行序列化,结果正是您在上面看到的。但是,当我发布该数据时,传递给控制器​​的 UIVendor 对象中的所有字段都是空的(尽管对象不是)。

【问题讨论】:

如何提交为formdata?? 【参考方案1】:

将字段定义为List(接口),而不是ArrayList(具体类型):

private List emailAddresses = new ArrayList();

【讨论】:

我希望这有效,但它没有。我仍然遇到同样的错误。 您可以尝试相反的方法 - 创建一个新的供应商对象并将其序列化为 JSON,然后查看它的样子。 顺便说一下,这是 $.param(vendor) 的输出(URL 解码)。我想知道这种格式是否不适用于 Spring: vendorId=45&vendorName=MAIL&vendorType=DFG&ftpFlag=true&ftpHost=kdsfjng&ftpUser=sdfg&ftpPath=dsfg&ftpPassword=sdfg&status=A&emails[0][success]=false&emails[0][failure]=false&emails[ 0][emailAddress]=dfg&emails[1][success]=true&emails[1][failure]=true&emails[1][emailAddress]=pfc@sj.org 可能有效。但我更喜欢 JSON 版本。使用 Jackson 将 UIVendor 的实例转换为 JSON,看看它与你的有什么不同。 @StevenFrancolla 仅供参考,Spring 3.1 支持将@Valid@RequestBody 注释放在一起。【参考方案2】:

更新:自 Spring 3.1 起,可以使用@Valid On @RequestBody Controller Method Arguments。

@RequestMapping(value="/ajax/saveVendor.do", method = RequestMethod.POST)
public @ResponseBody AjaxResponse saveVendor( @Valid @RequestBody UIVendor vendor,
                                              BindingResult result,
                                              Locale currentLocale )

经过多次反复试验,我终于尽可能地弄清楚了问题所在。使用以下控制器方法签名时:

@RequestMapping(value="/ajax/saveVendor.do", method = RequestMethod.POST)
public @ResponseBody AjaxResponse saveVendor( @Valid UIVendor vendor,
                                              BindingResult result,
                                              Locale currentLocale )

客户端脚本必须以 post-data(通常为“application/x-www-form-urlencoded”)格式(即 field=value&field2=value2)传递对象中的字段。这是在 jQuery 中完成的,如下所示:

$.post( "mycontroller.do", $.param(object), callback, "json" )

这适用于没有子对象或集合的简单 POJO 对象,但是一旦您对传递的对象引入了显着的复杂性,Spring 的映射逻辑就无法识别 jQuery 用于序列化对象数据的表示法:

object[0][field]

我解决这个问题的方法是将控制器中的方法签名更改为:

@RequestMapping(value="/ajax/saveVendor.do", method = RequestMethod.POST)
public @ResponseBody AjaxResponse saveVendor( @RequestBody UIVendor vendor,
                                              Locale currentLocale )

并将来自客户端的调用更改为:

    $.ajax(
            
              url:"ajax/mycontroller.do", 
              type: "POST", 
              data: JSON.stringify( objecdt ), 
              success: callback, 
              dataType: "json",
              contentType: "application/json"
             );    

这需要使用JSON javascript 库。它还强制 contentType 为“application/json”,这是 Spring 在使用 @RequestBody 注解时所期望的,并将对象序列化为 Jackson 可以反序列化为有效对象结构的格式。

唯一的副作用是现在我必须在控制器方法中处理我自己的对象验证,但这相对简单:

BindingResult result = new BeanPropertyBindingResult( object, "MyObject" );
Validator validator = new MyObjectValidator();
validator.validate( object, result );

如果有人对改进此过程有任何建议,我会全力以赴。

【讨论】:

我正在处理同样的问题。你有没有为验证者想到更好的方法 顺便说一句:我不认为 "[...] "application/json",这是 Spring 在使用 @RequestBody 注释时所期望的" 是真的.对我来说,@RequestBody MultiValueMap&lt;String, String&gt; body 也适用于 application/x-www-form-urlencoded。但它需要在可用消息转换器的定义中像 &lt;bean id="formHttpMessageConverter" class="org.springframework.http.converter.FormHttpMessageConverter" /&gt; 这样的东西。 TL;DR - 设置 contentType: "application/json"data: JSON.stringify(obj)【参考方案3】:

首先,对不起我的英语不好

在spring中,如果参数名称是object[0][field],他们会认为它是一个类类型,比如sub

public class Test 

    private List<Map> field;

    /**
     * @return the field
     */
    public List<Map> getField() 
        return field;
    

    /**
     * @param field the field to set
     */
    public void setField(List<Map> field) 
        this.field = field;
    

这就是为什么 spring 会抛出一个异常,说“既不是数组也不是 List 也不是 Map”。

只有当参数名称是object[0].field时,spring才会把它当作一个类的字段。

你可以在 org.springframework.beans.PropertyAccessor 中找到常量 def

所以我的解决方案是为 jquery 编写一个新的参数插件,如下所示:

(function($) 
  // copy from jquery.js
  var r20 = /%20/g,
  rbracket = /\[\]$/;

  $.extend(
    customParam: function( a ) 
      var s = [],
        add = function( key, value ) 
          // If value is a function, invoke it and return its value
          value = jQuery.isFunction( value ) ? value() : value;
          s[ s.length ] = encodeURIComponent( key ) + "=" + encodeURIComponent( value );
        ;

      // If an array was passed in, assume that it is an array of form elements.
      if ( jQuery.isArray( a ) || ( a.jquery && !jQuery.isPlainObject( a ) ) ) 
        // Serialize the form elements
        jQuery.each( a, function() 
          add( this.name, this.value );
        );

       else 
        for ( var prefix in a ) 
          buildParams( prefix, a[ prefix ], add );
        
      

      // Return the resulting serialization
      return s.join( "&" ).replace( r20, "+" );
    
  );

/* private method*/
function buildParams( prefix, obj, add ) 
  if ( jQuery.isArray( obj ) ) 
    // Serialize array item.
    jQuery.each( obj, function( i, v ) 
      if (rbracket.test( prefix ) ) 
        // Treat each array item as a scalar.
        add( prefix, v );

       else 
        buildParams( prefix + "[" + ( typeof v === "object" || jQuery.isArray(v) ? i : "" ) + "]", v, add );
      
    );

   else if (obj != null && typeof obj === "object" ) 
    // Serialize object item.
    for ( var name in obj ) 
      buildParams( prefix + "." + name, obj[ name ], add );
    

   else 
    // Serialize scalar item.
    add( prefix, obj );
  
;
)(jQuery);

其实我只是把代码改成

buildParams( prefix + "[" + name + "]", obj[ name ], traditional, add );

buildParams( prefix + "." + name, obj[ name ], add );

并在进行 ajax 请求时使用 $.customParam 而不是 $.param。

【讨论】:

这个是正确的直接答案,被接受的是一种解决方法。 正确,但有点罗嗦!总结一下:用emails[0].emailAddress替换emails[0][emailAddress] @keshin ,我班级中的对象不是列表,不是数组,也不是地图。它只是另一个对象。是否可以自动绑定数据,例如 email.emailAddress 或 email[].emailAddress,我试过 email[].emailAddress 和 email[0].emailAddress 都不起作用。 @anavaraslamurep,这个回答已经快十年了....如果我理解正确,email 是你类的一个字段,emailAdress 是一个在 email 类中定义的电子邮件字段。如果是这样,我认为应该是 email.emailAddress。在表单/url中绑定你的属性,但我没有尝试。 @keshin ,是的,我也试过了,但没有成功,最后创建了单独的 pojo ,作为简单对象而不是对象内部的对象发送到后端。谢谢你这么多天后的回复。这意味着很多。尊重你。【参考方案4】:

你可以试试这样的:

vendor['emails[0].emailAddress'] = "abc123@abc.com";
vendor['emails[0].flags'] = 3;
vendor['emails[1].emailAddress'] = "xyz@abc.com";
vendor['emails[1].flags'] = 3;

:)

【讨论】:

以上是关于使用 JSON 将嵌套对象发布到 Spring MVC 控制器的主要内容,如果未能解决你的问题,请参考以下文章

使用嵌套映射的 JSON 对象反序列化,Java Spring

如何将嵌套的 JSON 对象转换为数组 Spring Boot?

是否可以使用 Spring Boot 将嵌套的 Json 数组存储到数据库?

Spring Boot 嵌套动态 json 请求映射到 pojo

Spring boot JPA - 没有嵌套对象的 JSON 与 OneToMany 关系

如何在 Spring Boot 中将嵌套的 JSON 对象映射为 SQL 表行