Подтвердить что ты не робот

Поместить вложенный объект в Spring MVC-контроллер с помощью JSON

У меня есть контроллер с обработчиком POST, определенным так:

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

Объект UIVendor, если смотреть в формате JSON, выглядит так:

var vendor = 
{
  vendorId: 123,
  vendorName: "ABC Company",
  emails : [
             { emailAddress: "[email protected]", flags: 2 },
             { emailAddress: "[email protected]", flags: 3 }
           ]
}

UIVendor bean имеет поле под названием "Emails" типа ArrayList с соответствующими сеттерами и getters (getEmails/setEmails). Объект NotificationEmail также имеет соответствующие публичные сеттеры/получатели.

Когда я пытаюсь опубликовать объект, используя следующий код:

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

Как правильно разместить вложенный объект, подобный этому, в контроллер Spring и правильно ли десериализовать его в соответствующей структуре объекта.

UPDATE По просьбе Per Bohzo вот содержание класса UIVendor. Этот класс обертывает класс 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":"[email protected]"
              }
            ]
}

Я попробовал сериализацию с использованием библиотеки JSON с сайта www.json.org, а результат - именно то, что вы видите выше. Однако, когда я отправляю эти данные, все поля в объекте UIVendor, переданные контроллеру, являются нулевыми (хотя объект не является).

4b9b3361

Ответ 1

Обновление:, начиная с 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 )

Клиент script должен передать поле в объекте в формате post-data (обычно "application/x-www-form-urlencoded" ) (т.е. поле = значение & поле2 = значение2). Это делается в 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"
            } );    

Для этого требуется использование библиотеки JavaScript JSON. Он также заставляет contentType "application/json", что ожидается при использовании аннотации @RequestBody, и сериализует объект в формат, который Джексон может десериализовать в правильную структуру объекта.

Единственным побочным эффектом является то, что теперь я должен обрабатывать свою собственную проверку объекта внутри метода контроллера, но это относительно просто:

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

Если у кого-то есть предложения по улучшению этого процесса, я все уши.

Ответ 2

сначала, извините за мой бедный английский

в spring, если имя параметра похоже на объект [0] [поле], они будут рассматривать его как тип класса, например 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 выдаст исключение, сказал что-то "не является ни массивом, ни списком, ни картой".

только если имя параметра является объектом [0].field, spring будет рассматривать его как поле класса.

вы можете найти константы def в org.springframework.beans.PropertyAccessor

поэтому мое решение пишет новый плагин param для 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 );

и используйте $.customParam вместо $.param, когда делаете запрос ajax.

Ответ 3

Вы можете попробовать что-то вроде этого:

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

:)

Ответ 4

Определите поле List (интерфейс), а не ArrayList (конкретный тип):

private List emailAddresses = new ArrayList();