Search code examples
javabytecodebyte-buddy

Annotations to transform DTO to Entity using Byte Buddy


I have a simple entity User.

public class User {

  String name;

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

}

And his corresponding DTO

public class UsuarioDTO {

  String name;

  String getName(){
    return this.name;
  }

  public void setName(String name) {
    this.name = name;
  }

}

I want to achieve something like I show below to avoid multiple classes of transformers.

@Dto(entity = "Usuario")
public class UsuarioDTO {

  @BasicElement(name = "name")
  String name;

  String getName(){
    return this.name;
  }

  public void setName(String name) {
    this.name = name;
  }

}

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

  String name();

}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Dto {

  String entity() default "";

}

With this example classes I can do:

public class Transformer {

  public static void main(String[] args) {
    UserDTO usuarioDTO = new UserDTO("Gabriel");

    Class<UserDTO> obj = UserDTO.class;

    if (obj.isAnnotationPresent(Dto.class)) {

      Dto annotation = obj.getAnnotation(Dto.class);

      Class<?> clazz;
      try {
        clazz = Class.forName(annotation.entity());
        Constructor<?> constructor = clazz.getConstructor();
        Object instance = constructor.newInstance();

        for (Field originField : UserDTO.class.getDeclaredFields()) {
          originField.setAccessible(true);
          if (originField.isAnnotationPresent(BasicElement.class)) {
            BasicElement basicElement = originField.getAnnotation(BasicElement.class);
            Field destinationField = instance.getClass().getDeclaredField(basicElement.name());
            destinationField.setAccessible(true);
            destinationField.set(instance, originField.get(usuarioDTO));

          }
        }
        System.out.println(((User) instance).getName());

      } catch (Exception e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
      }

    }
  }
}

But this would be to expensive because consumes the annotations in each transformation.

It's possible with Byte-buddy to read the annotations and create a class transformer whose decompiled code look like this:

public class TransformerImpl implements ITransformer{
  public Object toEntity(Object dto){
    User user = new User();
    user.setName(dto.getName());
  }
}

UPDATE: @Rafael Winterhalter, something like this?

public class Transformer<D,E> {

    List<Field> dtoFields = new ArrayList<Field>();

    Constructor<D> dtoConstructor;

    List<Field> entityFields = new ArrayList<Field>();

    Constructor<E> entityConstructor;

    public Transformer(Class<D> dtoClass){
        try {
            Dto annotation = dtoClass.getAnnotation(Dto.class);
            Class<E> entityClass = (Class<E>) annotation.entity();
            //entityConstructor = entityClass.getConstructor();
            entityConstructor = entityClass.getDeclaredConstructor();
            entityConstructor.setAccessible(true);
            dtoConstructor = dtoClass.getConstructor();
            dtoConstructor.setAccessible(true);
            lookupFields(entityClass, dtoClass);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void lookupFields(Class<E> entityClass, Class<D> dtoClass) throws NoSuchFieldException {
        for (Field dtoField : dtoClass.getDeclaredFields()) {
            if (dtoField.isAnnotationPresent(BasicElement.class)) {
                BasicElement basicElement = dtoField.getAnnotation(BasicElement.class);
                String entityFieldName = (basicElement.name().equals("")) ? dtoField.getName() : basicElement.name();
                Field entityField = entityClass.getDeclaredField(entityFieldName);
                dtoField.setAccessible(true);
                entityField.setAccessible(true);
                dtoFields.add(dtoField);
                entityFields.add(entityField);
            }
        }
    }

    public E toEntity(D dto) throws ReflectiveOperationException {
        E entity = entityConstructor.newInstance();
        for (int i = 0; i < entityFields.size(); i++){
            Field destination = entityFields.get(i);
            Field origin = dtoFields.get(i);
            destination.set(entity, origin.get(dto));
        }
        return entity;
    }

    public D toDto(E entity) throws ReflectiveOperationException {
        D dto = dtoConstructor.newInstance();
        for (int i = 0; i < entityFields.size(); i++){
            Field origin = entityFields.get(i);
            Field destination = dtoFields.get(i);
            destination.set(dto, origin.get(entity));
        }
        return dto;
    }
}

Solution

  • To answer your question: Yes, it is possible. You can ask Byte Buddy to create instances of ITransformer for you where you implement the only method to do what you want. You would however need to implement your own Implementation instance for doing so.

    However, I would not recommend you to do so. I usually tell users that Byte Buddy should not be used for performance work and for a majority of use-cases, this is true. Your use case is one of them.

    If you implemented classes, you would have to cache these classes for any mapping. Otherwise, the class generation-costs would be the significant share. Instead, you rather want to maintain a transformer that caches the objects of the reflection API (reflective lookups are the expensive part of your operation, reflective invocation is not so problematic) and reuses previously looked-up values. This way, you gain on performance without dragging in code generation as another (complex) element of your application.