Praktische Anwendung von Annotationen in Java am Beispiel der Erstellung eines Telegramm-Bots

Reflection in Java ist eine spezielle API aus der Standardbibliothek, mit der Sie zur Laufzeit auf Informationen zu einem Programm zugreifen können.



Die meisten Programme verwenden Reflexion auf die eine oder andere Weise in ihren verschiedenen Formen, da es schwierig ist, ihre Fähigkeiten in einen Artikel zu integrieren.



Viele Antworten enden dort, aber was wichtiger ist, ist das Verständnis des allgemeinen Konzepts der Reflexion. Wir suchen nach kurzen Antworten auf Fragen, um das Interview erfolgreich zu bestehen, aber wir verstehen die Grundlagen nicht - woher es kam und was genau unter Reflexion zu verstehen ist.



In diesem Artikel werden wir alle diese Themen in Bezug auf Anmerkungen ansprechen und anhand eines Live-Beispiels sehen, wie Sie Ihre eigenen verwenden, finden und schreiben können.








Reflexion





Ich glaube, es wäre ein Fehler zu glauben, dass die Java-Reflexion nur auf ein Paket in der Standardbibliothek beschränkt ist. Daher schlage ich vor, es als Begriff zu betrachten, ohne an ein bestimmtes Paket gebunden zu sein.



Reflexion gegen Selbstbeobachtung



Neben der Reflexion gibt es auch das Konzept der Selbstbeobachtung. Introspektion ist die Fähigkeit eines Programms, Daten über den Typ und andere Eigenschaften eines Objekts abzurufen. Zum Beispiel instanceof



:



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





Dies ist eine sehr leistungsfähige Technik, ohne die Java nicht das wäre, was es ist. Trotzdem geht er nicht weiter als Daten zu empfangen, und Reflexion kommt ins Spiel.



Einige Reflexionsmöglichkeiten



Reflexion ist insbesondere die Fähigkeit eines Programms, sich zur Laufzeit selbst zu untersuchen und damit sein Verhalten zu ändern.



Daher ist das oben gezeigte Beispiel keine Reflexion, sondern nur eine Selbstbeobachtung des Objekttyps. Aber was ist dann Reflexion? Zum Beispiel eine Klasse erstellen oder eine Methode aufrufen, aber auf eine sehr eigenartige Weise. Unten ist ein Beispiel.



Stellen wir uns vor, wir haben kein Wissen über die Klasse, die wir erstellen möchten, sondern nur Informationen darüber, wo sie sich befindet. In diesem Fall können wir eine Klasse nicht auf offensichtliche Weise erstellen:



Object obj = new Cat();    //    ?
      
      





Verwenden wir Reflection und erstellen eine Instanz der Klasse:



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





Nennen wir seine Methode auch durch Reflexion:



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





Von der Theorie zur Praxis:



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);
    }
}
      
      





Sie können damit in Jdoodle spielen .

Trotz seiner Einfachheit gibt es in diesem Code eine Menge komplexer Dinge, und oft fehlt dem Programmierer nur die einfache Verwendung getDeclaredMethod and then invoke



.



Frage 1

Warum müssen wir im obigen Beispiel eine Instanz eines Objekts in der Aufrufmethode übergeben?



Ich werde nicht weiter gehen, da wir weit vom Thema entfernt sind. Stattdessen werde ich einen Link zu einem Artikel des älteren Kollegen Tagir Valeev hinterlassen .



Anmerkungen



Anmerkungen sind ein wichtiger Bestandteil der Java-Sprache. Dies ist eine Art Deskriptor, der an eine Klasse, ein Feld oder eine Methode gehängt werden kann. Beispielsweise haben Sie möglicherweise die Anmerkung gesehen @Override



:



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

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

}
      
      





Haben Sie sich jemals gefragt, wie es funktioniert? Wenn Sie es nicht wissen, versuchen Sie es zu erraten, bevor Sie weiterlesen.



Arten von Anmerkungen



Betrachten Sie die obige Anmerkung:



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

}
      
      





@Target



 - gibt an, wofür die Anmerkung gilt. In diesem Fall die Methode.



@Retention



 - die Lebensdauer der Anmerkung im Code (natürlich nicht in Sekunden).



@interface



- ist die Syntax zum Erstellen von Anmerkungen.



Wenn das erste und das letzte mehr oder weniger klar sind (siehe.  @Target



 In der  Dokumentation ),  @Retention



 schauen wir uns jetzt an, da es in verschiedene Arten von Anmerkungen unterteilt wird, was sehr wichtig zu verstehen ist.



Diese Anmerkung kann drei Werte annehmen:





Im ersten Fall wird die Anmerkung in den Bytecode Ihres Codes geschrieben, sollte jedoch zur Laufzeit nicht von der virtuellen Maschine beibehalten werden.



Im zweiten Fall ist die Annotation auch zur Laufzeit verfügbar, sodass wir sie verarbeiten können, z. B. alle Klassen mit dieser Annotation abrufen können.



Im dritten Fall wird die Annotation vom Compiler entfernt (sie befindet sich nicht im Bytecode). Dies sind normalerweise Anmerkungen, die nur für den Compiler nützlich sind.



Zurück zur Annotation sehen  @Override



wir, dass es hat,  RetentionPolicy.SOURCE



 was im Allgemeinen logisch ist, da es nur vom Compiler verwendet wird. Zur Laufzeit bietet diese Annotation wirklich nichts Nützliches.



SuperCat



Versuchen wir, eine eigene Anmerkung hinzuzufügen (dies ist für uns während der Entwicklung nützlich).



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!"); // <---
        }
    }
}
      
      





Lassen Sie uns zwei Katzen in unserem Haus haben: Tom und Alex. Erstellen wir eine Anmerkung für die Superkatze:



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

}

// ...

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

// ...
      
      





Gleichzeitig werden wir Tom als gewöhnliche Katze verlassen (die Welt ist unfair). Versuchen wir nun, die Klassen abzurufen, die mit diesem Element versehen wurden. Es wäre schön, eine solche Methode für die Annotationsklasse selbst zu haben:



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





Leider gibt es noch keine solche Methode. Wie finden wir dann diese Klassen?



ClassPath



Dies ist ein Parameter, der auf benutzerdefinierte Klassen verweist.



Ich hoffe, Sie kennen sie, und wenn nicht, dann beeilen Sie sich, um sie zu studieren, da dies eines der grundlegenden Dinge ist.


Nachdem wir herausgefunden haben, wo unsere Klassen gespeichert sind, können wir sie über den ClassLoader laden und die Klassen auf diese Annotation überprüfen. Gehen wir direkt zum 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()
                );
            }
        }
    
    }
}
      
      





Ich empfehle nicht, dies in Ihrem Programm zu verwenden. Der Code dient nur zu Informationszwecken!



Dieses Beispiel ist indikativ, wird jedoch aus diesem Grund nur zu Bildungszwecken verwendet:



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





Wir werden später herausfinden warum. Schauen wir uns zunächst die Zeilen von oben an:



// ...

//      
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();

// ...
      
      





Um herauszufinden, woher wir diese Dateien beziehen, betrachten Sie das JAR-Archiv, das beim Ausführen der Anwendung erstellt wird:



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





Somit classes



sind nur unsere kompilierten Dateien als Bytecode. Trotzdem ist File



dies noch keine heruntergeladene Datei, wir wissen nur, wo sie sich befinden, aber wir können immer noch nicht sehen, was sich in ihnen befindet.



Laden wir also jede Datei:



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);
}
      
      





Alles, was zuvor getan wurde, war nur, diese Methode Class.forName aufzurufen, die die benötigte Klasse lädt. Der letzte Teil besteht also darin, alle in der repoClass verwendeten Anmerkungen abzurufen und dann zu überprüfen, ob es sich um Anmerkungen handelt @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
      
      





Und du bist fertig! Nachdem wir die Klasse selbst haben, erhalten wir Zugriff auf alle Reflexionsmethoden.



Nachdenken



Wie im obigen Beispiel können wir einfach eine neue Instanz unserer Klasse erstellen. Aber vorher schauen wir uns ein paar Formalitäten an.



  • Erstens müssen Katzen irgendwo leben, also brauchen sie ein Zuhause. In unserem Fall können sie nicht ohne Zuhause existieren.
  • Zweitens erstellen wir eine Liste von Supercoats.


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





Die Verarbeitung nimmt also ihre endgültige Form an:



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





Und wieder die Überschrift der Fragen:



Frage 2

Was passiert, wenn wir eine @SuperCat



Klasse markieren , von der nicht geerbt wird Cat



?



Frage 3

Warum brauchen wir einen Konstruktor, der einen Argumenttyp annimmt Home



?


Denken Sie ein paar Minuten nach und analysieren Sie dann sofort die Antworten:



Antwort 2 : Ja ClassCastException



, da die Annotation selbst @SuperCat



nicht garantiert, dass die mit dieser Annotation gekennzeichnete Klasse etwas erbt oder implementiert.



Sie können dies überprüfen, indem Sie extends Cat



von Alex entfernen . Gleichzeitig werden Sie sehen, wie nützlich Anmerkungen sein können @Override



.



Antwort 3 : Katzen brauchen ein Zuhause, weil sie innere Klassen sind. Alles ist im Rahmen von Kapitel 15.9.3 der Java- Sprachspezifikation .



Sie können dies jedoch einfach vermeiden, indem Sie diese Klassen statisch machen. Aber wenn Sie mit Reflexion arbeiten, werden Sie oft auf solche Dinge stoßen. Und dafür müssen Sie die Java-Spezifikation nicht wirklich genau kennen. Diese Dinge sind ziemlich logisch, und Sie können sich vorstellen, warum wir eine Instanz der übergeordneten Klasse an den Konstruktor übergeben sollten, wenn dies der Fall ist non-static



.



Lassen Sie uns zusammenfassen und erhalten: 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!
      
      





Also, was ist los mit Class.forName



?



Er selbst tut genau das, was von ihm verlangt wird. Wir verwenden es jedoch falsch.



Stellen Sie sich vor, Sie arbeiten an Projekten mit 1000 oder mehr Klassen (schließlich schreiben wir in Java). Und stellen Sie sich vor, Sie laden jede Klasse, die Sie in classPath finden. Sie selbst verstehen, dass Speicher und andere JVM-Ressourcen kein Gummi sind.



Möglichkeiten, mit Anmerkungen zu arbeiten



Wenn es keine andere Möglichkeit gäbe, mit Anmerkungen zu arbeiten, wäre es sehr, sehr kontrovers, sie als Klassenbezeichnungen zu verwenden, wie zum Beispiel im Frühjahr.



Aber der Frühling scheint zu funktionieren. Ist mein Programm wegen ihnen so langsam? Leider oder zum Glück nein. Der Frühling funktioniert gut (in dieser Hinsicht), da er eine etwas andere Art der Arbeit mit ihnen verwendet.



Direkt zum Bytecode



Jeder (ich hoffe) hat irgendwie eine Vorstellung davon, was ein Bytecode ist. Es speichert alle Informationen zu unseren Klassen und deren Metadaten (einschließlich Anmerkungen).



Es ist Zeit, sich an unsere zu erinnern RetentionPolicy



. Im vorherigen Beispiel konnten wir diese Anmerkung finden, da wir angegeben haben, dass es sich um eine Laufzeitanmerkung handelt. Daher muss es im Bytecode gespeichert werden.



Warum lesen wir es nicht einfach (ja, vom Bytecode)? Aber hier werde ich kein Programm implementieren, um es aus Bytecode zu lesen, da es einen separaten Artikel verdient. Sie können es jedoch selbst tun - es wird eine großartige Übung sein, die das Material des Artikels konsolidiert.



Um sich mit dem Bytecode vertraut zu machen, können Sie mit meinem Artikel beginnen... Dort beschreibe ich die grundlegenden Bytecode-Dinge mit der Hello World! Der Artikel ist auch dann nützlich, wenn Sie nicht direkt mit Bytecode arbeiten. Es beschreibt die grundlegenden Punkte, die bei der Beantwortung der Frage helfen: Warum genau?



Willkommen zur offiziellen JVM-Spezifikation . Wenn Sie den Bytecode nicht manuell (nach Bytes) analysieren möchten, suchen Sie nach Bibliotheken wie ASM und Javassist .



Reflexionen



Reflections ist eine WTFPL-lizenzierte Bibliothek , mit der Sie alles tun können, was Sie wollen. Eine ziemlich schnelle Bibliothek für verschiedene Arbeiten mit Klassenpfaden und Metadaten. Das Nützliche ist, dass es Informationen über einige bereits gelesene Daten speichern kann, was Zeit spart. Sie können darin graben und die Store-Klasse finden.



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();
        }
    }
}
      
      





Frühlingskontext



Ich würde empfehlen, die Reflections-Bibliothek zu verwenden, da sie intern über javassist funktioniert, was darauf hinweist, dass Bytecode gelesen und nicht geladen wird.



Es gibt jedoch viele andere Bibliotheken, die auf ähnliche Weise funktionieren. Es gibt viele von ihnen, aber jetzt möchte ich nur einen von ihnen zerlegen - diesen spring-context



. Es ist vielleicht besser als das erste, wenn Sie einen Bot im Spring-Framework entwickeln. Aber hier gibt es auch ein paar Nuancen.



Wenn Ihre Klassen im Wesentlichen verwaltete Beans sind, dh sich in einem Spring-Container befinden, müssen Sie sie nicht erneut scannen. Sie können einfach über den Container selbst auf diese Beans zugreifen.



Eine andere Sache ist, wenn Sie möchten, dass Ihre getaggten Klassen Beans sind, dann können Sie dies manuell tun, ClassPathScanningCandidateComponentProvider



indem Sie über ASM arbeiten.



Auch hier ist es ziemlich selten, dass Sie diese Methode verwenden müssen, aber es lohnt sich, sie als Option in Betracht zu ziehen.

Ich habe einen Bot für VK darauf geschrieben. Hier ist ein Repository , mit dem Sie sich vertraut machen können, aber ich habe es vor langer Zeit geschrieben. Als ich nach einem Link in den Artikel suchte, sah ich, dass ich über das VK-Java-SDK Nachrichten mit nicht initialisierten Feldern erhalte, obwohl zuvor alles funktioniert hat.



Das Lustige ist, dass ich die SDK-Version noch nicht einmal geändert habe. Wenn Sie also den Grund dafür finden, bin ich Ihnen dankbar. Das Laden der Befehle selbst funktioniert jedoch einwandfrei. Genau das können Sie sich ansehen, wenn Sie ein Beispiel für die Arbeit mit sehen möchten spring-context



.



Die darin enthaltenen Befehle lauten wie folgt:



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

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





SuperCat



In diesem Repository finden Sie kommentierte Codebeispiele .



Praktische Anwendung von Anmerkungen beim Erstellen eines Telegramm-Bots



Dies war alles eine ziemlich lange, aber notwendige Einführung in die Arbeit mit Anmerkungen. Als nächstes werden wir einen Bot implementieren, aber der Zweck des Artikels ist kein Handbuch zum Erstellen. Dies ist eine praktische Anwendung von Anmerkungen. Hier kann alles sein: von Konsolenanwendungen bis zu denselben Bots für VK, Cart und andere Dinge.



Auch hier werden einige komplexe Prüfungen nicht absichtlich durchgeführt. Zuvor hatten die Beispiele beispielsweise keine Überprüfung auf Null oder korrekte Fehlerbehandlung, ganz zu schweigen von ihrer Protokollierung.



All dies geschieht, um den Code zu vereinfachen. Wenn Sie den Code aus den Beispielen entnehmen, sollten Sie ihn daher nicht faul ändern, damit Sie ihn besser verstehen und an Ihre Bedürfnisse anpassen können.



Wir werden die TelegramBots-Bibliothek mit einer MIT-Lizenz verwendenmit der Telegramm-API arbeiten. Sie können jeden anderen verwenden. Ich habe es gewählt, weil es sowohl "c" (hat eine Version mit einem Starter) als auch "ohne" Federstiefel funktionieren könnte.



Eigentlich möchte ich den Code auch nicht durch Hinzufügen einer Art Abstraktion komplizieren. Wenn Sie möchten, können Sie etwas Universelles tun, aber überlegen Sie, ob es sich lohnt. Für diesen Artikel verwenden wir daher häufig konkrete Klassen aus diesen Bibliotheken, die unsere binden Code zu ihnen.



Reflexionen



Der erste Bot in der Zeile ist ein Bot, der in der Reflexionsbibliothek ohne Spring geschrieben wurde. Wir werden nicht alles analysieren, sondern nur die Hauptpunkte, insbesondere die Bearbeitung von Anmerkungen. Bevor Sie es im Artikel analysieren, können Sie selbst herausfinden, wie es in meinem Repository funktioniert .



In allen Beispielen werden wir uns an die Tatsache halten, dass der Bot aus mehreren Befehlen besteht, und wir werden diese Befehle nicht manuell laden, sondern einfach Anmerkungen hinzufügen. Hier ist ein Beispielbefehl:

@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();
}
      
      





In diesem Fall wird der Parameter /hello



wird geschrieben werden , value



in der Anmerkung. value ist so etwas wie die Standardanmerkung. Das ist @Handler("/hello")



= @Handler(value = "/hello")



.



Wir werden auch Logger hinzufügen. Wir werden sie entweder vor oder nach der Bearbeitung der Anfrage anrufen und sie auch kombinieren:



@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());
    }
}
      
      





Wir können aber auch einen Parameter hinzufügen, um den Logger für bestimmte Nachrichten auszulösen:



@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!");
    }
}

      
      





Oder ausgelöst nach Bearbeitung der Anfrage:



@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());
    }
}
      
      





Oder sowohl dort als auch dort:



@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");
    }
}
      
      





Wir können dies tun, weil es executionTime



eine Reihe von Werten benötigt. Das Funktionsprinzip ist einfach. Beginnen wir also mit der Verarbeitung dieser Anmerkungen:



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();
    }
}
      
      





Tatsächlich erstellen wir nur eine Karte mit dem Befehlsnamen, die wir aus dem Wert value



in der Anmerkung entnehmen . Der Quellcode ist hier .



Wir machen dasselbe mit Log, nur kann es mehrere Logger mit den gleichen Mustern geben, also ändern wir unsere Datenstruktur geringfügig:



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());
    }
}
      
      





Für jedes Muster gibt es mehrere Logger. Der Rest ist der gleiche.

Jetzt müssen wir im Bot selbst Anforderungen konfigurieren executionTime



und an diese Klassen umleiten:



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();
        }
    }
}
      
      





Es ist am besten, den Code selbst zu lernen und im Repository nachzuschauen oder ihn noch besser über die IDE zu öffnen. Dieses Repository ist gut für den Einstieg und den Einstieg, aber als Bot nicht gut genug.



Erstens gibt es nicht genug Abstraktion zwischen den Teams. Das heißt, Sie können nur von jedem Befehl zurückkehren SendMessage



. Dies kann beispielsweise durch die Verwendung einer höheren Abstraktionsebene überwunden werden, BotApiMethodMessage



löst jedoch nicht wirklich alle Probleme.



Zweitens ist die Bibliothek selbst TelegramBots



, wie es mir scheint, nicht besonders auf eine solche Arbeit (Architektur) eines Bots ausgerichtet. Wenn Sie einen Bot mit dieser speziellen Bibliothek entwickeln, können Sie ihn verwenden Ability Bot



welches im Wiki der Bibliothek selbst aufgeführt ist. Aber ich möchte wirklich eine vollwertige Bibliothek mit einer solchen Architektur sehen. So können Sie mit dem Schreiben Ihrer Bibliothek beginnen!



Frühlingsbot



Dies ist sinnvoller, wenn Sie mit dem Frühlingsökosystem arbeiten:



  • Das Durcharbeiten von Anmerkungen verstößt nicht gegen das allgemeine Konzept des Federbehälters.
  • Wir können keine Befehle selbst erstellen, sondern sie aus dem Container abrufen und unsere Befehle als Beans markieren.
  • Wir bekommen ausgezeichnete DI aus dem Frühjahr.


Im Allgemeinen ist die Verwendung einer Feder als Rahmen für einen Bot ein Thema für ein anderes Gespräch. Schließlich denken viele vielleicht, dass dies für einen Bot zu schwierig ist (obwohl sie höchstwahrscheinlich auch keine Bots in Java schreiben).



Aber ich denke, der Frühling ist eine gute Umgebung, nicht nur für Unternehmens- / Webanwendungen. Es enthält nur eine Menge offizieller und Benutzerbibliotheken für sein Ökosystem (Frühling bedeutet Spring Boot).



Und vor allem können Sie damit viele Muster auf unterschiedliche Weise implementieren, die vom Container bereitgestellt werden.



Implementierung



Kommen wir zum Bot selbst.



Da wir auf den Federstapel schreiben, können wir keinen eigenen Befehlscontainer erstellen, sondern den im Frühling vorhandenen verwenden. Sie können nicht gescannt, sondern aus dem IoC-Container bezogen werden .



Unabhängigere Entwickler können sofort mit dem Lesen von Code beginnen .



Hier werde ich nur Lesebefehle analysieren, obwohl es im Repository selbst einige interessante Punkte gibt, die Sie selbst berücksichtigen können.

Die Implementierung ist dem Bot durch Reflections sehr ähnlich, daher sind die Anmerkungen gleich.



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;
}
      
      





Im Gegensatz zum vorherigen Beispiel wird hier bereits eine höhere Abstraktionsebene für Schnittstellen verwendet, was natürlich gut ist. Wir müssen auch keine Befehlsinstanzen selbst erstellen.



Fassen wir zusammen



Es liegt an Ihnen, zu entscheiden, was für Ihre Aufgabe am besten ist. Ich habe drei Fälle für ungefähr ähnliche Bots analysiert:



  • Reflexionen.
  • Frühlingskontext (kein Frühling).
  • ApplicationContext aus dem Frühjahr.


Aufgrund meiner Erfahrungen kann ich Ihnen jedoch Ratschläge geben:



  1. Überlegen Sie, ob Sie Frühling brauchen. Es bietet leistungsstarke IoC-Container- und Ökosystemfunktionen, aber alles hat seinen Preis. Normalerweise denke ich so: Wenn Sie eine Datenbank und einen Schnellstart benötigen, benötigen Sie Spring Boot. Wenn der Bot einfach genug ist, können Sie darauf verzichten.
  2. Wenn Sie keine komplexen Abhängigkeiten benötigen, können Sie Reflections verwenden.


Die Implementierung von JPA zum Beispiel ohne Spring Data scheint mir eine ziemlich zeitaufwändige Aufgabe zu sein, obwohl Sie auch Alternativen in Form von Mikronaut oder Quarkus prüfen können, aber ich habe nur davon gehört und habe nicht genug Erfahrung, um etwas dazu zu beraten.



Wenn Sie auch ohne JPA an einem saubereren Ansatz von Grund auf festhalten, schauen Sie sich diesen Bot an, der über JDBC über VK und Telegramm funktioniert.



Dort sehen Sie viele Einträge des Formulars:



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





Der Code ist jedoch zwei Jahre alt, daher empfehle ich nicht, alle Muster von dort zu übernehmen. Und im Allgemeinen würde ich dies überhaupt nicht empfehlen (Arbeit über JDBC).



Auch persönlich arbeite ich nicht gerne direkt mit Hibernate. Ich hatte bereits die traurige Erfahrung des Schreibens DAO



und HibernateSessionFactoryUtil



(diejenigen, die geschrieben haben, werden verstehen, was ich meine).



Der Artikel selbst habe ich versucht, ihn kurz zu halten, aber genug, damit Sie mit der Entwicklung dieses Artikels beginnen können. Dies ist jedoch kein Kapitel in einem Buch, sondern ein Artikel über Habré. Sie können Anmerkungen und Reflexionen im Allgemeinen selbst genauer untersuchen, indem Sie beispielsweise denselben Bot erstellen.



Allen viel Glück! Und vergessen Sie nicht den HABR-Gutscheincode, der einen zusätzlichen Rabatt von 10% auf den auf dem Banner angegebenen gewährt.



Bild
























All Articles