In den ersten und zweiten Beitrag dieser Reihe wurde ein Annotation Processor implementiert, der bei der Feststellung von Unterschieden zwischen zwei Java Beans unterstützt. In diesem Beitrag soll die Implementierung für Collection Klassen und Bean Strukturen erweitert werden.
Die bisherige Implementierung unterscheidet zwei Collections nur anhand ihrer Ungleichheit. Für eine bessere Unterscheidung sollen die Elemente aufgeführt werden, die hinzugefügt oder entfernt wurden.
Die Attribute von den Typen List
und Set
werden bislang wie alle anderen Attribute von der Methode addPropertyDifference
verarbeitet.
protected <T> void addPropertyDifference(List<PropertyDifference<?>> changes, T previous, T next, String property) { if (!Objects.equals(previous, next)) { changes.add(new PropertyDifference<>(property, previous, next)); } }
Statt diese Methode zu erweitern, um darin den Typ der Parameter zur Laufzeit zu prüfen, werden vom Annotation Processor gesonderte Methoden während der Kompilierung erstellt.
protected <T> void addSetPropertyDifference(List<PropertyDifference<?>> changes, Set<T> previous, Set<T> next, String property) { Set<T> remove = new HashSet<>(previous); Set<T> add = new HashSet<>(next); remove.removeAll(next); add.removeAll(previous); if (!add.isEmpty() || !remove.isEmpty()) { changes.add(new PropertyDifference<>(property, add, remove)); } }
Die Methode addSetPropertyDifference
erwartet zwei Parameter vom Typ
und berechnet ihren Unterschied in hinzugefügten und entfernten Elementen. Da für ein Set
Set
die Reihenfolge der Elemente egal ist und keine identischen Elemente enthalten sein können, ist der Algorithmus recht primitiv. Die Elemente eines Set
werden von einer Kopie des anderen Set
abgezogen. So bleiben in der Kopie des früheren Set
alle Elemente, die entfernt wurden und in der Kopie des späteren Set
alle Elemente, die hinzugefügt wurden.
protected <T> void addListPropertyDifference(List<PropertyDifference<?>> changes, List<T> previous, List<T> next, String property) { List<T> add = new ArrayList<>(next); List<T> remove = new ArrayList<>(previous); previous.stream().filter(add::contains).forEach(add::remove); next.stream().filter(remove::contains).forEach(remove::remove); if (!add.isEmpty() || !remove.isEmpty()) { changes.add(new PropertyDifference<>(property, add, remove)); } }
Bei der addListPropertyDifference
Methode für den Typ List
ist die Verarbeitung etwas komplizierter, weil Elemente mehrfach vorkommen können und nur für ein einmaliges Hinzufügen oder Entfernen betrachtet werden. Sollen auch veränderte Positionen in der Liste festgestellt werden, dann reicht dieser einfache Algorithmus nicht aus. Dann muss ein Edit-Distance Algorithmus (z.B. Myers Diff Algorithmus) auf Listen angepasst werden.
Um die jeweils richtige Methode zu erzeugen, enthält das Template für die Differ Klasse eine switch
Anweisung. Basierend auf dem Typ des Attributes wird entweder die bisherige addPropertyDifference
oder eine der beiden neuen Methoden aufgerufen.
<#list method.beanAttributes as attribute> <#switch attribute.type.name()> <#case "SET"> addSetPropertyDifference(changes, previousWrapper.${attribute.wrapperMethodName}(), nextWrapper.${attribute.wrapperMethodName}(), "${attribute.name}"); <#break> <#case "LIST"> addListPropertyDifference(changes, previousWrapper.${attribute.wrapperMethodName}(), nextWrapper.${attribute.wrapperMethodName}(), "${attribute.name}"); <#break> <#case "DEFAULT"> <#default> addPropertyDifference(changes, previousWrapper.${attribute.wrapperMethodName}(), nextWrapper.${attribute.wrapperMethodName}(), "${attribute.name}"); </#switch> </#list>
Für Bean Strukturen gelten ähnliche Anforderungen, wie für die Collections. Leider zeigen die Java Beans an dieser Stelle einen gewissen Nachteil gegenüber den Collections. Existieren bei den Collections die beiden Basis-Typen
und Set
List
, tummeln sich bei den Java Beans Heerscharen von Java Klassen.
Ein leicht erweitertes Beispiel erklärt die Probleme.
@Data class PersonBean { private String name; private int age; private CarBean car; private BikeBean bike; } @Data class BikeBean { private int size; private String color; } @Data class CarBean { private String type; private String color; } @Diff interface PersonDiffer { Difference<PersonBean> diff(PersonBean previous, PersonBean next); }
Die generierte PersonDiffer
Implementierung prüft die beiden PersonBean
Instanzen und vergleicht dabei auch die Attribute car
und bike
, die auch Java Beans sind. Die PersonDiffer
Implementierung kann für diese beiden Java Beans zusätzliche Logik enthalten, um diese auch zu prüfen. Es können aber weitere Java Beans in die Klasse PersonBean
oder in die Klassen CarBean
und BikeBean
eingefügt werden. Damit wird die Anzahl der Bean Klassen innerhalb einer Implementierung immer größer. Andererseits kann es auch andere Differ Implementierungen geben, die für gemeinsame Bean Klassen ihren eigenen Code generieren. Zusätzlich besteht das Problem zu unterscheiden, welche Klasse als Bean geprüft werden soll und welche nicht.
Eine beliebte Lösung, um aus diesem Dilemma herauszukommen, ist das Delegieren. Der PersonDiffer
prüft ein Attribut als Bean nur dann, wenn ihm ein Differ für dieses Bean bekannt ist.
@Diff interface CarDiffer { Difference<CarBean> diff(CarBean previous, CarBean next); } @Diff interface BikeDiffer { Difference<BikeBean> diff(BikeBean previous, BikeBean next); } @Diff(uses={CarDiffer.class, BikeDiffer.class}) interface PersonDiffer { Difference<PersonBean> diff(PersonBean previous, PersonBean next); }
In diesem Beispiel werden die Klassen CarBean
und BikeBean
als Bean geprüft, weil dem PersonDiffer
über das Attribute uses
passende Differ Implementierungen zur Verfügung stehen.
Um diese Funktionalität umzusetzen, werden zu den Diff-Methoden aus der PersonDiffer
Klasse auch die Diff-Methoden aus den Klassen CarBean
und BikeBean
benötigt.
public DiffTyp create(TypeElement differInterfaceType) { List<DiffMethod> methods = extractDiffMethods(differInterfaceType); if (methods.isEmpty()) { throw new NoSuchElementException("no diff methods found " + differInterfaceType); } Set<TypeMirror> usedDiffers = getUsedDiffers(differInterfaceType); processingEnv.getMessager().printMessage(Kind.NOTE, "used: " + usedDiffers); Map<TypeMirror, List<DiffMethod>> differPropertyMethods = usedDiffers.stream() .map(tm -> tm.accept(declaredTypeVisitor, null)).map(DeclaredType::asElement).map(TypeElement.class::cast) .filter(e -> e.getAnnotation(Diff.class) != null) .collect(toMap(Element::asType, this::extractDiffMethods)); return new DiffTyp(differInterfaceType, methods, usedDiffers, differPropertyMethods); }
Dazu wird die Methode extractDiffMethods
nicht nur auf das aktuelle Interface (Zeile 2), sondern auch auf alle Interfaces, die im uses
Attribute aufgelistet sind (Zeile 13).
Mit den Informationen aus der differPropertyMethods
Map wird vor der Codegenerierung geprüft, ob an einen anderen Differ delegiert werden soll. Um diesen zusätzlichen Fall zu behandeln wird die switch
Anweisung im Template um den Fall DIFFER
erweitert.
<#switch attribute.type.name()> <#case "SET"> addSetPropertyDifference(changes, previousWrapper.${attribute.wrapperMethodName}(), nextWrapper.${attribute.wrapperMethodName}(), "${attribute.name}"); <#break> <#case "LIST"> addListPropertyDifference(changes, previousWrapper.${attribute.wrapperMethodName}(), nextWrapper.${attribute.wrapperMethodName}(), "${attribute.name}"); <#break> <#case "DIFFER"> addPropertyDifference(changes, ${attribute.differCall}(previousWrapper.${attribute.wrapperMethodName}(), nextWrapper.${attribute.wrapperMethodName}()), "${attribute.name}"); <#break> <#case "DEFAULT"> <#default> addPropertyDifference(changes, previousWrapper.${attribute.wrapperMethodName}(), nextWrapper.${attribute.wrapperMethodName}(), "${attribute.name}"); </#switch>
Der geänderte Annotation Processor produziert für den
die folgende Implementierung.PersonDiffer
public final class PersonDifferImpl extends AbstractDiffer implements PersonDiffer { private final BikeDiffer bikeDiffer; private final CarDiffer carDiffer; public PersonDifferImpl( BikeDiffer bikeDiffer, CarDiffer carDiffer ) { this.bikeDiffer = bikeDiffer; this.carDiffer = carDiffer; } @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"); addPropertyDifference(changes, carDiffer.diff(previousWrapper.getCar(), nextWrapper.getCar()), "car"); addPropertyDifference(changes, bikeDiffer.diff(previousWrapper.getBike(), nextWrapper.getBike()), "bike"); return new ClassDifference<>(PersonBean.class, previousWrapper.hash(), nextWrapper.hash(), changes); } private static class PersonBeanWrapper extends AbstractWrapper<PersonBean> { private PersonBeanWrapper(PersonBean wrapped) { super(wrapped); } private CarBean getCar() { return wrapped == null ? null : wrapped.getCar(); } private String getName() { return wrapped == null ? null : wrapped.getName(); } private Integer getAge() { return wrapped == null ? null : wrapped.getAge(); } private BikeBean getBike() { return wrapped == null ? null : wrapped.getBike(); } } }
In den Zeilen 23 und 24 delegiert die Klasse PersonDifferImpl
an die, im Konstruktor übergebenen BikeDiffer
und CarDiffer
, um Unterschiede in den Attributen zu prüfen. Mit diesen Ergänzungen kann der Annotation Processor produktiv in eigenen Projekten verwendet werden.
1 thought on “Unterschiede finden mit dem Java Annotation Processor (Teil 3)”
Comments are closed.