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 wirdCat
?
Frage 3
Warum brauchen wir einen Konstruktor, der einen Argumenttyp annimmtHome
?
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:
- Ü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.
- 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.