Unterschiede finden mit dem Java Annotation Processor (Teil 2)

A complex system that works is invariably found to have evolved from a simple system that worked. A complex system designed from scratch never works and cannot be patched up to make it work. You have to start over with a working simple system.“

John Gall

Im ersten Beitrag wurde die Implementierung eines AnnotationProcessor vorgestellt, der Klassen zum Bean vergleich erzeugt. Um zwei Instanzen einer Klasse ExampleBean zu vergleichen, muss nur ein spezielles Interface definiert werden, um automatisch eine passende Implementierung zu erhalten.

@Diff
public interface ExampleDiffer extends Differ<ExampleBean> {
  @Ignore("age")
  Difference<ExampleBean> diff(ExampleBean previous, ExampleBean next);
}

Durch die Annotation @Diff und die Erweiterung des Interface Differ<T> wird die Klasse ExampleDifferImpl erzeugt. Um spezielle Attribute der Klasse zu ignorieren, kann die Methode mit der @Ignore Annotation versehen werden.

Die erste Implementierung funktioniert schon wie gewünscht, zeigt aber noch diverse Schwächen. Unter anderem benötigen zwei unterschiedliche Vergleichsvarianten eine eigene Interface Definition. Besser wäre es, wenn mehrere Vergleiche in einem Interface definiert werden könnten.

@Diff
public interface ExampleDiffer extends Differ<ExampleBean> {
  @Ignore("age")
  Difference<ExampleBean> diffIgnoringAge(ExampleBean previous, ExampleBean next);

  Difference<ExampleBean> diff(ExampleBean previous, ExampleBean next);
}

In der ersten Version des DiffEvaluatorProcessor wurde die Vergleichsmethode anhand ihres Namen, des Rückgabewertes und der beiden Parameter identifiziert. Da der Name nun variabel sein kann reicht es aus nach Methoden zu suchen, deren beiden Parameter zum Rückgabewert passen.

private List<DiffMethod> extractDiffMethods(TypeElement differInterfaceType) {
  return differInterfaceType.getEnclosedElements().stream()
      .filter(element -> element.getKind() == ElementKind.METHOD).map(ExecutableElement.class::cast)
      .map(this::diffMethod).filter(Objects::nonNull).collect(toList());
}

Innerhalb der Methode diffMethod, wird geprüft, ob die aktuelle Methode alle Bedingungen einer Vergleichsmethode erfüllt und liefert die notwendigen Informationen als DiffMethod Instanz zurück. Die Identifizierung einer Vergleichsmethode hängt nur von der jeweiligen Methode ab, daher kann man den bisherigen Ansatz des DiffEvaluatorProcessor aufweichen. Die Erweiterung des Differ Interfaces ist nicht nötig und kann entfernt werden.

@Diff
public interface ExampleDiffer {
  @Ignore("age")
  Difference<ExampleBean> diffIgnoringAge(ExampleBean previous, ExampleBean next);

  Difference<ExampleBean> diff(ExampleBean previous, ExampleBean next);
}

Da es nun keine Festlegung auf einen speziellen Typ für die Vergleichsklasse gibt, können auch unterschiedliche getypte Vergleiche in einer Klasse stattfinden..

@Diff
public interface PersonAndCarDiffer {
  @Ignore("age")
  Difference<Person> diffIgnoringAge(Person previous, Person next);

  Difference<Person> diff(Person previous, Person next);

  Difference<Car> diff(Car previous, Car next);
}

Der Wechsel von einem einzelnen Bean Typen auf eine Liste ist aber kein großer Sprung mehr. Durch das Bündeln der notwendigen Informationen in DiffMethod Objekte, müssen nur noch alle zu vergleichenden Attribute für die Beans bestimmt werden.

private boolean processDifferInterfaces(TypeElement differInterfaceType) {
  try {
    List<DiffMethod> methods = extractDiffMethods(differInterfaceType);
    processingEnv.getMessager().printMessage(Kind.NOTE, "methods: " + methods);

    Set<DiffBean> beans = methods.stream()
        .collect(groupingBy(DiffMethod::getBeanType, mapping(DiffMethod::getBeanAttributes, toSet())))
        .entrySet().stream()
        .map(e -> new DiffBean(e.getKey(), e.getValue().stream().flatMap(List::stream).collect(toSet())))
        .collect(toSet());

    return createDifferClassFile(differInterfaceType, methods, beans);
  } catch (IllegalArgumentException e) {
    processingEnv.getMessager().printMessage(Kind.ERROR, "cannot create implementation: " + e, differInterfaceType);
    return false;
  }
}

Dies geschieht in der modifizierten processDifferInterfaces Methode in den Zeilen 6 bis 10. Die mittlerweile in den DiffMethod abgelegten Bean Attribute werden nach dem jeweiligen Bean Typ gruppiert und in eine DiffBean Instanz abgelegt. Die DiffMethod Instanzen dienen als Quelle für die Code Generierung der Vergleichsmethoden und die DiffBean Instanzen dienen als Quelle für die Code Generierung der internen Wrapper Klassen.

Mit diesen Änderungen erzeugt der DiffEvaluatorProcessor anhand des Interface PersonAndCarDiffer die folgende Klasse.

/**
* @author Diff Evaluator Generator by Jens Kaiser
*/
package de.schegge;

import java.util.Objects;
import java.util.List;
import de.schegge.diff.AbstractDiffer;
import de.schegge.example.CarBean;
import de.schegge.example.PersonBean;
import de.schegge.diff.PropertyDifference;
import de.schegge.diff.ClassDifference;
import de.schegge.diff.Difference;
import java.util.Collections;
import java.util.ArrayList;

public final class ExampleBeanDifferImpl extends AbstractDiffer implements ExampleBeanDiffer {
  @Override
  public Difference<PersonBean> diffWithoutAge(PersonBean previous, PersonBean next) {
    if (previous == next) {
      return new ClassDifference<>(PersonBean.class, null, null, Collections.emptyList());
    }
    PersonBeanWrapper previousWrapper = new PersonBeanWrapper(previous);
    PersonBeanWrapper nextWrapper = new PersonBeanWrapper(next);
    List <PropertyDifference<?>> changes = new ArrayList<>();
    addPropertyDifference(changes, previousWrapper.getName(), nextWrapper.getName(), "name");
    addPropertyDifference(changes, previousWrapper.getAge(), nextWrapper.getAge(), "age");
    return new ClassDifference<>(PersonBean.class, previousWrapper.hash(), nextWrapper.hash(), changes);
  }

  @Override
  public Difference<PersonBean> diff(PersonBean previous, PersonBean next) {
    if (previous == next) {
      return new ClassDifference<>(PersonBean.class, null, null, Collections.emptyList());
    }
    PersonBeanWrapper previousWrapper = new PersonBeanWrapper(previous);
    PersonBeanWrapper nextWrapper = new PersonBeanWrapper(next);
    List <PropertyDifference<?>> changes = new ArrayList<>();
    addPropertyDifference(changes, previousWrapper.getName(), nextWrapper.getName(), "name");
    addPropertyDifference(changes, previousWrapper.getAge(), nextWrapper.getAge(), "age");
    return new ClassDifference<>(PersonBean.class, previousWrapper.hash(), nextWrapper.hash(), changes);
  }

  @Override
  public Difference<CarBean> diff(CarBean previous, CarBean next) {
    if (previous == next) {
      return new ClassDifference<>(CarBean.class, null, null, Collections.emptyList());
    }
    CarBeanWrapper previousWrapper = new CarBeanWrapper(previous);
    CarBeanWrapper nextWrapper = new CarBeanWrapper(next);
    List <PropertyDifference<?>> changes = new ArrayList<>();
    addPropertyDifference(changes, previousWrapper.getTyp(), nextWrapper.getTyp(), "typ");
    addPropertyDifference(changes, previousWrapper.getColor(), nextWrapper.getColor(), "color");
    return new ClassDifference<>(CarBean.class, previousWrapper.hash(), nextWrapper.hash(), changes);
  }


  private static class CarBeanWrapper extends AbstractWrapper<CarBean> {
    private CarBeanWrapper(CarBean wrapped) { super(wrapped); }

    private String getColor() { return wrapped == null ? null : wrapped.getColor(); }
    private String getTyp() { return wrapped == null ? null : wrapped.getTyp(); }
  }

  private static class PersonBeanWrapper extends AbstractWrapper<PersonBean> {
    private PersonBeanWrapper(PersonBean wrapped) { super(wrapped); }

    private Integer getAge() { return wrapped == null ? null : wrapped.getAge(); }
    private String getName() { return wrapped == null ? null : wrapped.getName(); }
  }
}

Durch einfache Umstrukturierungen der Daten und der Umstellung von einer Bean auf eine Liste von Beans, wurde nicht nur das Ziel erreicht verschiedene Vergleichsvarianten für einen Typ in einem Interface zu definieren. Es können nun auch Vergleiche für verschiedene Typen in einem Interface definiert werden.

Im nächsten Beitrag wird das Vergleichen von Listen, Sets und Maps verbessert. Bislang wird nur festgestellt, dass eine Collection sich geändert hat, aber nicht wie die Änderung ausschaut. Eine Auflistung von hinzugefügten, geänderten und gelöschten Elementen soll die bisherige Darstellung ersetzen.