vrijdag 18 december 2015

A bit of Reflection

Introduction

In all the years I have been programming one of the dullest things I found to do was writing and maintaining converters. I had a couple of discussion about the subject. The point that always came up as a defence to not use bean mappers like Dozer was the fact that they used Reflection under the hood. I agree up to some level that the use of Reflection in production code should not be used in production code. The interesting point is that most of the third party libraries are using Reflection under the hood. Every library that uses Annotations for example are using it. Think about Spring, all the JPA libraries, log4j, Junit etc...
So why not for something like bean mapping. So lets see if we can build one our selfs.


The solution

A bean mapper is meant to  copy values from one bean to the other. So what you need to accomplish this is to know the field names of the "other" bean.

 The solution is actually not that complicated What I did was create a couple of Annotations and a handler.

If the field names are different you can pass the name of the fields in the "other" bean by using an annotation.
The Annotations look like this:


//The rentention policy tells when this Annotation should be used.
@Retention(RetentionPolicy.RUNTIME)
//The target tells where the annation should be placed. In this case field only.
@Target(ElementType.FIELD)
public @interface Copy {
    // the string that contains the value given to the Annotation.
    public String copyTo();

}

If the field names are the same you can do it without an annotation. 
I show that later.

If you dont want the fields to be copied you can use simply another annotation that prevents the field to be part of the copy process:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface IgnoreCopy {
}

The annotation in use look something like this:

// copying with a different field name
@Copy(copyTo = "testString2")
private String testString;
// copying wit the same fieldname
private String testStringNoAnnotation;
 
should not be copied.
@IgnoreCopyprivate String notToCopy;


To copy the values first of all you need the fields of the bean to copy from:
//Use the getDeclaredFields instead of the getFields. Otherwise you can't find 
//the field you are searching for. 
Field[] fields = copyFrom.getClass().getDeclaredFields();

//looping through the fields to find the annotation if needed.
for(Field field : fields) {
    //The annotation if the field names are different.
    Copy copy = field.getAnnotation(Copy.class);
    //The annotation to see if the field shouldn't be copied. 
    IgnoreCopy ignoreCopy = field.getAnnotation(IgnoreCopy.class);
    if(ignoreCopy == null) {
        if ( copy != null) {
            //the actual method to copy with different field name 
            addValueToField(copyFrom,copyTo, field, copy.copyTo());
        } else {
            //the actual method to copy with the same  field name 
            addValueToField(copyFrom, copyTo, field, field.getName());
        }
    }
}


private void addValueToField(final Object copyFrom, 
final Object copyTo, final Field field, final String name) {
    try {
        // Searching for the field in the "other" bean
        Field copyToField = copyTo.getClass().getDeclaredField(name);
        // The fields are private so we make them accessible
        copyToField.setAccessible(true);
        field.setAccessible(true);
        // The actual copying of the values
        copyToField.set(copyTo, field.get(copyFrom));
        //stop the accessibility of the fields. 
        copyToField.setAccessible(false);
        field.setAccessible(false);
    } catch (NoSuchFieldException e) {
        log.error("could not find field " + name);
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        log.error("could not access field " + name);
        e.printStackTrace();
    }
}
 
 
 
// I added two more methods that are helpfull with the cherry picking. 
  
/** * checks if ignorefield is set in the target.class 
* @param copyTo the target class. 
* @param fieldName the fieldname to be set. 
* @return a boolean if the field is right to set. 
*/
 private boolean ignoreCopyToField(final Object copyTo, final String fieldName) {
    Field field = null;
    // A list of fieldnames to prevent the NoSuchFieldException
         List<String> fieldNames = checkFieldExists(copyTo);
        IgnoreCopy ignoreCopy = null;
        try {
            // check if the field exists in the target class. 
           if (fieldNames.contains(fieldName)) {
                // retrieve the field with the name 
                field = copyTo.getClass().getDeclaredField(fieldName);
                // retrieve the annotation from the field
                ignoreCopy = field.getAnnotation(IgnoreCopy.class);
            }
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }        return  ignoreCopy != null;
}

/** * creates a list that has all the fieldnames in it. 
* @param copyTo the target class 
* @return a list that has all the fieldnames in it 
*/ 
private List<String> checkFieldExists(Object copyTo) {
    List<String> fieldNames = new ArrayList<String>();
    Field[] fields = copyTo.getClass().getDeclaredFields();
    for (Field field1: fields) {
        fieldNames.add(field1.getName());
    }
    return fieldNames;
}
 

Conclusion

The choice to be made is more of a philosophic one. Is it worth to add some more reflection to the project at hand. I think the decision is to use it where it is useful. If it can save you writing and maintaining miles of boring code my advice would be to do it. In many cases it allready happens. So think about and work this out for your self. The code to build this is short and simple.

The original source code can be downloaded  here
The jar file can ben downloaded  here
Have fun!


Geen opmerkingen:

Een reactie posten