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

我有一个控制器,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控制器并将其正确地反序列化到适当的对象结构中。

UPDATE Per 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 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 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 getEmails() { if ( this.emails == null ) { this.emails = new ArrayList(); } return emails; } public void setEmails(List 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对象中的所有字段都为空(尽管该对象不是)。

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

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

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

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

 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可以反序列化为有效对象结构的格式。

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

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

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

首先,抱歉我的英语不好

在spring中,如果param名称类似于object [0] [field],他们会将其视为类似sub的​​类

 public class Test { private List field; /** * @return the field */ public List getField() { return field; } /** * @param field the field to set */ public void setField(List field) { this.field = field; } } 

这就是为什么spring会抛出exception所说的“既不是数组也不是列表也不是地图”。

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

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

所以我的解决方案是为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); 

实际我只是改变了代码

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

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

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

你可以尝试这样的事情:

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

🙂

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

 private List emailAddresses = new ArrayList();