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

时间:2022-12-01 15:58:43

I have a controller with the POST handler defined like so:

我有一个控制器,它的POST处理器是这样定义的:

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

The UIVendor object, when viewed in JSON format, looks like:

UIVendor对象,当以JSON格式查看时,看起来如下:

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

The UIVendor bean has a field called "Emails" of type ArrayList, with appropriate setters and getters (getEmails/setEmails). The NotificationEmail object has the appropriate public setters/getters as well.

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

When I try to post the object using the following code:

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

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

I get this error in the logs:

我在日志中得到这个错误:

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]

How do I correctly post a nested object like this to a Spring controller and have it correctly deserialize into the appropriate object structure.

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

UPDATE Per Bohzo's request, here is the content of the UIVendor class. This class wraps a web-service-generated bean class, exposing the VendorAttributes as individual fields:

根据Bohzo的请求进行更新,这是UIVendor类的内容。这个类封装了一个web服务生成的bean类,将供应商属性作为单独的字段公开:

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;
  }
}

UPDATE 2 Here's the output from Jackson.:

更新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"
}

And here is the structure of the object I'm returning on the 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"
              }
            ]
}

I've tried serializing using the JSON library from www.json.org as well, and the result is exactly what you see above. However, when I post that data, all of the fields in the UIVendor object passed to the controller are null (although the object is not).

我也尝试过使用来自www.json.org的JSON库进行序列化,结果与上面看到的完全一样。然而,当我发布这些数据时,传递给控制器的UIVendor对象中的所有字段都是null(尽管对象不是)。

4 个解决方案

#1


30  

Update: since Spring 3.1, it's possible to use @Valid On @RequestBody Controller Method Arguments.

更新:自Spring 3.1以来,可以在@RequestBody控制器方法参数上使用@Valid。

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

After much trial and error, I've finally figured out, as well as I can, what the problem is. When using the following controller method signature:

经过反复试验,我终于弄明白了问题出在哪里。使用以下控制器方法签名:

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

The client script has to pass the field in the object in post-data (typically "application/x-www-form-urlencoded") format (i.e., field=value&field2=value2). This is done in jQuery like this:

客户端脚本必须以后数据格式(通常是“application/ www-form- urlencoding”)传递对象中的字段(例如)。、字段= value&field2 = value2)。这是在jQuery中完成的:

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

This works fine for simple POJO objects that don't have child objects or collections, but once you introduce significant complexity to the object being passed, the notation used by jQuery to serialize the object data is not recognized by Spring's mapping logic:

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

object[0][field]

The way that I solved this problem was to change the method signature in the controller to:

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

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

And change the call from client to:

并将客户的电话更改为:

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

This requires the use of the JSON javascript library. It also forces the contentType to "application/json", which is what Spring expects when using the @RequestBody annotation, and serializes the object to a format that Jackson can deserialize into a valid object structure.

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

The only side effect is that now I have to handle my own object validation inside of the controller method, but that's relatively simple:

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

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

If anyone has any suggestions to improve upon this process, I'm all ears.

如果有人对这个过程有什么改进意见,我将洗耳恭听。

#2


17  

first, sorry for my poor english

首先,我为我的糟糕英语感到抱歉

in spring, if the param name is like object[0][field], they will consider it as a class type like sub

在spring中,如果param名称类似于对象[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;
    }
}

that's why the spring will throw an exception said something "is neither an array nor a List nor a Map".

这就是为什么spring会抛出异常的原因,“既不是数组,也不是列表,也不是映射”。

only when the param name is object[0].field, spring will treat it as a class's field.

只有当param名称是对象[0]时。field, spring会把它当作一个类的字段。

you could find the constants def in org.springframework.beans.PropertyAccessor

您可以在org.springframe . beans.propertyaccessor中找到def常量

so my solution is write a new param plugin for jquery, like below:

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

(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);

actual I just change the code from

实际上我只是把代码从

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

to

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

and use $.customParam instead of $.param when do ajax request.

并使用美元。customParam而不是美元。何时请求ajax。

#3


4  

You can try something like this:

你可以试试这样的方法:

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

:)

:)

#4


1  

Define the field to be List (interface), not ArrayList (concrete type):

定义要列表的字段(接口),而不是ArrayList(具体类型):

private List emailAddresses = new ArrayList();

#1


30  

Update: since Spring 3.1, it's possible to use @Valid On @RequestBody Controller Method Arguments.

更新:自Spring 3.1以来,可以在@RequestBody控制器方法参数上使用@Valid。

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

After much trial and error, I've finally figured out, as well as I can, what the problem is. When using the following controller method signature:

经过反复试验,我终于弄明白了问题出在哪里。使用以下控制器方法签名:

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

The client script has to pass the field in the object in post-data (typically "application/x-www-form-urlencoded") format (i.e., field=value&field2=value2). This is done in jQuery like this:

客户端脚本必须以后数据格式(通常是“application/ www-form- urlencoding”)传递对象中的字段(例如)。、字段= value&field2 = value2)。这是在jQuery中完成的:

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

This works fine for simple POJO objects that don't have child objects or collections, but once you introduce significant complexity to the object being passed, the notation used by jQuery to serialize the object data is not recognized by Spring's mapping logic:

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

object[0][field]

The way that I solved this problem was to change the method signature in the controller to:

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

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

And change the call from client to:

并将客户的电话更改为:

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

This requires the use of the JSON javascript library. It also forces the contentType to "application/json", which is what Spring expects when using the @RequestBody annotation, and serializes the object to a format that Jackson can deserialize into a valid object structure.

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

The only side effect is that now I have to handle my own object validation inside of the controller method, but that's relatively simple:

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

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

If anyone has any suggestions to improve upon this process, I'm all ears.

如果有人对这个过程有什么改进意见,我将洗耳恭听。

#2


17  

first, sorry for my poor english

首先,我为我的糟糕英语感到抱歉

in spring, if the param name is like object[0][field], they will consider it as a class type like sub

在spring中,如果param名称类似于对象[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;
    }
}

that's why the spring will throw an exception said something "is neither an array nor a List nor a Map".

这就是为什么spring会抛出异常的原因,“既不是数组,也不是列表,也不是映射”。

only when the param name is object[0].field, spring will treat it as a class's field.

只有当param名称是对象[0]时。field, spring会把它当作一个类的字段。

you could find the constants def in org.springframework.beans.PropertyAccessor

您可以在org.springframe . beans.propertyaccessor中找到def常量

so my solution is write a new param plugin for jquery, like below:

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

(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);

actual I just change the code from

实际上我只是把代码从

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

to

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

and use $.customParam instead of $.param when do ajax request.

并使用美元。customParam而不是美元。何时请求ajax。

#3


4  

You can try something like this:

你可以试试这样的方法:

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

:)

:)

#4


1  

Define the field to be List (interface), not ArrayList (concrete type):

定义要列表的字段(接口),而不是ArrayList(具体类型):

private List emailAddresses = new ArrayList();