Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Copy object properties by direct field access

Tags:

java

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.

like image 876
whistling_marmot Avatar asked Oct 28 '25 12:10

whistling_marmot


2 Answers

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;
    }
}
like image 81
whistling_marmot Avatar answered Oct 30 '25 01:10

whistling_marmot


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.

like image 20
Daniel Avatar answered Oct 30 '25 01:10

Daniel



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!