Application pratique de l'annotation en Java par l'exemple de création d'un bot Telegram

Reflection in Java est une API spéciale de la bibliothèque standard qui vous permet d'accéder aux informations sur un programme lors de l'exécution.



La plupart des programmes utilisent la réflexion d'une manière ou d'une autre sous ses différentes formes, car ses capacités sont difficiles à intégrer dans un article.



De nombreuses réponses s'arrêtent là, mais le plus important est de comprendre le concept général de réflexion. Nous recherchons des réponses courtes aux questions afin de réussir l'entretien, mais nous ne comprenons pas les bases - d'où cela vient et ce que l'on entend exactement par réflexion.



Dans cet article, nous aborderons toutes ces questions en relation avec les annotations et avec un exemple en direct, nous verrons comment utiliser, trouver et écrire les vôtres.








Réflexion





Je pense que ce serait une erreur de penser que la réflexion Java est limitée à un simple paquet dans la bibliothèque standard. Par conséquent, je propose de le considérer comme un terme, sans le lier à un ensemble spécifique.



Réflexion vs introspection



Parallèlement à la réflexion, il y a aussi le concept d'introspection. L'introspection est la capacité d'un programme à obtenir des données sur le type et d'autres propriétés d'un objet. Par exemple, ceci instanceof



:



if (obj instanceof Cat) {
   Cat cat = (Cat) obj;
   cat.meow();
}
      
      





C'est une technique très puissante, sans laquelle Java ne serait pas ce qu'il est. Néanmoins, il ne va pas plus loin que la réception de données, et la réflexion entre en jeu.



Quelques possibilités de réflexion



Plus précisément, la réflexion est la capacité d'un programme à s'examiner lui-même à l'exécution et à l'utiliser pour modifier son comportement.



Par conséquent, l'exemple ci-dessus n'est pas une réflexion, mais uniquement une introspection du type d'objet. Mais qu'est-ce donc que la réflexion? Par exemple, créer une classe ou appeler une méthode, mais d'une manière très particulière. Voici un exemple.



Imaginons que nous n'ayons aucune connaissance sur la classe que nous voulons créer, mais seulement des informations sur son emplacement. Dans ce cas, nous ne pouvons pas créer une classe de la manière évidente:



Object obj = new Cat();    //    ?
      
      





Utilisons la réflexion et créons une instance de la classe:



Object obj = Class.forName("complete.classpath.MyCat").newInstance();
      
      





Appelons aussi sa méthode par réflexion:



Method m = obj.getClass().getDeclaredMethod("meow");
m.invoke(obj);
      
      





De la théorie à la pratique:



import java.lang.reflect.Method;
import java.lang.Class;

public class Cat {

    public void meow() {
        System.out.println("Meow");
    }
    
    public static void main(String[] args) throws Exception {
        Object obj = Class.forName("Cat").newInstance();
         Method m = obj.getClass().getDeclaredMethod("meow");
         m.invoke(obj);
    }
}
      
      





Vous pouvez jouer avec dans Jdoodle .

Malgré sa simplicité, il se passe beaucoup de choses complexes dans ce code, et souvent le programmeur ne manque que d'une simple utilisation getDeclaredMethod and then invoke



.



Question n ° 1

Pourquoi devons-nous passer une instance d'un objet dans la méthode invoke dans l'exemple ci-dessus?



Je n'irai pas plus loin, car nous irons loin du sujet. Au lieu de cela, je laisserai un lien vers un article par un collègue senior Tagir Valeev .



Annotations



Les annotations sont une partie importante du langage Java. C'est une sorte de descripteur qui peut être accroché à une classe, un champ ou une méthode. Par exemple, vous avez peut-être vu l'annotation @Override



:



public abstract class Animal {
    abstract void doSomething();
}

public class Cat extends Animal {
    @Override
    public void doSomething() {
        System.out.println("Meow");
    }

}
      
      





Vous êtes-vous déjà demandé comment cela fonctionne? Si vous ne savez pas, avant de lire plus loin, essayez de deviner.



Types d'annotations



Considérez l'annotation ci-dessus:



@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {

}
      
      





@Target



 - indique à quoi s'applique l'annotation. Dans ce cas, la méthode.



@Retention



 - la durée de vie de l'annotation dans le code (pas en secondes, bien sûr).



@interface



- est la syntaxe pour créer des annotations.



Si le premier et le dernier plus ou moins clair (voir.  @Target



 Dans la  documentation ), alors  @Retention



 regardons maintenant, car il sera divisé en plusieurs types d'annotations, ce qui est très important à comprendre.



Cette annotation peut prendre trois valeurs:





Dans le premier cas, l'annotation sera écrite dans le bytecode de votre code, mais ne devrait pas être persistée par la VM lors de l'exécution.



Dans le second cas, l'annotation sera disponible au moment de l'exécution, donc nous pouvons la traiter, par exemple, obtenir toutes les classes qui ont cette annotation.



Dans le troisième cas, l'annotation sera supprimée par le compilateur (elle ne sera pas dans le bytecode). Ce sont généralement des annotations qui ne sont utiles que pour le compilateur.



En revenant à l'annotation  @Override



, on voit qu'elle a,  RetentionPolicy.SOURCE



 ce qui est généralement logique, étant donné qu'elle n'est utilisée que par le compilateur. Au runtime, cette annotation ne fournit vraiment rien d'utile.



SuperCat



Essayons d'ajouter notre propre annotation (cela sera utile pendant le développement).



abstract class Cat {
    abstract void meow();
}

public class Home {

    private class Tom extends Cat {
        @Override
        void meow() {
            System.out.println("Tom-style meow!"); // <---
        }
    }
    
    private class Alex extends Cat {
        @Override
        void meow() {
            System.out.println("Alex-style meow!"); // <---
        }
    }
}
      
      





Ayons deux chats dans notre maison: Tom et Alex. Créons une annotation pour le super chat:



@Target(ElementType.TYPE)     //    
@Retention(RetentionPolicy.RUNTIME)  //       
@interface SuperCat {

}

// ...

    @SuperCat   // <---
    private class Alex extends Cat {
        @Override
        void meow() {
            System.out.println("Alex-style meow!");
        }
    }

// ...
      
      





En même temps, nous laisserons Tom comme un chat ordinaire (le monde est injuste). Essayons maintenant d'obtenir les classes annotées avec cet élément. Ce serait bien d'avoir une méthode comme celle-ci sur la classe d'annotation elle-même:



Set<class<?>> classes = SuperCat.class.getAnnotatedClasses();
      
      





Mais, malheureusement, il n'existe pas encore de méthode de ce type. Alors comment trouvons-nous ces classes?



ClassPath



Il s'agit d'un paramètre qui pointe vers des classes personnalisées.



J'espère que vous les connaissez, et sinon, dépêchez-vous de l'étudier, car c'est l'une des choses fondamentales.


Ainsi, après avoir découvert où nos classes sont stockées, nous pouvons les charger via le ClassLoader et vérifier les classes pour cette annotation. Passons directement au code:



public static void main(String[] args) throws ClassNotFoundException {

    String packageName = "com.apploidxxx.examples";
    ClassLoader classLoader = Home.class.getClassLoader();
    
    String packagePath = packageName.replace('.', '/');
    URL urls = classLoader.getResource(packagePath);
    
    File folder = new File(urls.getPath());
    File[] classes = folder.listFiles();
    
    for (File aClass : classes) {
        int index = aClass.getName().indexOf(".");
        String className = aClass.getName().substring(0, index);
        String classNamePath = packageName + "." + className;
        Class<?> repoClass = Class.forName(classNamePath);
    
        Annotation[] annotations = repoClass.getAnnotations();
        for (Annotation annotation : annotations) {
            if (annotation.annotationType() == SuperCat.class) {
                System.out.println(
                  "Detected SuperCat!!! It is " + repoClass.getName()
                );
            }
        }
    
    }
}
      
      





Je ne recommande pas de l'utiliser dans votre programme. Le code est à titre informatif uniquement!



Cet exemple est indicatif, mais utilisé uniquement à des fins éducatives pour cette raison:



Class<?> repoClass = Class.forName(classNamePath);
      
      





Nous découvrirons pourquoi plus tard. Pour l'instant, jetons un coup d'œil aux lignes ci-dessus:



// ...

//      
String packageName = "com.apploidxxx.examples";

//  ,      -
ClassLoader classLoader = Home.class.getClassLoader();

// com.apploidxxx.examples -> com/apploidxxx/examples
String packagePath = packageName.replace('.', '/');
URL urls = classLoader.getResource(packagePath);

File folder = new File(urls.getPath());

//     
File[] classes = folder.listFiles();

// ...
      
      





Pour savoir d'où nous obtenons ces fichiers, considérez l'archive JAR qui est créée lorsque nous exécutons l'application:



├───com
│   └───apploidxxx
│       └───examples
│               Cat.class
│               Home$Alex.class
│               Home$Tom.class
│               Home.class
│               Main.class
│               SuperCat.class
      
      





Ainsi, classes



ce ne sont que nos fichiers compilés sous forme de bytecode. Néanmoins, File



ce n'est pas encore un fichier téléchargé, nous savons seulement où ils se trouvent, mais nous ne pouvons toujours pas voir ce qu'il y a à l'intérieur.



Alors chargons chaque fichier:



for (File aClass : classes) {
    //  ,   , Home.class, Home$Alex.class  
    //      .class     
    //     Java
    int index = aClass.getName().indexOf(".");
    String className = aClass.getName().substring(0, index);
    String classNamePath = packageName + "." + className;
    // classNamePath = com.apploidxxx.examples.Home

    Class<?> repoClass = Class.forName(classNamePath);
}
      
      





Tout ce qui était fait auparavant consistait uniquement à appeler cette méthode Class.forName, qui chargera la classe dont nous avons besoin. La dernière partie consiste donc à récupérer toutes les annotations utilisées sur le repoClass, puis à vérifier si ce sont des annotations @SuperCat



:



Annotation[] annotations = repoClass.getAnnotations();
for (Annotation annotation : annotations) {
    if (annotation.annotationType() == SuperCat.class) {
        System.out.println(
          "Detected SuperCat!!! It is " + repoClass.getName()
        );
    }
}
output: Detected SuperCat!!! It is com.apploidxxx.examples.Home$Alex
      
      





Et tu as fini! Maintenant que nous avons la classe elle-même, nous avons accès à toutes les méthodes de réflexion.



Réfléchissant



Comme dans l'exemple ci-dessus, nous pouvons simplement créer une nouvelle instance de notre classe. Mais avant cela, regardons quelques formalités.



  • Premièrement, les chats ont besoin de vivre quelque part, ils ont donc besoin d'une maison. Dans notre cas, ils ne peuvent exister sans maison.
  • Deuxièmement, créons une liste de super-manteaux.


List<cat> superCats = new ArrayList<>();
final Home home = new Home();    // ,     
      
      





Le traitement prend donc sa forme définitive:



for (Annotation annotation : annotations) {
    if (annotation.annotationType() == SuperCat.class) {
        Object obj = repoClass
          .getDeclaredConstructor(Home.class)
          .newInstance(home);
        superCats.add((Cat) obj);
    }
}
      
      





Et encore la rubrique des questions:



Question # 2

Que se passe-t-il si nous marquons une @SuperCat



classe qui n'hérite pas Cat



?



Question n ° 3

Pourquoi avons-nous besoin d'un constructeur qui prend un type d'argument Home



?


Réfléchissez pendant quelques minutes, puis analysez immédiatement les réponses:



Réponse n ° 2 : Oui ClassCastException



, car l'annotation elle @SuperCat



- même ne garantit pas que la classe marquée de cette annotation héritera ou implémentera quelque chose.



Vous pouvez vérifier cela en supprimant extends Cat



d'Alex. En même temps, vous verrez à quel point les annotations peuvent être utiles @Override



.



Réponse n ° 3 : Les chats ont besoin d'une maison parce qu'ils sont des classes intérieures. Tout est dans le cadre du chapitre 15.9.3 de la spécification du langage Java .



Cependant, vous pouvez éviter cela simplement en rendant ces classes statiques. Mais lorsque vous travaillez avec la réflexion, vous rencontrerez souvent ce genre de chose. Et vous n'avez pas vraiment besoin de connaître à fond la spécification Java pour cela. Ces choses sont assez logiques, et vous pouvez penser à vous-même pourquoi nous devrions passer une instance de la classe parente au constructeur, si c'est le cas non-static



. Résumons



et obtenons: Home.java



package com.apploidxxx.examples;

import java.io.File;
import java.lang.annotation.*;
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;

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

}

abstract class Cat {
    abstract void meow();
}

public class Home {

    public class Tom extends Cat {
        @Override
        void meow() {
            System.out.println("Tom-style meow!");
        }
    }
    
    @SuperCat
    public class Alex extends Cat {
        @Override
        void meow() {
            System.out.println("Alex-style meow!");
        }
    }
    
    public static void main(String[] args) throws Exception {
    
        String packageName = "com.apploidxxx.examples";
        ClassLoader classLoader = Home.class.getClassLoader();
    
        String packagePath = packageName.replace('.', '/');
        URL urls = classLoader.getResource(packagePath);
    
        File folder = new File(urls.getPath());
        File[] classes = folder.listFiles();
    
        List<Cat> superCats = new ArrayList<>();
        final Home home = new Home();
    
        for (File aClass : classes) {
            int index = aClass.getName().indexOf(".");
            String className = aClass.getName().substring(0, index);
            String classNamePath = packageName + "." + className;
            Class<?> repoClass = Class.forName(classNamePath);
            Annotation[] annotations = repoClass.getAnnotations();
            for (Annotation annotation : annotations) {
                if (annotation.annotationType() == SuperCat.class) {
                    Object obj = repoClass
                      .getDeclaredConstructor(Home.class)
                      .newInstance(home);
                    superCats.add((Cat) obj);
                }
            }
        }
    
        superCats.forEach(Cat::meow);
    }
}
output: Alex-style meow!
      
      





Alors, quel est le problème Class.forName



?



Il fait lui-même tout ce qui lui est demandé. Cependant, nous ne l'utilisons pas correctement.



Imaginez que vous travaillez sur des projets avec 1000 classes ou plus (après tout, nous écrivons en Java). Et imaginez charger chaque classe que vous trouvez dans classPath. Vous comprenez vous-même que la mémoire et les autres ressources JVM ne sont pas en caoutchouc.



Façons de travailler avec les annotations



S'il n'y avait pas d'autre moyen de travailler avec les annotations, alors les utiliser comme étiquettes de classe, comme, par exemple, au printemps, serait très, très controversé.



Mais le printemps semble fonctionner. Mon programme est-il si lent à cause d'eux? Malheureusement ou heureusement non. Spring fonctionne très bien (à cet égard) car il utilise une manière légèrement différente de travailler avec eux.



Directement au bytecode



Tout le monde (espérons-le) a en quelque sorte une idée de ce qu'est un bytecode. Il stocke toutes les informations sur nos classes et leurs métadonnées (y compris les annotations).



Il est temps de se souvenir du nôtre RetentionPolicy



. Dans l'exemple précédent, nous avons pu trouver cette annotation car nous avons indiqué qu'il s'agissait d'une annotation d'exécution. Par conséquent, il doit être stocké en bytecode.



Alors pourquoi ne pas le lire (oui, à partir du bytecode)? Mais ici, je n'implémenterai pas de programme pour le lire à partir du bytecode, car il mérite un article séparé. Cependant, vous pouvez le faire vous-même - ce sera une excellente pratique qui consolidera le contenu de l'article.



Pour vous familiariser avec le bytecode, vous pouvez commencer par mon article... Là, je décris les choses de base du bytecode avec Hello World! L'article sera utile même si vous n'allez pas travailler directement avec le bytecode. Il décrit les points fondamentaux qui aideront à répondre à la question: pourquoi exactement?



Après cela, bienvenue dans la spécification officielle de la JVM . Si vous ne souhaitez pas analyser le bytecode manuellement (par octets), recherchez des bibliothèques telles que ASM et Javassist .



Réflexions



Reflections est une bibliothèque avec une licence WTFPL qui vous permet d'en faire ce que vous voulez. Une bibliothèque assez rapide pour divers travaux avec classpath et métadonnées. L'utile est qu'il peut enregistrer des informations sur certaines données déjà lues, ce qui fait gagner du temps. Vous pouvez creuser à l'intérieur et trouver la classe Store.



package com.apploidxxx.examples;

import org.reflections.Reflections;

import java.lang.reflect.InvocationTargetException;
import java.util.Optional;
import java.util.Set;

public class ExampleReflections {
    private static final Home HOME = new Home();

    public static void main(String[] args) {
    
        Reflections reflections = new Reflections("com.apploidxxx.examples");
    
        Set<Class<?>> superCats = reflections
          .getTypesAnnotatedWith(SuperCat.class);
    
        for (Class<?> clazz : superCats) {
            toCat(clazz).ifPresent(Cat::meow);
        }
    }
    
    private static Optional<Cat> toCat(Class<?> clazz) {
        try {
            return Optional.of((Cat) clazz
                               .getDeclaredConstructor(Home.class)
                               .newInstance(HOME)
                              );
        } catch (InstantiationException | 
                 IllegalAccessException | 
                 InvocationTargetException | 
                 NoSuchMethodException e) 
        {
            e.printStackTrace();
            return Optional.empty();
        }
    }
}
      
      





contexte-printemps



Je recommanderais d'utiliser la bibliothèque Reflections, car elle fonctionne en interne via javassist, ce qui indique qu'elle lit le bytecode, ne le charge pas.



Cependant, il existe de nombreuses autres bibliothèques qui fonctionnent de la même manière. Il y en a beaucoup, mais maintenant je ne veux démonter qu'un seul d'entre eux - celui-ci spring-context



. C'est peut-être mieux que le premier lorsque vous développez un bot dans le framework Spring. Mais il y a aussi quelques nuances ici.



Si vos classes sont essentiellement des beans gérés, c'est-à-dire qu'elles se trouvent dans un conteneur Spring, vous n'avez pas besoin de les réexaminer. Vous pouvez simplement accéder à ces beans depuis le conteneur lui-même.



Une autre chose est que si vous voulez que vos classes marquées soient des beans, vous pouvez le faire manuellement via ClassPathScanningCandidateComponentProvider



qui fonctionne via ASM.



Encore une fois, il est assez rare que vous ayez besoin d'utiliser cette méthode, mais cela vaut la peine d'être considéré comme une option.

J'ai écrit un bot pour VK dessus. Voici un référentiel avec lequel vous pouvez vous familiariser, mais je l'ai écrit il y a longtemps, et quand je suis allé chercher pour insérer un lien dans l'article, j'ai vu qu'à travers le VK-Java-SDK je reçois des messages avec des champs non initialisés, bien que tout fonctionnait avant.



Ce qui est drôle, c'est que je n'ai même pas changé la version du SDK, donc si vous en trouvez la raison, je vous en serai reconnaissant. Cependant, le chargement des commandes elles-mêmes fonctionne correctement, ce qui est exactement ce que vous pouvez regarder si vous voulez voir un exemple de travail avec spring-context



.



Les commandes qu'il contient sont les suivantes:



@Command(value = "hello", aliases = {"", ""})
public class Hello implements Executable {

    public BotResponse execute(Message message) throws Exception {
        return BotResponseFactoryUtil.createResponse("hello-hello", 
                                                     message.peerId);
    }
}
      
      





SuperCat



Vous pouvez trouver des exemples de code annoté dans ce référentiel .



Application pratique des annotations lors de la création d'un bot Telegram



C'était une introduction assez longue mais nécessaire au travail avec les annotations. Ensuite, nous allons implémenter un bot, mais le but de l'article n'est pas un manuel pour le créer. Il s'agit d'une application pratique des annotations. Il pourrait y avoir n'importe quoi ici: des applications console aux mêmes bots pour VK, panier et autres choses.



De plus, ici, certaines vérifications complexes ne seront pas effectuées délibérément. Par exemple, avant cela, les exemples ne comportaient aucune vérification de la gestion des erreurs nulles ou correcte, sans parler de leur journalisation.



Tout cela est fait pour simplifier le code. Par conséquent, si vous prenez le code des exemples, ne soyez pas paresseux pour le modifier, afin de mieux le comprendre et de le personnaliser en fonction de vos besoins.



Nous utiliserons la bibliothèque TelegramBots avec une licence MITpour travailler avec l'API de télégramme. Vous pouvez utiliser n'importe quel autre. Je l'ai choisi car il pouvait fonctionner à la fois "c" (il a une version avec démarreur) et "sans" botte à ressort.



En fait, je ne veux pas non plus compliquer le code en ajoutant une sorte d'abstraction, si vous le souhaitez, vous pouvez faire quelque chose d'universel, mais demandez-vous si cela en vaut la peine, donc pour cet article, nous utiliserons souvent des classes concrètes de ces bibliothèques, liant notre code pour eux.



Réflexions



Le premier bot en ligne est un bot écrit dans la bibliothèque de réflexions, sans Spring. Nous n'analyserons pas tout, mais seulement les points principaux, en particulier, nous nous intéressons au traitement des annotations. Avant de l'analyser dans l'article, vous pouvez vous-même comprendre comment cela fonctionne dans mon référentiel .



Dans tous les exemples, nous adhérerons au fait que le bot se compose de plusieurs commandes, et nous ne chargerons pas ces commandes manuellement, mais ajouterons simplement des annotations. Voici un exemple de commande:

@Handler("/hello")
public class HelloHandler implements RequestHandler {

    private static final Logger log = LoggerFactory
      .getLogger(HelloHandler.class);
    
    @Override
    public SendMessage execute(Message message) {
        log.info("Executing message from : " + message.getText());
        return SendMessage.builder()
                .text("Yaks")
                .chatId(String.valueOf(message.getChatId()))
                .build();
    }
}
@Retention(RetentionPolicy.RUNTIME)
public @interface Handler {
    String value();
}
      
      





Dans ce cas, le paramètre /hello



sera écrit value



dans l'annotation. value est quelque chose comme l'annotation par défaut. C'est @Handler("/hello")



= @Handler(value = "/hello")



.



Nous ajouterons également des enregistreurs. Nous les appellerons soit avant de traiter la demande, soit après, et les combinerons également:



@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
    String value() default ".*";    // regex
    ExecutionTime[] executionTime() default ExecutionTime.BEFORE;
}
default` ,    ,     `value
@Log
public class LogHandler implements RequestLogger {

    private static final Logger log = LoggerFactory
      .getLogger(LogHandler.class);
    
    @Override
    public void execute(Message message) {
        log.info("Just log a received message : " + message.getText());
    }
}
      
      





Mais on peut aussi ajouter un paramètre pour déclencher le logger pour certains messages:



@Log(value = "/hello")
public class HelloLogHandler implements RequestLogger {
    public static final Logger log = LoggerFactory
      .getLogger(HelloLogHandler.class);

    @Override
    public void execute(Message message) {
        log.info("Received special hello command!");
    }
}

      
      





Ou déclenché après le traitement de la demande:



@Log(executionTime = ExecutionTime.AFTER)
public class AfterLogHandler implements RequestLogger {

    private static final Logger log = LoggerFactory
      .getLogger(AfterLogHandler.class);
    
    @Override
    public void executeAfter(Message message, SendMessage sendMessage) {
        log.info("Bot response >> " + sendMessage.getText());
    }
}
      
      





Ou les deux ici et là:



@Log(executionTime = {ExecutionTime.AFTER, ExecutionTime.BEFORE})
public class AfterAndBeforeLogger implements RequestLogger {
    private static final Logger log = LoggerFactory
      .getLogger(AfterAndBeforeLogger.class);

    @Override
    public void execute(Message message) {
        log.info("Before execute");
    }
    
    @Override
    public void executeAfter(Message message, SendMessage sendMessage) {
        log.info("After execute");
    }
}
      
      





Nous pouvons le faire car cela executionTime



prend un tableau de valeurs. Le principe de fonctionnement est simple, commençons donc à traiter ces annotations:



Set<Class<?>> annotatedCommands = 
  reflections.getTypesAnnotatedWith(Handler.class);

final Map<String, RequestHandler> commandsMap = new HashMap<>();

final Class<RequestHandler> requiredInterface = RequestHandler.class;

for (Class<?> clazz : annotatedCommands) {
    if (LoaderUtils.isImplementedInterface(clazz, requiredInterface)) {
        for (Constructor<?> c : clazz.getDeclaredConstructors()) {
            //noinspection unchecked
            Constructor<RequestHandler> castedConstructor = 
              (Constructor<RequestHandler>) c;
            commandsMap.put(extractCommandName(clazz), 
                            OBJECT_CREATOR.instantiateClass(castedConstructor));
        }

    } else {
        log.warn("Command didn't implemented: " 
                 + requiredInterface.getCanonicalName());
    
    }
}

// ...
private static String extractCommandName(Class<?> clazz) {
    Handler handler = clazz.getAnnotation(Handler.class);
    if (handler == null) {
        throw new 
          IllegalArgumentException(
            "Passed class without Handler annotation"
            );
    } else {
        return handler.value();
    }
}
      
      





En fait, nous créons simplement une carte avec le nom de la commande, que nous prenons à partir de la valeur value



dans l'annotation. Le code source est ici .



Nous faisons la même chose avec Log, seulement il peut y avoir plusieurs enregistreurs avec les mêmes motifs, donc nous changeons légèrement notre structure de données:



Set<Class<?>> annotatedLoggers = reflections.getTypesAnnotatedWith(Log.class);

final Map<String, Set<RequestLogger>> commandsMap = new HashMap<>();
final Class<RequestLogger> requiredInterface = RequestLogger.class;

for (Class<?> clazz : annotatedLoggers) {
    if (LoaderUtils.isImplementedInterface(clazz, requiredInterface)) {
        for (Constructor<?> c : clazz.getDeclaredConstructors()) {
            //noinspection unchecked
            Constructor<RequestLogger> castedConstructor = 
              (Constructor<RequestLogger>) c;
            String name = extractCommandName(clazz);
            commandsMap.computeIfAbsent(name, n -> new HashSet<>());
            commandsMap
              .get(extractCommandName(clazz))
              .add(OBJECT_CREATOR.instantiateClass(castedConstructor));

        }
    
    } else {
        log.warn("Command didn't implemented: " 
                 + requiredInterface.getCanonicalName());
    }
}
      
      





Il existe plusieurs enregistreurs pour chaque modèle. Le reste est le même.

Maintenant, dans le bot lui-même, nous devons configurer executionTime



et rediriger les requêtes vers ces classes:



public final class CommandService {

    private static final Map<String, RequestHandler> commandsMap 
      = new HashMap<>();
    private static final Map<String, Set<RequestLogger>> loggersMap 
      = new HashMap<>();
    
    private CommandService() {
    }
    
    public static synchronized void init() {
        initCommands();
        initLoggers();
    }
    
    private static void initCommands() {
        commandsMap.putAll(CommandLoader.readCommands());
    }
    
    private static void initLoggers() {
        loggersMap.putAll(LogLoader.loadLoggers());
    }
    
    public static RequestHandler serve(String message) {
        for (Map.Entry<String, RequestHandler> entry : commandsMap.entrySet()) {
            if (entry.getKey().equals(message)) {
                return entry.getValue();
            }
        }
    
        return msg -> SendMessage.builder()
                .text("  ")
                .chatId(String.valueOf(msg.getChatId()))
                .build();
    }
    
    public static Set<RequestLogger> findLoggers(
      String message, 
      ExecutionTime executionTime
    ) {
        final Set<RequestLogger> matchedLoggers = new HashSet<>();
        for (Map.Entry<String, Set<RequestLogger>> entry:loggersMap.entrySet()) {
            for (RequestLogger logger : entry.getValue()) {
    
                if (containsExecutionTime(
                  extractExecutionTimes(logger), executionTime
                )) 
                {
                    if (message.matches(entry.getKey()))
                        matchedLoggers.add(logger);
                }
            }
    
        }
    
        return matchedLoggers;
    }
    
    private static ExecutionTime[] extractExecutionTimes(RequestLogger logger) {
        return logger.getClass().getAnnotation(Log.class).executionTime();
    }
    
    private static boolean containsExecutionTime(
      ExecutionTime[] times,
      ExecutionTime executionTime
    ) {
        for (ExecutionTime et : times) {
            if (et == executionTime) return true;
        }
    
        return false;
    }

}
public class DefaultBot extends TelegramLongPollingBot {
    private static final Logger log = LoggerFactory.getLogger(DefaultBot.class);

    public DefaultBot() {
        CommandService.init();
        log.info("Bot initialized!");
    }
    
    @Override
    public String getBotUsername() {
        return System.getenv("BOT_NAME");
    }
    
    @Override
    public String getBotToken() {
        return System.getenv("BOT_TOKEN");
    }
    
    @Override
    public void onUpdateReceived(Update update) {
        try {
            Message message = update.getMessage();
            if (message != null && message.hasText()) {
                // run "before" loggers
                CommandService
                  .findLoggers(message.getText(), ExecutionTime.BEFORE)
                  .forEach(logger -> logger.execute(message));
    
                // command execution
                SendMessage response;
                this.execute(response = CommandService
                             .serve(message.getText())
                             .execute(message));
    
                // run "after" loggers
                CommandService
                  .findLoggers(message.getText(), ExecutionTime.AFTER)
                  .forEach(logger -> logger.executeAfter(message, response));
    
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
      
      





Il est préférable de trouver le code vous-même et de regarder dans le référentiel, ou mieux encore de l'ouvrir via l'EDI. Ce référentiel est bon pour démarrer et démarrer, mais pas assez bon en tant que bot.



Premièrement, il n'y a pas assez d'abstraction entre les équipes. Autrement dit, vous ne pouvez revenir qu'à partir de chaque commande SendMessage



. Cela peut être surmonté en utilisant un niveau d'abstraction plus élevé, par exemple BotApiMethodMessage



, mais cela ne résout pas vraiment tous les problèmes.



Deuxièmement, la bibliothèque elle TelegramBots



- même , me semble-t-il, n'est pas particulièrement focalisée sur un tel travail (architecture) du bot. Si vous développez un bot à l'aide de cette bibliothèque particulière, vous pouvez utiliser Ability Bot



qui est répertorié dans le wiki de la bibliothèque elle-même. Mais je veux vraiment voir une bibliothèque à part entière avec une telle architecture. Vous pouvez donc commencer à écrire votre bibliothèque!



Bot de printemps



Cela a plus de sens lorsque vous travaillez avec l'écosystème printanier:



  • Travailler à travers des annotations ne viole pas le concept général du conteneur à ressort.
  • Nous ne pouvons pas créer de commandes nous-mêmes, mais les obtenir du conteneur, en marquant nos commandes comme des beans.
  • Nous obtenons une excellente DI du printemps.


En général, l'utilisation d'un ressort comme cadre pour un bot est un sujet pour une conversation distincte. Après tout, beaucoup peuvent penser que c'est trop difficile pour un bot (bien que, très probablement, ils n'écrivent pas non plus de bots en Java).



Mais je pense que le printemps est un bon environnement non seulement pour les applications d'entreprise / Web. Il contient juste de nombreuses bibliothèques officielles et utilisateur pour son écosystème (au printemps, je veux dire Spring Boot).



Et, plus important encore, cela vous permet d'implémenter de nombreux modèles de différentes manières fournies par le conteneur.



la mise en oeuvre



Eh bien, passons au bot lui-même.



Puisque nous écrivons sur la pile de ressorts, nous ne pouvons pas créer notre propre conteneur de commandes, mais utiliser l'existant au printemps. Ils ne peuvent pas être scannés, mais obtenus à partir du conteneur IoC .



Des développeurs plus indépendants peuvent commencer à lire le code immédiatement .



Ici, je vais analyser uniquement la lecture des commandes, bien qu'il y ait quelques points intéressants dans le référentiel lui-même que vous pouvez prendre en compte vous-même.

L'implémentation est très similaire au bot via Reflections, donc les annotations sont les mêmes.



ObjectLoader.java



@Service
public class ObjectLoader {
    private final ApplicationContext applicationContext;

    public ObjectLoader(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }
    
    public Collection<Object> loadObjectsWithAnnotation(
      Class<? extends Annotation> annotation
    ) {
        return applicationContext.getBeansWithAnnotation(annotation).values();
    }
}
      
      





CommandLoader.java



public Map<String, RequestHandler> readCommands() {

    final Map<String, RequestHandler> commandsMap = new HashMap<>();
    
    for (Object obj : objectLoader.loadObjectsWithAnnotation(Handler.class)) {
        if (obj instanceof RequestHandler) {
            RequestHandler handler = (RequestHandler) obj;
            commandsMap.put(extractCommandName(handler.getClass()), handler);
        }
    }
    
    return commandsMap;
}
      
      





Contrairement à l'exemple précédent, cela utilise déjà un niveau d'abstraction plus élevé pour les interfaces, ce qui, bien sûr, est bon. Nous n'avons pas non plus besoin de créer nous-mêmes des instances de commande.



Résumons



C'est à vous de décider ce qui convient le mieux à votre tâche. J'ai analysé trois cas pour des bots à peu près similaires:



  • Réflexions.
  • Spring-Context (pas de Spring).
  • ApplicationContext de Spring.


Cependant, je peux vous donner des conseils basés sur mon expérience:



  1. Considérez si vous avez besoin du printemps. Il fournit un conteneur IoC puissant et des capacités d'écosystème, mais tout a un prix. Je pense généralement comme ceci: si vous avez besoin d'une base de données et d'un démarrage rapide, vous avez besoin de Spring Boot. Si le bot est assez simple, vous pouvez vous en passer.
  2. Si vous n'avez pas besoin de dépendances complexes, n'hésitez pas à utiliser Reflections.


Mettre en œuvre, par exemple, JPA sans Spring Data me semble être une tâche plutôt chronophage, même si vous pouvez également envisager des alternatives sous la forme de micronaute ou de quarkus, mais je n'en ai entendu que parler et je n'ai pas assez d'expérience pour conseiller quelque chose à ce sujet.



Si vous adhérez à une approche plus propre à partir de zéro, même sans JPA, regardez ce bot, qui fonctionne via JDBC via VK et Telegram.



Vous y verrez de nombreuses entrées du formulaire:



PreparedStatement stmt = connection.prepareStatement("UPDATE alias SET aliases=?::jsonb WHERE vkid=?");
stmt.setString(1, aliases.toJSON());
stmt.setInt(2, vkid);
stmt.execute();
      
      





Mais le code a deux ans, donc je ne recommande pas de prendre tous les modèles à partir de là. Et en général, je ne recommanderais pas du tout de faire cela (travailler via JDBC).



De plus, personnellement, je n'aime pas vraiment travailler directement avec Hibernate. J'ai déjà eu la triste expérience de l'écriture DAO



et HibernateSessionFactoryUtil



(ceux qui ont écrit comprendront ce que je veux dire).



Quant à l'article lui-même, j'ai essayé de le garder court, mais assez pour qu'avec seulement cet article en main, vous puissiez commencer à développer. Pourtant, ce n'est pas un chapitre du livre, mais un article sur Habré. Vous pouvez étudier vous-même les annotations et la réflexion en général plus en profondeur, par exemple en créant le même bot.



Bonne chance à tous! Et n'oubliez pas le code promo HABR, qui offre une remise supplémentaire de 10% par rapport à celui indiqué sur le bandeau.



image
























All Articles