Automatické kopírování entitních objektů do DTO objektů

Před časem jsme vytvořili návrh aplikace, která mezi prezentační a aplikační vrstvou, namísto entitních objektů, přenáší tzv. DTO (Data Transfer Object) objekty. Základní výhodou tohoto přístupu předávání objektů je úplná nezávislost aplikační vrstvy na vrstvě prezentační. Při vývoji bylo nutné si co nejvíce usnadnit práci s kopírováním dat z entitního objektu do DTO objektu.

DTO objekt může nést atributy z jedné nebo i z několika entit. Cílem bylo vytvoření utilitky, která na vstup dostane entitní a DTO objekt a překopíruje data z entitního objektu do DTO objektu. Toto nám usnadní „ruční“ kopírování a neustálé psaní dto.setValue(entita.getValue). Alternativou k této funkčnosti je samozřejmě funkčnost opačná, tedy přenost dat z DTO objektu do entitního objektu.

Dalšími požadavky na funkčnost jsou:
1) možnost nastavit atribut DTO třídy tak, že se ho utilita nebude snažit nastavovat hodnotou z entitní třídy
2) možnost určit u atributu DTO třídy entitní třídu, ze které se bude nastavovat jeho hodnota

Právě podobně nastíněný problém je snadné vyřešit díky tzv. reflexi v kombinaci s vytvořením vlastních anotací.

Pro splnění požadavku (1) jsme si vytvořili následující anotaci:

package eu.bcv;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
public @interface BCVNonCopy {

}

Anotace @Retention nám říká, že naše anotace bude dostupná za běhu.

Pro splnění požadavku (2) jsme si vytvořili další anotaci:

package eu.bcv;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
public @interface BCVCopy {

	public Class entityClass() default Object.class;
	
}

Entitní objekt, který budeme v tomto vzorovém příkladu používat vypadá takto:

package eu.bcv.entity;

public class IdentityEntity {
	private String id;
	private Integer age;
	private boolean admin;
	private double data;
	private Boolean admin2;

	public IdentityEntity() {
		
	}

	// dale gettery a settery
}


DTO objekt, do kterého budeme kopírovat data vypadá takto:

package eu.bcv.dto;

import eu.bcv.BCVCopy;
import eu.bcv.BCVNonCopy;
import eu.bcv.entity.IdentityEntity;
import eu.bcv.entity.RoleEntity;

@BCVCopy(entityClass=IdentityEntity.class)
public class IdentityDTO {

	private String id;
	private Integer age;
	
	@BCVCopy(entityClass=RoleEntity.class)
	private String roleName;
	
	private boolean admin;
	
	private Boolean admin2;
	
	@BCVNonCopy
	private IdentityDTO parent = null;

	public IdentityDTO(String id, Integer age, String roleName, boolean admin,
			Boolean admin2, IdentityDTO parent) {
		super();
		this.id = id;
		this.age = age;
		this.roleName = roleName;
		this.admin = admin;
		this.admin2 = admin2;
		this.parent = parent;
	}

	public IdentityDTO(){
	}
	
	// dale gettery a settery
	
}

Další (a poslední) entitou, na které si budeme ukazovat řešení je třída RoleEntity:

package eu.bcv.entity;

public class RoleEntity {

	private String roleName;

	public RoleEntity(String roleName) {
		super();
		this.roleName = roleName;
	}
}

Nyní máme následující cíl: Z databáze si načteme entitní objekt IdentityEntity a RoleEntity. Chceme na prezentační vrstvu poslat DTO objekt, který bude obsahovat atributy z těchto dvou entit.

Podíváme-li se na třídu IdentityDTO tak zjistíme, že je anotovaná @BCVCopy(entityClass=IdentityEntity.class). To nám říká, že všechny atributy (není-li u jednotlivých atributů nastaveno jinak) budou nastavovány těmi atributy, které jsou ve třídě IdentityEntity a mají stejný název. Vyjímkou je atribut roleName, který bude nastavován hodnotou ze třídy RoleEntity. Atribut parent nebude automaticky nastavován vůbec.

Následující třída ReflectionUtils vyřeší zadaný úkol:

package eu.bcv;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

public class ReflectionUtils {
	
	/**
	 * Fill data from data transfer object into entity
	 * @param entity
	 * @param dto
	 * @return
	 * @throws Exception
	 */
	public static Object fillDTO(Object entity, Object dto) throws Exception {
		// Find class annotation
		BCVCopy bcvCopy = dto.getClass().getAnnotation(BCVCopy.class);
		
		for (Field field : dto.getClass().getDeclaredFields()) {
			if (field.getAnnotation(BCVNonCopy.class) != null) {
				//Field will not be copy
				continue;
			}

			BCVCopy attBcvCopy = field.getAnnotation(BCVCopy.class);
			if (field.getAnnotation(BCVCopy.class) == null) {
				if (entity.getClass() == bcvCopy.entityClass()) {
					//Default class is same as entity class
					dto = copyField(dto, entity, field);
				}
			} else {
				//Field has your own annotation
				if (entity.getClass() == attBcvCopy.entityClass()) {
					//Field´s annotation contain same class as entity class
					dto = copyField(dto, entity, field);
				}
			}
		}

		return dto;
	}

	public static Object fillEntity(Object entity, Object dto) throws Exception {
		// Find class annotation
		BCVCopy bcvCopy = dto.getClass().getAnnotation(BCVCopy.class);
		
		for (Field field : dto.getClass().getDeclaredFields()) {
			if (field.getAnnotation(BCVNonCopy.class) != null) {
				//Field will not be copy
				continue;
			}

			BCVCopy attBcvCopy = field.getAnnotation(BCVCopy.class);
			if (field.getAnnotation(BCVCopy.class) == null) {
				if (entity.getClass() == bcvCopy.entityClass()) {
					//Default class is same as entity class
					entity = copyField(entity, dto, field);
				}
			} else {
				//Field has your own annotation
				if (entity.getClass() == attBcvCopy.entityClass()) {
					//Field´s annotation contain same class as entity class
					entity = copyField(entity, dto, field);
				}
			}
		}
		return entity;
	}

	/**
	 * Copy field from source object into target object
	 * @param target
	 * @param source
	 * @param field
	 * @return
	 * @throws Exception
	 */
	private static Object copyField(Object target, Object source, Field field)
			throws Exception {
		String[] gettersParam = { "get", "is" };
		List sourceGetters = ReflectionUtils.findMethods(source
				.getClass().getMethods(), gettersParam);

		Method getter = null;
		Method setter = null;

		String getterName = constructGetterName(field);
		//Find getter in sourceGetters
		for (Method get : sourceGetters) {
			if (get.getName().equals(getterName)) {
				getter = get;
				break;
			}
		}

		String[] settersParam = { "set" };
		List targetSetters = ReflectionUtils.findMethods(target
				.getClass().getMethods(), settersParam);

		String setterName = constructSetterName(field);
		//Find setter in sourceSetters
		for (Method set : targetSetters) {
			if (set.getName().equals(setterName)) {
				// System.out.println("Nalezeno " + setterName);
				setter = set;
				break;
			}
		}

		if (getter == null || setter == null) {
			throw new Exception("No setter or getter found");
		} else {
			Object param = getter.invoke(source);
			setter.invoke(target, param);
		}

		return target;
	}

	/**
	 * Create getter name for input Field
	 * @param field
	 * @return
	 */
	private static String constructGetterName(Field field) {
		String name = field.getName();
		String prefix = "get";
		if (field.getGenericType().toString().equals("boolean")) {
			prefix = "is";
		}
		return prefix + name.substring(0, 1).toUpperCase()
				+ name.substring(1, name.length());
	}

	/**
	 * Create setter name for input Field
	 * @param field
	 * @return
	 */
	private static String constructSetterName(Field field) {
		String name = field.getName();
		return "set" + name.substring(0, 1).toUpperCase()
				+ name.substring(1, name.length());
	}
	
	/**
	 * Find methods in array which prefix equals input prefixs
	 * @param methods
	 * @param prefix
	 *            Example: "get" or "set"
	 * @return
	 */
	private static List findMethods(Method[] methods, String[] prefixs) {
		List result = new ArrayList();

		for (Method method : methods) {
			String name = method.getName();
			for (String prefix : prefixs) {
				if (name.startsWith(prefix)) {
					result.add(method);
					break;
				}
			}
		}

		return result;
	}

}

Nyní použijeme ReflectionUtils např. následujícím způsobem:

                IdentityEntity iEntita = new IdentityEntity();
		iEntita.setId("novaIdentita");
		iEntita.setAdmin(true);
		iEntita.setAdmin2(true);
		iEntita.setAge(23);
		iEntita.setData(3.3);
		
		RoleEntity rRole = new RoleEntity("BCV_blog");
		IdentityDTO iDTO = new IdentityDTO();
		
		iDTO = (IdentityDTO) ReflectionUtils.fillDTO(iEntita, iDTO);
		iDTO = (IdentityDTO) ReflectionUtils.fillDTO(rRole, iDTO);
		
		System.out.println("ID: " + iDTO.getId());
		System.out.println("AGE: " + iDTO.getAge());
		System.out.println("ADMIN: " + iDTO.isAdmin());
		System.out.println("ADMIN2: " + iDTO.getAdmin2());
		System.out.println("Role name: " + iDTO.getRoleName());

Tzv. vytvoříme entity IdentityEntity a RoleEntity (představme si, že jsme objekty získali z datové vrstvy). Vytvoříme prázdný DTO objekt. DTO předáme společně s entitními objekty třídě ReflectionUtils. Tato třída nám nasetuje do DTO objektu data. Pro kontrolu výsledku si necháme hodnoty DTO vypsat (v tomto případě do konzole).

A co se vlastně děje v ReflectionUtils? Z naší ukázky voláme metodu fillDTO. V té nejprve načteme anotaci BCVCopy, kterou je anotován DTO objekt. Poté procházíme všechny atributy DTO objektu a kopírujeme ty, pro které je splněna jedna z podmínek:

  • Atribut nemá anotaci BCVNonCopy a entitní třída na vstupu je shodná s hodnotou, kterou jsme získali z anotace BCVCopy DTO objektu.
  • Atribut nemá anotaci BCVNonCopy, atribut je anotován anotací BCVCopy, jejíž hodnota je shodná se zadanou entitní třídou.

Kopírování zajišťuje metoda copyField. Ta na vstupu dostane cílový a zdrojový objekt a dále atribut, který se má zkopírovat. Pomocí třídy findMethods nalezneme v entitní třídě gettery (procházíme všechny metody a vybíráme ty, které mají prefix „get“ nebo „is“). Poté z názvu atributu, který se bude kopírovat vytvoříme název getteru a vyhledáme getter v seznamu gettrů. Obdobně postupujeme pro nalezení setteru.

Pokud jsme nalezli ve zdrojovém objektu getter a v cílovém objektu setter, získáme hodnotu ze zdroje pomocí konstrukce getter.invoke(source). Nastavení provedeme pomocí konstrukce setter.invoke(target, param).

V závěru bych rád podotknul, že publikovaná verze není finální a kód by se dal napsat efektivněji. Berme tuto verzi spíše jako verzi výukovou.

Celá síle reflexe je zde tedy demostrována na jednom případu – zavolání metody. Tato zdánlivá banalita nám však vývoj velice usnadnila a zpříjemnila, neboť nebylo nutné všechny atributy kopírovat ručně.