Is there an easy way to copy an object's property's onto another object of a different class which has the same field names using direct field access - i.e. when one of the classes does not have getters or setters for the fields? I can use org.springframework.beans.BeanUtils#copyProperties(Object source, Object target) when they both have getter and setter methods, but what can I do when they don't?
It may also be relevant that the fields are public.
I know that I can write my own code to do this using reflection, but I'm hoping that there's some library that provides a one-liner.
I didn't find a 3rd-party library to do this quite how I wanted. I'll paste my code here in case it is useful to anyone:
import java.lang.reflect.Field;
import java.util.AbstractMap;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* An alternative to Spring's BeanUtils#copyProperties for classes that don't have getters and setters.
*/
public class FieldCopier {
private static final Logger log = LoggerFactory.getLogger(FieldCopier.class);
/** Always use the same instance, so that we can cache the fields. */
private static final FieldCopier instance = new FieldCopier();
/** Caching the paired fields cuts the time taken by about 25% */
private final Map<Map.Entry<Class<?>, Class<?>>, Map<Field, Field>> PAIRED_FIELDS = new ConcurrentHashMap<>();
/** Caching the fields cuts the time taken by about 50% */
private final Map<Class<?>, Field[]> FIELDS = new ConcurrentHashMap<>();
public static FieldCopier instance() {
return instance;
}
private FieldCopier() {
// do not instantiate
}
public <S, T> T copyFields(S source, T target) {
Map<Field, Field> pairedFields = getPairedFields(source, target);
for (Field sourceField : pairedFields.keySet()) {
Field targetField = pairedFields.get(sourceField);
try {
Object value = getValue(source, sourceField);
setValue(target, targetField, value);
} catch(Throwable t) {
throw new RuntimeException("Failed to copy field value", t);
}
}
return target;
}
private <S, T> Map<Field, Field> getPairedFields(S source, T target) {
Class<?> sourceClass = source.getClass();
Class<?> targetClass = target.getClass();
Map.Entry<Class<?>, Class<?>> sourceToTarget = new AbstractMap.SimpleImmutableEntry<>(sourceClass, targetClass);
PAIRED_FIELDS.computeIfAbsent(sourceToTarget, st -> mapSourceFieldsToTargetFields(sourceClass, targetClass));
Map<Field, Field> pairedFields = PAIRED_FIELDS.get(sourceToTarget);
return pairedFields;
}
private Map<Field, Field> mapSourceFieldsToTargetFields(Class<?> sourceClass, Class<?> targetClass) {
Map<Field, Field> sourceFieldsToTargetFields = new HashMap<>();
Field[] sourceFields = getDeclaredFields(sourceClass);
Field[] targetFields = getDeclaredFields(targetClass);
for (Field sourceField : sourceFields) {
if (sourceField.getName().equals("serialVersionUID")) {
continue;
}
Field targetField = findCorrespondingField(targetFields, sourceField);
if (targetField == null) {
log.warn("No target field found for " + sourceField.getName());
continue;
}
if (Modifier.isFinal(targetField.getModifiers())) {
log.warn("The target field " + targetField.getName() + " is final, and so cannot be written to");
continue;
}
sourceFieldsToTargetFields.put(sourceField, targetField);
}
return Collections.unmodifiableMap(sourceFieldsToTargetFields);
}
private Field[] getDeclaredFields(Class<?> clazz) {
FIELDS.computeIfAbsent(clazz, Class::getDeclaredFields);
return FIELDS.get(clazz);
}
private <S> Object getValue(S source, Field sourceField) throws IllegalArgumentException, IllegalAccessException {
sourceField.setAccessible(true);
return sourceField.get(source);
}
private <T> void setValue(T target, Field targetField, Object value) throws IllegalArgumentException, IllegalAccessException {
targetField.setAccessible(true);
targetField.set(target, value);
}
private Field findCorrespondingField(Field[] targetFields, Field sourceField) {
for (Field targetField : targetFields) {
if (sourceField.getName().equals(targetField.getName())) {
if (sourceField.getType().equals(targetField.getType())) {
return targetField;
} else {
log.warn("Different types for field " + sourceField.getName()
+ " source " + sourceField.getType() + " and target " + targetField.getType());
return null;
}
}
}
return null;
}
}
Write a simple utility class for that and you got your one liner... this task is IMHO to easy to use a library for it.
Just keep in mind to make your fields accessible if they aren't by default. Here are two functions you could adapt from our codebase:
public void injectIntoObject(Object o, Object value) {
try {
getField().set(o, value);
} catch (IllegalArgumentException e) {
throw new RuntimeException("Illegal argument while injecting property '"+name+"' of class '"+beanDef.getName()+"' in object '"+o+"' to '"+value+"'. Got one of type "+value.getClass().getCanonicalName()+" but needed one of "+type.getCanonicalName()+"!",e);
} catch (IllegalAccessException e) {
getField().setAccessible(true);
try {
getField().set(o, value);
} catch (IllegalArgumentException e1) {
throw new RuntimeException("Illegal argument while injecting property '"+name+"' of class '"+beanDef.getName()+"' in object '"+o+"' to '"+value+"'. Got one of type "+value.getClass().getCanonicalName()+" but needed one of "+type.getCanonicalName()+"!",e);
} catch (IllegalAccessException e1) {
throw new RuntimeException("Access exception while injecting property '"+name+"' of class '"+beanDef.getName()+"' in object '"+o+"' to '"+value+"'!",e);
}
} catch (Exception e) {
throw new RuntimeException("Exception while setting property '"+name+"' of class '"+beanDef.getName()+"' in object '"+o+"' to '"+value+"'!",e);
}
}
public Object extractFromObject(Object o) {
try {
return getField().get(o);
} catch (IllegalArgumentException e) {
throw new RuntimeException("Illegal argument while read property '"+name+"' of class '"+beanDef.getName()+"' in object '"+o+"' but needed one of "+type.getCanonicalName()+"!",e);
} catch (IllegalAccessException e) {
getField().setAccessible(true);
try {
return getField().get(o);
} catch (IllegalArgumentException e1) {
throw new RuntimeException("Illegal argument while read property '"+name+"' of class '"+beanDef.getName()+"' in object '"+o+"' but needed one of "+type.getCanonicalName()+"!",e);
} catch (IllegalAccessException e1) {
throw new RuntimeException("Access exception while read property '"+name+"' of class '"+beanDef.getName()+"' in object '"+o+"'!",e);
}
} catch (Exception e) {
throw new RuntimeException("Exception while read property '"+name+"' of class '"+beanDef.getName()+"' in object '"+o+"'!",e);
}
}
getField() returns a java.lang.Field, should be easy to implement.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With