Скопируйте свойства класса Groovy

Я хочу скопировать свойства объекта в другой объект обычным способом (если свойство существует в целевом объекте, я копирую его из исходного объекта).

Мой код отлично работает с использованием ExpandoMetaClass, но мне не нравится решение . Есть ли другие способы сделать это?

class User {
    String name = 'Arturo'
    String city = 'Madrid'
    Integer age = 27
}

class AdminUser {
    String name
    String city
    Integer age
}

def copyProperties(source, target) {
    target.properties.each { key, value ->
        if (source.metaClass.hasProperty(source, key) && key != 'class' && key != 'metaClass') {
            target.setProperty(key, source.metaClass.getProperty(source, key))
        }
    }
}

def (user, adminUser) = [new User(), new AdminUser()]
assert adminUser.name == null
assert adminUser.city == null
assert adminUser.age == null

copyProperties(user, adminUser)
assert adminUser.name == 'Arturo'
assert adminUser.city == 'Madrid'
assert adminUser.age == 27

person Arturo Herrero    schedule 30.01.2012    source источник
comment
Вы всегда можете использовать BeanUtils.   -  person Dave Newton    schedule 31.01.2012
comment
@DaveNewton Не уверен, что BeanUtils будет работать, поскольку источник и место назначения - это разные классы ...   -  person tim_yates    schedule 31.01.2012
comment
Связано: Groovy - привязать свойства одного объекта к другому   -  person Arturo Herrero    schedule 08.03.2012
comment
Как насчет использования аннотации AutoClone? groovy.codehaus.org/gapi/groovy/transform/AutoClone.html   -  person    schedule 28.04.2014


Ответы (4)


Я думаю, что ваше решение неплохое и находится в правильном направлении. По крайней мере, мне это вполне понятно.

Более лаконичная версия этого решения могла бы быть ...

def copyProperties(source, target) {
    source.properties.each { key, value ->
        if (target.hasProperty(key) && !(key in ['class', 'metaClass'])) 
            target[key] = value
    }
}

... но принципиально не отличается. Я перебираю исходные свойства, чтобы затем использовать значения для назначения цели :). Однако оно может быть менее надежным, чем ваше исходное решение, поскольку я думаю, что оно сломается, если целевой объект определит метод getAt(String).

Если вы хотите пофантазировать, вы можете сделать что-то вроде этого:

def copyProperties(source, target) {
    def (sProps, tProps) = [source, target]*.properties*.keySet()
    def commonProps = sProps.intersect(tProps) - ['class', 'metaClass']
    commonProps.each { target[it] = source[it] }
}

По сути, он сначала вычисляет общие свойства двух объектов, а затем копирует их. Это тоже работает, но я думаю, что первый более простой и понятный :)

Иногда лучше меньше, да лучше.

person epidemian    schedule 31.01.2012
comment
Я думаю, что для любителей однострочников должно работать что-то вроде этого: [source, target]*.properties*.keySet().grep { it != 'class' && it != 'metaClass' }.each { target[it] = source[it] } - person Yannick Mauray; 15.07.2014
comment
Возможно, вы также захотите поместить целевое назначение в try catch на всякий случай, если типы не могут быть принуждены - person GameSalutes; 08.04.2016
comment
Работает в Idea при отладке, но выдает исключение при вызове развернутого jar: Нет сигнатуры метода: java.util.ArrayList.keySet () не применима для типов аргументов: () значения: [] \ nВозможные решения: toSet () , toSet (), set (int, java.lang.Object), set (int, java.lang.Object), get (int), get (int) - person yuranos; 26.10.2017
comment
Это не удается, если параметры имеют свойство с именем properties или метод с именем getProperties() с нулевыми параметрами. Ответ на эти случаи можно найти здесь: stackoverflow.com/a/46979194/212749 - person Mene; 27.10.2017

Я думаю, лучший и понятный способ - использовать метод InvokerHelper.setProperties.

Пример:

import groovy.transform.ToString
import org.codehaus.groovy.runtime.InvokerHelper

@ToString
class User {
    String name = 'Arturo'
    String city = 'Madrid'
    Integer age = 27
}

@ToString
class AdminUser {
    String name
    String city
    Integer age
}

def user = new User()
def adminUser = new AdminUser()

println "before: $user $adminUser"
InvokerHelper.setProperties(adminUser, user.properties)
println "after : $user $adminUser"

Вывод:

before: User(Arturo, Madrid, 27) AdminUser(null, null, null)
after : User(Arturo, Madrid, 27) AdminUser(Arturo, Madrid, 27)

Примечание. Если вам нужна более удобная читаемость, вы можете использовать категорию

use(InvokerHelper) {
    adminUser.setProperties(user.properties) 
}
person Michal Z m u d a    schedule 10.05.2014
comment
Спасибо за InvokerHelper, какое-то время боролся с этой проблемой. - person dbrin; 12.03.2015
comment
Я подумал, что это такая хорошая идея, и она сработала в случае DomainUnitTest, а затем, без особого предупреждения, провалилась в тесте интеграции служб! гррр .... почему! - person Brent Fisher; 19.03.2021
comment
Хорошо, оказывается, у меня они были наоборот, и мой тест просто проверял на равенство, если два. копия превратила их обоих в ноль! - person Brent Fisher; 19.03.2021

Другой способ сделать:

def copyProperties( source, target ) {
  [source,target]*.getClass().declaredFields*.grep { !it.synthetic }.name.with { a, b ->
    a.intersect( b ).each {
      target."$it" = source."$it"
    }
  }
}

Которая получает общие свойства (которые не являются синтетическими полями), а затем назначает их целевому объекту.


Вы также можете (используя этот метод) сделать что-то вроде:

def user = new User()

def propCopy( src, clazz ) {
  [src.getClass(), clazz].declaredFields*.grep { !it.synthetic }.name.with { a, b ->
    clazz.newInstance().with { tgt ->
      a.intersect( b ).each {
        tgt[ it ] = src[ it ]
      }
      tgt
    }
  }
}


def admin = propCopy( user, AdminUser )
assert admin.name == 'Arturo'
assert admin.city == 'Madrid'
assert admin.age == 27

Таким образом, вы передаете методу объект, из которого нужно скопировать свойства, и класс возвращаемого объекта. Затем метод создает новый экземпляр этого класса (при условии, что конструктор без аргументов), устанавливает свойства и возвращает его.


Редактировать 2

Предполагая, что это классы Groovy, вы можете вызвать конструктор Map и установить все общие свойства следующим образом:

def propCopy( src, clazz ) {
  [src.getClass(), clazz].declaredFields*.grep { !it.synthetic }.name.with { a, b ->
    clazz.metaClass.invokeConstructor( a.intersect( b ).collectEntries { [ (it):src[ it ] ] } )
  }
}
person tim_yates    schedule 31.01.2012
comment
В дополнение к конструктору карты также можно назначать свойства как карту существующему экземпляру с помощью MetaClass.setProperties: stackoverflow.com/a/8507884 / 190201 - person ataylor; 31.01.2012

Spring BeanUtils.copyProperties будет работать, даже если исходный / целевой классы относятся к разным типам. http://docs.spring.io/autorepo/docs/spring/3.2.3.RELEASE/javadoc-api/org/springframework/beans/BeanUtils.html

person kazuar    schedule 22.04.2015