Copie todos los valores de los campos de una clase a otra mediante la reflexión


82

Tengo una clase que es básicamente una copia de otra clase.

public class A {
  int a;
  String b;
}

public class CopyA {
  int a;
  String b;
}

Lo que estoy haciendo es poner los valores de la clase Aen CopyAantes de enviarlos CopyAa través de una llamada de servicio web. Ahora me gustaría crear un método de reflexión que básicamente copia todos los campos que son idénticos (por nombre y tipo) de una clase Aa otra CopyA.

¿Cómo puedo hacer esto?

Esto es lo que tengo hasta ahora, pero no funciona del todo. Creo que el problema aquí es que estoy tratando de establecer un campo en el campo que estoy recorriendo.

private <T extends Object, Y extends Object> void copyFields(T from, Y too) {

    Class<? extends Object> fromClass = from.getClass();
    Field[] fromFields = fromClass.getDeclaredFields();

    Class<? extends Object> tooClass = too.getClass();
    Field[] tooFields = tooClass.getDeclaredFields();

    if (fromFields != null && tooFields != null) {
        for (Field tooF : tooFields) {
            logger.debug("toofield name #0 and type #1", tooF.getName(), tooF.getType().toString());
            try {
                // Check if that fields exists in the other method
                Field fromF = fromClass.getDeclaredField(tooF.getName());
                if (fromF.getType().equals(tooF.getType())) {
                    tooF.set(tooF, fromF);
                }
            } catch (SecurityException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (NoSuchFieldException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (IllegalArgumentException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }

        }
    }

Estoy seguro de que debe haber alguien que ya haya hecho esto de alguna manera.



Sí o los BeanUtils de Apache Jakarta.
Shaun F

Respuestas:


102

Si no le importa usar una biblioteca de terceros, BeanUtils de Apache Commons lo manejará con bastante facilidad, usando copyProperties(Object, Object).


13
Aparentemente, BeanUtils no funciona con campos de fecha nulos. Use Apache PropertyUtils si esto es un problema para usted: mail-archive.com/user@commons.apache.org/msg02246.html
ripper234

10
Aparentemente, esto no funciona para campos privados sin captadores y definidores. ¿Alguna solución que funcione directamente con campos, en lugar de propiedades?
Andrea Ratto

Tampoco funciona con campos públicos simples sin captadores: stackoverflow.com/questions/34263122/…
Vadzim

17

¿Por qué no utiliza la biblioteca gson https://github.com/google/gson

simplemente convierta la cadena Class A a json. Luego convierta jsonString a su subclase (CopyA). Usando el siguiente código:

Gson gson= new Gson();
String tmp = gson.toJson(a);
CopyA myObject = gson.fromJson(tmp,CopyA.class);

¿Por qué generar otra cadena que también podría ser grande? Hay mejores alternativas descritas como respuestas aquí. Al menos nosotros (la industria) progresamos de XML a json para las representaciones de cadenas, pero todavía no queremos que todo se pase a esa representación de cadenas en ninguna oportunidad ...
arntg

Tenga en cuenta que la cuerda es un producto secundario cuando se usa la reflexión. ¡Incluso aunque no lo salvaste! Esta es una respuesta para principiantes de Java, y su objetivo es ser breve y claro. @arntg
Eric Ho

Todavía necesitaría reflexionar sobre los objetos de origen y destino, pero ahora también está introduciendo el formato binario / texto / binario y la sobrecarga de análisis.
Evvo

Preste atención si usa Proguard para ofuscar el código. Si lo usa, este código no funcionará.
SebastiaoRealino

8

BeanUtils solo copiará campos públicos y es un poco lento. En su lugar, utilice los métodos getter y setter.

public Object loadData (RideHotelsService object_a) throws Exception{

        Method[] gettersAndSetters = object_a.getClass().getMethods();

        for (int i = 0; i < gettersAndSetters.length; i++) {
                String methodName = gettersAndSetters[i].getName();
                try{
                  if(methodName.startsWith("get")){
                     this.getClass().getMethod(methodName.replaceFirst("get", "set") , gettersAndSetters[i].getReturnType() ).invoke(this, gettersAndSetters[i].invoke(object_a, null));
                        }else if(methodName.startsWith("is") ){
                            this.getClass().getMethod(methodName.replaceFirst("is", "set") ,  gettersAndSetters[i].getReturnType()  ).invoke(this, gettersAndSetters[i].invoke(object_a, null));
                        }

                }catch (NoSuchMethodException e) {
                    // TODO: handle exception
                }catch (IllegalArgumentException e) {
                    // TODO: handle exception
                }

        }

        return null;
    }

BeanUtils funciona bien en campos privados, siempre que los getters / setters sean públicos. Con respecto al rendimiento, no he realizado ninguna evaluación comparativa, pero creo que sí almacena en caché interno el bean que ha introspectado.
Greg Case

2
esto solo funcionará si los dos beans tienen el mismo tipo de datos de campos.
TimeToCodeTheRoad

@Para Kra Funcionaría solo si tiene un getter / setter para ese campo.
WoLfPwNeR

8

Aquí hay una solución probada y funcional. Puede controlar la profundidad del mapeo en la jerarquía de clases.

public class FieldMapper {

    public static void copy(Object from, Object to) throws Exception {
        FieldMapper.copy(from, to, Object.class);
    }

    public static void copy(Object from, Object to, Class depth) throws Exception {
        Class fromClass = from.getClass();
        Class toClass = to.getClass();
        List<Field> fromFields = collectFields(fromClass, depth);
        List<Field> toFields = collectFields(toClass, depth);
        Field target;
        for (Field source : fromFields) {
            if ((target = findAndRemove(source, toFields)) != null) {
                target.set(to, source.get(from));
            }
        }
    }

    private static List<Field> collectFields(Class c, Class depth) {
        List<Field> accessibleFields = new ArrayList<>();
        do {
            int modifiers;
            for (Field field : c.getDeclaredFields()) {
                modifiers = field.getModifiers();
                if (!Modifier.isStatic(modifiers) && Modifier.isPublic(modifiers)) {
                    accessibleFields.add(field);
                }
            }
            c = c.getSuperclass();
        } while (c != null && c != depth);
        return accessibleFields;
    }

    private static Field findAndRemove(Field field, List<Field> fields) {
        Field actual;
        for (Iterator<Field> i = fields.iterator(); i.hasNext();) {
            actual = i.next();
            if (field.getName().equals(actual.getName())
                && field.getType().equals(actual.getType())) {
                i.remove();
                return actual;
            }
        }
        return null;
    }
}

1
He creado una solución similar. Guardé en caché una clase para los nombres de campo en el mapa de campos.
Orden

La solución es buena, pero tendrás un problema en esta línea i.remove(). Incluso si ha creado iterador no se puede llamar removede List's iterator. Debería serArrayList
Farid

Farid, eliminar no puede ser un problema porque collectFields () crea objetos ArrayList.
JHead

5

Mi solución:

public static <T > void copyAllFields(T to, T from) {
        Class<T> clazz = (Class<T>) from.getClass();
        // OR:
        // Class<T> clazz = (Class<T>) to.getClass();
        List<Field> fields = getAllModelFields(clazz);

        if (fields != null) {
            for (Field field : fields) {
                try {
                    field.setAccessible(true);
                    field.set(to,field.get(from));
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
    }

public static List<Field> getAllModelFields(Class aClass) {
    List<Field> fields = new ArrayList<>();
    do {
        Collections.addAll(fields, aClass.getDeclaredFields());
        aClass = aClass.getSuperclass();
    } while (aClass != null);
    return fields;
}

No creo que esto no funcione para objetos personalizados. Solo si tiene una clase sin padres y solo campos primitivos
Shervin Asgari

Para cubrir campos de superclase, estoy usando el método personalizado 'getAllModelFields'
Mohsen Kashi

4

El primer argumento tooF.set()debe ser el objeto de destino ( too), no el campo, y el segundo argumento debe ser el valor , no el campo del que proviene el valor. (Para obtener el valor, debe llamar fromF.get(), nuevamente pasando un objeto de destino, en este caso from).

La mayor parte de la API de reflexión funciona de esta manera. Obtienes Fieldobjetos, Methodobjetos, etc. de la clase, no de una instancia, por lo que para usarlos (excepto para estática) generalmente necesitas pasarles una instancia.



4

Esta es una publicación tardía, pero aún puede ser efectiva para las personas en el futuro.

Spring proporciona una utilidad BeanUtils.copyProperties(srcObj, tarObj) que copia valores del objeto de origen al objeto de destino cuando los nombres de las variables miembro de ambas clases son iguales.

Si hay una conversión de fecha, (p. Ej., Cadena a fecha) se copiará "nulo" en el objeto de destino. Entonces podemos establecer explícitamente los valores de la fecha según sea necesario.

El BeanUtils de Apache Commonarroja un error cuando no coinciden los tipos de datos (especialmente la conversión desde y hacia la fecha)

¡Espero que esto ayude!


Esto no proporciona ninguna información adicional a la respuesta aceptada de stackoverflow.com/a/1667911/4589003
Sudip Bhandari

3

Creo que puedes probar la topadora . Tiene un buen soporte para la conversión de frijol a frijol. También es fácil de usar. Puede inyectarlo en su aplicación de primavera o agregar el jar en la ruta de clases y listo.

Para un ejemplo de su caso:

 DozerMapper mapper = new DozerMapper();
A a= new A();
CopyA copyA = new CopyA();
a.set... // set fields of a.
mapper.map(a,copyOfA); // will copy all fields from a to copyA

3
  1. Sin usar BeanUtils o Apache Commons

  2. public static <T1 extends Object, T2 extends Object>  void copy(T1     
    origEntity, T2 destEntity) throws IllegalAccessException, NoSuchFieldException {
        Field[] fields = origEntity.getClass().getDeclaredFields();
        for (Field field : fields){
            origFields.set(destEntity, field.get(origEntity));
         }
    }
    

Esta no es una solución funcional, sino un buen punto de partida. Los campos deben filtrarse para manejar solo los campos públicos y no estáticos que están presentes en ambas clases.
JHead

¿No ignoraría esto los campos en las clases principales?
Evvo

2

Spring tiene un BeanUtils.copyPropertiesmétodo incorporado . Pero no funciona con clases sin getters / setters. La serialización / deserialización de JSON puede ser otra opción para copiar campos. Jackson se puede utilizar para este propósito. Si está utilizando Spring En la mayoría de los casos, Jackson ya está en su lista de dependencias.

ObjectMapper mapper     = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
Clazz        copyObject = mapper.readValue(mapper.writeValueAsString(sourceObject), Clazz.class);

1

Orika es un marco de mapeo de beans más rápido y simple porque lo hace mediante la generación de código de bytes. Hace asignaciones anidadas y asignaciones con diferentes nombres. Para obtener más detalles, consulte aquí El mapeo de muestra puede parecer complejo, pero para escenarios complejos sería simple.

MapperFactory factory = new DefaultMapperFactory.Builder().build();
mapperFactory.registerClassMap(mapperFactory.classMap(Book.class,BookDto.class).byDefault().toClassMap());
MapperFacade mapper = factory.getMapperFacade();
BookDto bookDto = mapperFacade.map(book, BookDto.class);

Esto no hace lo que pide la pregunta. SerializationUtils.clone()va a dar un nuevo objeto de la misma clase. Además, solo funciona en clases serializables.
Kirby


1

Resolví el problema anterior en Kotlin que funciona bien para mí para mi desarrollo de aplicaciones de Android:

 object FieldMapper {

fun <T:Any> copy(to: T, from: T) {
    try {
        val fromClass = from.javaClass

        val fromFields = getAllFields(fromClass)

        fromFields?.let {
            for (field in fromFields) {
                try {
                    field.isAccessible = true
                    field.set(to, field.get(from))
                } catch (e: IllegalAccessException) {
                    e.printStackTrace()
                }

            }
        }
    } catch (e: Exception) {
        e.printStackTrace()
    }

}

private fun getAllFields(paramClass: Class<*>): List<Field> {

    var theClass:Class<*>? = paramClass
    val fields = ArrayList<Field>()
    try {
        while (theClass != null) {
            Collections.addAll(fields, *theClass?.declaredFields)
            theClass = theClass?.superclass
        }
    }catch (e:Exception){
        e.printStackTrace()
    }

    return fields
}

}


0

No quería agregar dependencia a otro archivo JAR debido a esto, así que escribí algo que se adaptara a mis necesidades. Sigo la convención de fjorm https://code.google.com/p/fjorm/ lo que significa que mis campos generalmente accesibles son públicos y que no me molesto en escribir setters y getters. (en mi opinión, el código es más fácil de administrar y más legible en realidad)

Entonces escribí algo (en realidad no es muy difícil) que se adapta a mis necesidades (supone que la clase tiene un constructor público sin argumentos) y podría extraerse en la clase de utilidad

  public Effect copyUsingReflection() {
    Constructor constructorToUse = null;
    for (Constructor constructor : this.getClass().getConstructors()) {
      if (constructor.getParameterTypes().length == 0) {
        constructorToUse = constructor;
        constructorToUse.setAccessible(true);
      }
    }
    if (constructorToUse != null) {
      try {
        Effect copyOfEffect = (Effect) constructorToUse.newInstance();
        for (Field field : this.getClass().getFields()) {
          try {
            Object valueToCopy = field.get(this);
            //if it has field of the same type (Effect in this case), call the method to copy it recursively
            if (valueToCopy instanceof Effect) {
              valueToCopy = ((Effect) valueToCopy).copyUsingReflection();
            }
            //TODO add here other special types of fields, like Maps, Lists, etc.
            field.set(copyOfEffect, valueToCopy);
          } catch (IllegalArgumentException | IllegalAccessException ex) {
            Logger.getLogger(Effect.class.getName()).log(Level.SEVERE, null, ex);
          }
        }
        return copyOfEffect;
      } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
        Logger.getLogger(Effect.class.getName()).log(Level.SEVERE, null, ex);
      }
    }
    return null;
  }

Antipattern: Reinvente la rueda
Spektakulatius

0

La idea básica de Mladen funcionó (gracias), pero necesitaba algunos cambios para ser sólida, así que los aporté aquí.

El único lugar donde se debe usar este tipo de solución es si desea clonar el objeto, pero no puede hacerlo porque es un objeto administrado. Si tiene la suerte de tener objetos que tengan un 100% libre de efectos secundarios para todos los campos, definitivamente debería usar la opción BeanUtils.

Aquí, utilizo los métodos de utilidad de lang3 para simplificar el código, por lo que si lo pega, primero debe importar la biblioteca lang3 de Apache.

Copiar codigo

static public <X extends Object> X copy(X object, String... skipFields) {
        Constructor constructorToUse = null;
        for (Constructor constructor : object.getClass().getConstructors()) {
            if (constructor.getParameterTypes().length == 0) {
                constructorToUse = constructor;
                constructorToUse.setAccessible(true);
                break;
            }
        }
        if (constructorToUse == null) {
            throw new IllegalStateException(object + " must have a zero arg constructor in order to be copied");
        }
        X copy;
        try {
            copy = (X) constructorToUse.newInstance();

            for (Field field : FieldUtils.getAllFields(object.getClass())) {
                if (Modifier.isStatic(field.getModifiers())) {
                    continue;
                }

                //Avoid the fields that you don't want to copy. Note, if you pass in "id", it will skip any field with "id" in it. So be careful.
                if (StringUtils.containsAny(field.getName(), skipFields)) {
                    continue;
                }

                field.setAccessible(true);

                Object valueToCopy = field.get(object);
                //TODO add here other special types of fields, like Maps, Lists, etc.
                field.set(copy, valueToCopy);

            }

        } catch (InstantiationException | IllegalAccessException | IllegalArgumentException
                | InvocationTargetException e) {
            throw new IllegalStateException("Could not copy " + object, e);
        }
        return copy;
}

0
    public <T1 extends Object, T2 extends Object> void copy(T1 origEntity, T2 destEntity) {
        DozerBeanMapper mapper = new DozerBeanMapper();
        mapper.map(origEntity,destEntity);
    }

 <dependency>
            <groupId>net.sf.dozer</groupId>
            <artifactId>dozer</artifactId>
            <version>5.4.0</version>
        </dependency>

Este mapper.map tiene problemas, en una entidad no está copiando las claves primarias
Mohammed Rafeeq

0
public static <T> void copyAvalableFields(@NotNull T source, @NotNull T target) throws IllegalAccessException {
    Field[] fields = source.getClass().getDeclaredFields();
    for (Field field : fields) {
        if (!Modifier.isStatic(field.getModifiers())
                && !Modifier.isFinal(field.getModifiers())) {
            field.set(target, field.get(source));
        }
    }
}

Leemos todos los campos de la clase. Filtra los campos no estáticos y no finales del resultado. Pero puede haber un error al acceder a campos no públicos. Por ejemplo, si esta función está en la misma clase y la clase que se copia no contiene campos públicos, se producirá un error de acceso. La solución puede ser colocar esta función en el mismo paquete o cambiar el acceso a público o en este código dentro del campo de llamada al bucle .setAccessible (true); qué hará que los campos estén disponibles


Si bien este código puede proporcionar una solución a la pregunta, es mejor agregar contexto sobre por qué / cómo funciona. Esto puede ayudar a los futuros usuarios a aprender y aplicar ese conocimiento a su propio código. También es probable que tenga comentarios positivos de los usuarios en forma de votos a favor, cuando se explique el código.
borchvm
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.