BT

Diffuser les Connaissances et l'Innovation dans le Développement Logiciel d'Entreprise

Contribuez

Sujets

Sélectionner votre région

Accueil InfoQ Articles Introduction Au Reflectionless : Découvrez La Nouvelle Tendance Dans Le Monde Java

Introduction Au Reflectionless : Découvrez La Nouvelle Tendance Dans Le Monde Java

Points Clés

  • Qu'est-ce que le reflectionless ?
  • Reflection vs reflectionless
  • Comment créer un premier processeur d'annotations en Java ?

Au cours des vingt-cinq dernières années, de nombreuses choses ont changé en plus des versions de Java, comme les décisions architecturales et leurs exigences. Actuellement, il y a le facteur du cloud computing qui, en général, oblige l'application à avoir un meilleur démarrage en plus d'un faible tas de mémoire initiale. Avec cela, il est nécessaire de repenser la façon dont les frameworks sont fabriqués, en éliminant le goulot d'étranglement lié à la réflexion. Le but de cet article est de présenter quelques-unes des solutions qui aident sans avoir recours à la réflexion, les compromis de ce choix, en plus de présenter le processus de tratement des annotations en Java.

Dans le monde des frameworks, la réflexion joue certainement un grand rôle dans plusieurs outils, à la fois pour les classiques ORM et pour d'autres points, comme une API REST avec JAX-RS. Ce type de mécanisme facilite la vie du développeur Java en réduisant massivement le code boilerplate de diverses opérations. Il y a des gens qui rapportent que la plus grande différence dans le monde Java est précisément le grand nombre d'outils et d'écosystèmes autour du langage.

Pour l'utilisateur final, et ici je veux dire l'utilisateur qui utilise ces frameworks, tout ce processus se déroule de manière magique. Mettez simplement quelques annotations dans la classe et toutes les opérations sont effectuées à partir de ces paramètres. Elles ou les métadonnées de la classe seront lues et utilisées pour faciliter certains processus. Actuellement, le moyen le plus populaire de réaliser ce type de lecture est la réflexion qui effectue une introspection générant légèrement une idée de langage dynamique au sein de Java.

La première raison est que tout le traitement et la structure des données seront effectués au moment de l'exécution. Imaginez un moteur d'injection de dépendances qui a besoin d'analyser classe par classe, de vérifier la portée, les dépendances, etc. Ainsi, plus il y a de classes à analyser, plus le traitement est nécessaire et a tendance à augmenter le temps de réponse.

Un deuxième point est la consommation de mémoire, l'une des raisons est liées au fait que chaque classe doit être parcourue pour rechercher des métadonnées dans la classe, qu'il existe un cache ReflectionData qui charge toutes les informations de la classe, c'est-à-dire pour rechercher une information simple, telle que getSimpleName(), toutes les informations de métadonnées sont chargées et référencées via une SoftReference qui prend du temps à sortir de la mémoire.

En bref, l'approche de réflexion pose un problème à la fois dans la consommation de mémoire initiale et dans le délai de démarrage de l'application. En effet, le traitement des données, des analyses et des parseurs est effectué dès le démarrage d'une application. La consommation de mémoire et d'exécution a tendance à augmenter à mesure que le nombre de classes augmente. C'est l'une des raisons qui a poussé Graeme Rocher à donner une conférence expliquant le problème de la réflexion et comment elle a inspiré la création de Micronaut.

Une solution aux problèmes est de faire effectuer ces opérations aux frameworks au moment de la compilation, au lieu d'être au moment de l'exécution, apportant les avantages suivants :

  • Les métadonnées et les structures seront prêtes au démarrage de l'application, on peut imaginer ici un espèce de cache;
  • Il n'est pas nécessaire d'appeler les classes de réflexion, parmi lesquelles ReflectionData, réduisant ainsi la consommation de mémoire au démarrage;

Un autre point pour éviter la réflexion est le fait que nous pouvons utiliser beaucoup plus facilement AoT et également créer du code natif via GraalVM, une possibilité très intéressante, en particulier pour le concept serverless, car le programme a tendance à s'exécuter une fois, puis à renvoyer toute la ressource au système opérateur.

Certes, il existe plusieurs mythes autour d'Ahead of Time, après tout, comme tout choix, il y a certains compromis. C'est pourquoi Steve Millidge a écrit un article brillant sur le sujet.

 

Allons à ce qui compte ! Le code !

Après avoir expliqué les concepts, les motivations et les compromis des types de lectures, l'étape suivante sera la création d'un outil simple qui convertit une classe Java en Map à partir de certaines annotations qui définissent comment l'entité sera mappée, comment les attributs seront convertis et quel est le champ qui sera l’identifiant unique. Nous ferons tout cela comme indiqué dans le code ci dessous :

@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Entity {
    String value() default "";
}

@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Column {
    String value() default "";
}

@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Id {
    String value() default "";
}

Pour simplifier la comparaison avec la réflexion, ou éventuellement d'autres options, une interface sera créée qui sera responsable de la conversion vers / depuis une Map.

import java.util.Map;
import java.util.Map;
public interface Mapper {
    <T>  T toEntity(Map<String, Object> map, Class<T> type);
    <T> Map<String, Object> toMap(T entity);
}

Afin de comparer les deux solutions, la première implémentation se fera par réflexion. Un point notable est qu'il existe plusieurs stratégies pour travailler avec la réflexion, par exemple, en utilisant le package «java.beans» avec Introspector, cependant, dans cet exemple, nous le ferons de la manière la plus simple pour montrer les bases de son fonctionnement.

public class ReflectionMapper implements Mapper {

    @Override
    public <T> T toEntity(Map<String, Object> map, Class<T> type) {
        Objects.requireNonNull(map, "Map is required");
        Objects.requireNonNull(type, "type is required");

        final Constructor<?>[] constructors = type.getConstructors();
        try {
            final T instance = (T) constructors[0].newInstance();
            for (Field field : type.getDeclaredFields()) {
                write(map, instance, field);
            }
            return instance;
        } catch (InstantiationException | IllegalAccessException | InvocationTargetException exception) {
            throw new RuntimeException("An error to field the entity process", exception);
        }
    }


    @Override
    public <T> Map<String, Object> toMap(T entity) {
        Objects.requireNonNull(entity, "entity is required");
        Map<String, Object> map = new HashMap<>();
        final Class<?> type = entity.getClass();
        final Entity annotation = Optional.ofNullable(
                type.getAnnotation(Entity.class))
                .orElseThrow(() -> new RuntimeException("The class must have Entity annotation"));

        String name = annotation.value().isBlank() ? type.getSimpleName() : annotation.value();
        map.put("entity", name);
        for (Field field : type.getDeclaredFields()) {
            try {
                read(entity, map, field);
            } catch (IllegalAccessException exception) {
                throw new RuntimeException("An error to field the map process", exception);
            }
        }

        return map;
    }

    private <T> void read(T entity, Map<String, Object> map, Field field) throws IllegalAccessException {
        final Id id = field.getAnnotation(Id.class);
        final Column column = field.getAnnotation(Column.class);
        final String fieldName = field.getName();
        if (id != null) {
            String idName = id.value().isBlank() ? fieldName : id.value();
            field.setAccessible(true);
            final Object value = field.get(entity);
            map.put(idName, value);
        } else if (column != null) {
            String columnName = column.value().isBlank() ? fieldName : column.value();
            field.setAccessible(true);
            final Object value = field.get(entity);
            map.put(columnName, value);
        }
    }

    private <T> void write(Map<String, Object> map, T instance, Field field) throws IllegalAccessException {
        final Id id = field.getAnnotation(Id.class);
        final Column column = field.getAnnotation(Column.class);
        final String fieldName = field.getName();
        if (id != null) {
            String idName = id.value().isBlank() ? fieldName : id.value();
            field.setAccessible(true);
            final Object value = map.get(idName);
            if (value != null) {
                field.set(instance, value);
            }
        } else if (column != null) {
            String columnName = column.value().isBlank() ? fieldName : column.value();
            field.setAccessible(true);
            final Object value = map.get(columnName);
            if (value != null) {
                field.set(instance, value);
            }
        }
    }
}

Une fois le mappeur créé, l'étape suivante consiste à créer un petit exemple. Alors créons une entité Animal.

@Entity("animal")
public class Animal {
    @Id
    private String id;

    @Column("native_name")
    private String name;

    public Animal() {
    }

    public Animal(String id, String name) {
        this.id = id;
        this.name = name;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

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

public class ReflectionMapperTest {

    private Mapper mapper;

    @BeforeEach
    public void setUp() {
        this.mapper = new ReflectionMapper();
    }

    @Test
    public void shouldCreateMap() {
        Animal animal = new Animal("id", "lion");
        final Map<String, Object> map = mapper.toMap(animal);
        Assertions.assertEquals("animal", map.get("entity"));
        Assertions.assertEquals("id", map.get("id"));
        Assertions.assertEquals("lion", map.get("native_name"));
    }

    @Test
    public void shouldCreateEntity() {
        Map<String, Object> map = new HashMap<>();
        map.put("id", "id");
        map.put("native_name", "lion");

        final Animal animal = mapper.toEntity(map, Animal.class);
        Assertions.assertEquals("id", animal.getId());
        Assertions.assertEquals("lion", animal.getName());
    }
}

Avec cela, l'implémentation de l'utilisation de la réflexion a été démontrée. Si le développeur souhaite utiliser ce type d'outil dans d'autres projets, il est possible de créer un petit projet et de l'ajouter comme toute autre dépendance et toutes ces opérations et lectures seront effectuées au moment de l'exécution.

Il est important de noter que dans le monde de la réflexion, il existe certaines options et stratégies pour travailler avec lui, par exemple, créer un cache interne de ces métadonnées pour éviter d'utiliser les informations ReflectionData ou, à partir de ces informations, compiler des classes au moment de l'exécution comme Geoffrey de Smet le démontre dans son article, en utilisant le JavaCompiler.

Cependant, le gros point est que tout ce processus aura lieu au moment de l'exécution. Pour faire passer le traitement à la compilation, nous utiliserons l'API Java Annotation Processor.

En général, la classe pour être une entité dans le processus doit hériter de la classe AbstractProcessor et être annotée avec SupportedAnnotationTypes pour définir quelles classes seront lues au moment de la compilation. La méthode process() est l'endroit où toute l'analyse sera effectuée. La dernière étape consiste à enregistrer la classe en tant que SPI et le code sera prêt à être exécuté au moment de la compilation.

@SupportedAnnotationTypes("org.soujava.medatadata.api.Entity")
public class EntityProcessor extends AbstractProcessor {

//… 
    @Override
    public boolean process(Set<? extends TypeElement> annotations,
                           RoundEnvironment roundEnv) {

        final List<String> entities = new ArrayList<>();
        for (TypeElement annotation : annotations) {
            roundEnv.getElementsAnnotatedWith(annotation)
                    .stream().map(e -> new ClassAnalyzer(e, processingEnv))
                    .map(ClassAnalyzer::get)
                    .filter(IS_NOT_BLANK).forEach(entities::add);
        }

        try {
            if (!entities.isEmpty()) {
                createClassMapping(entities);
                createProcessorMap();
            }
        } catch (IOException exception) {
            error(exception);
        }
        return false;
    }
//… 

Un point important est que la configuration du traitement des annotations Java nécessite plus d' étapes de configuration que la réflexion. Cependant, avec les étapes initiales, les suivantes ont tendance à être similaires à l'API de réflexion. La dépendance sur ce type de la librairie peut se faire en utilisant la balise annotationProcessorPaths dans le fichier pom.xml. Un avantage majeur est que ces dépendances ne seront visibles que dans le cadre de la compilation.Vous pouvez ajouter des dépendances pour générer des classes, par exemple en utilisant Mustache, sans se soucier de ces dépendances à l'exécution.

Dès que la dépendance est ajoutée à un projet et qu'elle est exécutée, les classes seront générées dans le dossier target/generated-sources. Dans l'exemple, toutes les classes ont été générées grâce au projet Mustache.

@Generated(value= "Soujava ClassMappings Generator", date = "2021-01-21T13:08:48.618494")
public final class ProcessorClassMappings implements ClassMappings {

    private final List<EntityMetadata> entities;

    public ProcessorClassMappings() {
        this.entities = new ArrayList<>();
        this.entities.add(new org.soujava.metadata.example.PersonEntityMetaData());
        this.entities.add(new org.soujava.metadata.example.AnimalEntityMetaData());
        this.entities.add(new org.soujava.metadata.example.CarEntityMetaData());
    }
...
@Generated(value= "Soujava ClassMappings Generator", date = "2021-01-21T13:08:48.618494")
@Entity("animal")
public class Animal {
    @Id
    private String id;

    @Column("native_name")
    private String name;

    public Animal() {
    }

    public Animal(String id, String name) {
        this.id = id;
        this.name = name;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

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

public class ProcessorMapperTest {

    private Mapper mapper;

    @BeforeEach
    public void setUp() {
        this.mapper = new ProcessorMapper();
    }

    @Test
    public void shouldCreateMap() {
        Animal animal = new Animal("id", "lion");
        final Map<String, Object> map = mapper.toMap(animal);
        Assertions.assertEquals("animal", map.get("entity"));
        Assertions.assertEquals("id", map.get("id"));
        Assertions.assertEquals("lion", map.get("native_name"));
    }

    @Test
    public void shouldCreateEntity() {
        Map<String, Object> map = new HashMap<>();
        map.put("id", "id");
        map.put("native_name", "lion");

        final Animal animal = mapper.toEntity(map, Animal.class);
        Assertions.assertEquals("id", animal.getId());
        Assertions.assertEquals("lion", animal.getName());
    }
}

Dans cet article, nous avons parlé un peu des effets, des avantages et des inconvénients dans le monde de la réflexion. Nous avons présenté un exemple avec Java Annotation Processor et avons montré les avantages de l'AOT en Java et l’avons même converti en natif, le rendant plus facile dans plusieurs situations comme Serverless.

 

Code: https://github.com/soujava/annotation-processors

A propos de l'auteur

Otávio Santana est un ingénieur logiciel avec une vaste expérience dans le développement open source, avec plusieurs contributions à JBoss Weld, Hibernate, Apache Commons et à d'autres projets. Axé sur le développement multilingue et les applications haute performance, Otávio a travaillé sur de grands projets dans les domaines de la finance, du gouvernement, des médias sociaux et du commerce électronique. Membre du comité exécutif du JCP et de plusieurs groupes d'experts JSR, il est également un champion Java et a reçu le JCP Outstanding Award et le Duke's Choice Award.

 

Evaluer cet article

Pertinence
Style

Contenu Éducatif

BT