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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/5900840/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-26 19:59:15  来源:igfitidea点击:

Post Nested Object to Spring MVC controller using JSON

jqueryajaxjsonspringspring-mvc

提问by pconrey

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: "[email protected]", flags: 2 },
             { emailAddress: "[email protected]", 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 有一个名为“Emails”的 ArrayList 类型字段,带有适当的 setter 和 getter (getEmails/setEmails)。NotificationEmail 对象也有适当的公共设置器/获取器。

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 [[email protected]]

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 控制器并使其正确反序列化为适当的对象结构。

UPDATEPer 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 类,将 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.Hymanson.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 2Here's the output from Hymanson.:

更新 2这是 Hymanson 的输出。:

{
  "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:

这是我在 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":"[email protected]"
              }
            ]
}

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 对象中的所有字段都为空(尽管该对象不是)。

回答by pconrey

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

更新:从 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 )


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:

客户端脚本必须以post-data(通常是“application/x-www-form-urlencoded”)格式(即field=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 对象,但是一旦您向传递的对象引入了显着的复杂性,Spring 的映射逻辑就无法识别 jQuery 用于序列化对象数据的符号:

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 JSONjavascript 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 Hymanson can deserialize into a valid object structure.

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

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:

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

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.

如果有人对改进此过程有任何建议,我会全神贯注。

回答by keshin

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 中,如果参数名称类似于 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;
    }
}

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

这就是为什么春天会抛出一个异常说“既不是数组也不是列表也不是地图”的原因。

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

仅当参数名称为 object[0].field 时,spring 才会将其视为类的字段。

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

你可以在 org.springframework.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.

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

回答by Miro

You can try something like this:

你可以尝试这样的事情:

vendor['emails[0].emailAddress'] = "[email protected]";
vendor['emails[0].flags'] = 3;
vendor['emails[1].emailAddress'] = "[email protected]";
vendor['emails[1].flags'] = 3;

:)

:)

回答by Bozho

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

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

private List emailAddresses = new ArrayList();