Outil de comparaison d'objets par réflexion en Java

Dans le cadre d'un projet nécessitant le suivi des modifications apportées aux données utilisateur, il est devenu nécessaire de mettre en place un mécanisme automatique de détection des changements. La comparaison manuelle champ par champ étant fastideiuse et sujette aux oublis, l'approche par réflexion offre une solution élégante pour identifier les champs modifiés entre deux instances d'un même type.

Implémentation de l'utilitaire

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;

/**
 * Analyseur de variations entre deux objets par introspection
 */
public class ComparateurReflexif {

    /**
     * Produit la liste des champs ayant subi une modification
     * @param source instance de référence
     * @param cible instance modifiée
     * @return collection des variations détectées
     */
    public static List<variationchamp> identifierModifications(Object source, Object cible) {
        List<variationchamp> variations = new LinkedList<>();

        verifierPreconditions(source, cible);

        Class> type = source.getClass();
        Field[] champs = type.getDeclaredFields();

        for (Field champ : champs) {
            champ.setAccessible(true);
            Object valeurOriginale = extraireValeur(champ, source);
            Object valeurModifiee = extraireValeur(champ, cible);

            if (!Objects.equals(valeurOriginale, valeurModifiee)) {
                String libelle = extraireLibelle(champ);
                variations.add(new VariationChamp(libelle, valeurOriginale, valeurModifiee));
            }
        }

        return variations;
    }

    private static void verifierPreconditions(Object source, Object cible) {
        if (source == null || cible == null) {
            throw new IllegalArgumentException("Les instances comparées ne peuvent être nulles");
        }
        if (!source.getClass().equals(cible.getClass())) {
            throw new IllegalArgumentException("Les types des objets diffèrent et ne peuvent être comparés");
        }
    }

    private static Object extraireValeur(Field champ, Object proprietaire) {
        try {
            return champ.get(proprietaire);
        } catch (IllegalAccessException e) {
            throw new RuntimeException("Accès refusé au champ : " + champ.getName(), e);
        }
    }

    private static String extraireLibelle(Field champ) {
        if (champ.isAnnotationPresent(Libelle.class)) {
            return champ.getAnnotation(Libelle.class).valeur();
        }
        return champ.getName();
    }

    /**
     * Représentation d'une modification sur un champ
     */
    public static class VariationChamp {
        private final String libelle;
        private final Object precedente;
        private final Object courante;

        public VariationChamp(String libelle, Object precedente, Object courante) {
            this.libelle = libelle;
            this.precedente = precedente;
            this.courante = courante;
        }

        public String getLibelle() { return libelle; }
        public Object getPrecedente() { return precedente; }
        public Object getCourante() { return courante; }
    }

    /**
     * Annotation pour définir un libellé descriptif sur un champ
     */
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.FIELD)
    public @interface Libelle {
        String valeur();
    }
}
</variationchamp></variationchamp>

Exemple d'utilisation

1. Modélsiation de l'entité

public class Client {

    @ComparateurReflexif.Libelle(valeur = "Identifiant")
    private Long id;

    @ComparateurReflexif.Libelle(valeur = "Nom complet")
    private String nom;

    @ComparateurReflexif.Libelle(valeur = "Âge")
    private Integer age;

    public Client() {}

    public Client(Long id, String nom, Integer age) {
        this.id = id;
        this.nom = nom;
        this.age = age;
    }
}

2. Instanciation et comparaison

Client etatInitial = new Client(1L, "Martin Dupont", 42);
Client etatFinal = new Client(1L, "Martin Bernard", 45);

List<ComparateurReflexif.VariationChamp> modifications =
    ComparateurReflexif.identifierModifications(etatInitial, etatFinal);

modifications.forEach(v ->
    System.out.printf("[%s] %s → %s%n",
        v.getLibelle(),
        v.getPrecedente(),
        v.getCourante())
);

3. Résultat produit

[Nom complet] Martin Dupont → Martin Bernard
[Âge] 42 → 45

Points clés de l'implémentation

  1. Annotation @Libelle : permet d'associer une description explicite à chaque champ, récupérée dynamiquement lors de l'introspection.
  2. Méthode identifierModifications() : parcourt les champs déclarés via getDeclaredFields() et utilise Objects.equals() pour une comparaison null-safe.
  3. Méthode verifierPreconditions() : factorise les contrôles de validité (nullité et compatibilité des types).

Pistes d'enrichissement

Exclusion de champs

@Libelle(valeur = "Date de création", ignorer = true)
private Date dateCreation;

Il suffit d'ajouter un attribut ignorer à l'annotation et de filtrer les champs correspondants avant la comparaison :

if (champ.isAnnotationPresent(Libelle.class)) {
    Libelle meta = champ.getAnnotation(Libelle.class);
    if (meta.ignorer()) continue;
}

Comparaison récursive des objets imbriqués

private static boolean estTypePrimitifOuWrapper(Class> type) {
    return type.isPrimitive() || type.equals(String.class)
        || Number.class.isAssignableFrom(type);
}

// Dans la boucle de comparaison :
if (!estTypePrimitifOuWrapper(champ.getType())
        && valeurOriginale != null
        && valeurModifiee != null) {
    List<VariationChamp> sousVariations =
        identifierModifications(valeurOriginale, valeurModifiee);
    variations.addAll(sousVariations);
}

Stratégie de comparaison personnalisée

@FunctionalInterface
public interface StrategieComparaison {
    boolean tester(Object avant, Object apres);
}

// Intégration dans la méthode principale :
StrategieComparaison strategie = (a, b) -> Objects.equals(a, b);
if (!strategie.tester(valeurOriginale, valeurModifiee)) {
    variations.add(new VariationChamp(libelle, valeurOriginale, valeurModifiee));
}

Cette approche permet d'injecter des logiques spécifiques, par exemple une comparaison de dates avec tolérance ou une comparaison de collections sur leurs contenus plutôt que sur leurs références.

Étiquettes: Java Réflection Java Annotation Java Introspection API Java Reflection

Publié le 7 août à 20h26