Telegramボットの作成例でのJavaでの注釈の実用的なアプリケーション

Reflection in Javaは、実行時にプログラムに関する情報にアクセスできるようにする、標準ライブラリの特別なAPIです。



ほとんどのプログラムは、その機能を1つの記事に収めることが難しいため、さまざまな形で何らかの形でリフレクションを使用します。



多くの答えはそこで終わりますが、より重要なのは反射の一般的な概念を理解することです。インタビューに合格するために、質問に対する短い回答を追いかけていますが、基本的なこと、つまり、それがどこから来たのか、そして正確に何を反映するのかを理解していません。



この記事では、注釈に関連するこれらすべての問題に触れ、実際の例を使用して、独自の問題を使用、検索、および作成する方法を説明します。








反射





Javaリフレクションが標準ライブラリのパッケージだけに限定されていると考えるのは間違いだと思います。したがって、特定のパッケージに拘束されることなく、それを用語と見なすことを提案します。



リフレクションvsイントロスペクション



振り返りとともに、内省の概念もあります。イントロスペクションは、オブジェクトのタイプやその他のプロパティに関するデータを取得するプログラムの機能です。たとえば、これ instanceof







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





これは非常に強力な手法であり、それがなければJavaはそれ自体ではありません。それにもかかわらず、彼はデータを受信する以上のことはせず、反省が働き始めます。



反射のいくつかの可能性



より具体的には、リフレクションは、実行時にそれ自体を調べ、それを使用してその動作を変更するプログラムの機能です。



したがって、上記の例は反映ではなく、オブジェクトのタイプの内省のみです。しかし、それでは、反射とは何ですか?たとえば、クラスを作成したり、メソッドを呼び出したりしますが、非常に特殊な方法です。以下に例を示します。



作成したいクラスについての知識はなく、クラスが配置されている場所についての情報しか持っていないことを想像してみましょう。この場合、明白な方法でクラスを作成することはできません。



Object obj = new Cat();    //    ?
      
      





リフレクションを使用して、クラスのインスタンスを作成しましょう。



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





また、リフレクションを介してそのメソッドを呼び出しましょう。



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





理論から実践へ:



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





Jdoodleで遊ぶことができ ます。

その単純さにもかかわらず、このコードでは非常に多くの複雑なことが起こっており、多くの場合、プログラマーは単純な使用法しか欠いていません getDeclaredMethod and then invoke







質問#1

上記の例のinvokeメソッドで、オブジェクトインスタンスを渡す必要があるのはなぜですか?



トピックから遠く離れるので、これ以上は進みません。代わりに、先輩の TagirValeevによる記事リンクを残して おきます。



注釈



注釈はJava言語の重要な部分です。これは、クラス、フィールド、またはメソッドに掛けることができるある種の記述子です。たとえば、注釈を見たことがあるかもしれません @Override







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

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

}
      
      





それがどのように機能するのか疑問に思ったことはありますか?わからない場合は、さらに読む前に、推測してみてください。



注釈の種類



上記の注釈を検討してください。



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

}
      
      





@Target



 -注釈の適用対象を示します。この場合、メソッドに。



@Retention



 -コード内の注釈の存続期間(もちろん、秒単位ではありません)。



@interface



-は注釈を作成するための構文です。



(参照してください。多かれ少なかれ明確な最初と最後の場合  @Target



 では  ドキュメント)、そして  @Retention



 、理解することが非常に重要である、注釈のいくつかのタイプに分けられるように、今ではのを見てみましょう。



この注釈は、次の3つの値を取ることができます。





最初のケースでは、注釈はコードのバイトコードに書き込まれますが、実行時に仮想マシンによって永続化されるべきではありません。



2番目のケースでは、注釈は実行時に使用可能になります。これにより、注釈を処理できます。たとえば、この注釈を持つすべてのクラスを取得できます。



3番目のケースでは、注釈はコンパイラーによって削除されます(バイトコードには含まれません)。これらは通常、コンパイラーにのみ役立つ注釈です。



注釈@Override



戻ると、  RetentionPolicy.SOURCE



 コンパイラによってのみ使用されることを考えると、一般的に論理的であることがわかり  ます。実行時には、この注釈は実際には何も役に立ちません。



SuperCat



独自の注釈を追加してみましょう(これは開発時に役立ちます)。



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





私たちの家にトムとアレックスの2匹の猫を飼いましょう。スーパーキャットの注釈を作成しましょう:



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

}

// ...

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

// ...
      
      





同時に、トムは普通の猫のままにしておきます(世界は不公平です)。それでは、この要素で注釈が付けられたクラスを取得してみましょう。注釈クラス自体に次のようなメソッドがあると便利です。



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





しかし、残念ながら、そのような方法はまだありません。では、これらのクラスをどのように見つけるのでしょうか。



ClassPath



これは、カスタムクラスを指すパラメーターです。



あなたがそれらに精通していることを願っています、そしてそうでなければ、これは基本的なことの一つなので、それを研究するために急いでください。


したがって、クラスが格納されている場所を見つけたら、ClassLoaderを介してクラスをロードし、クラスでこの注釈を確認できます。コードに直接行きましょう:



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





プログラムでこれを使用することはお勧めしません。コードは情報提供のみを目的としています。



この例は参考用ですが、次の理由から教育目的でのみ使用されています。



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





その理由は後でわかります。今のところ、上からの行を見てみましょう:



// ...

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

// ...
      
      





これらのファイルをどこから取得するかを理解するために、アプリケーションの実行時に作成されるJARアーカイブを見てみましょう。



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





したがって、 classes



これらはバイトコードとしてコンパイルされたファイルにすぎません。それにもかかわらず、 File



これはまだダウンロードされたファイルではなく、それらがどこにあるかを知っているだけですが、それでもそれらの中に何があるかはわかりません。



それでは、各ファイルをロードしましょう。



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





以前に行われたことはすべて、このメソッドClass.forNameを呼び出すことだけでした。これにより、必要なクラスがロードされます。したがって、最後の部分は、repoClassで使用されるすべての注釈を取得し、それらが注釈であるかどうかを確認することです @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
      
      





そして、あなたは完了です!クラス自体ができたので、すべてのリフレクションメソッドにアクセスできます。



反映



上記の例のように、クラスの新しいインスタンスを簡単に作成できます。しかしその前に、いくつかの手続きを見てみましょう。



  • まず、猫はどこかに住む必要があるので、家が必要です。私たちの場合、彼らは家なしでは存在できません。
  • 次に、スーパーコートのリストを作成しましょう。


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





したがって、処理は最終的な形式になります。



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





そして再び質問の見出し:



質問#2 から継承しないクラス

をマークするとどうなり ますか? 質問#3なぜ引数型をとるコンストラクターが必要なの ですか? @SuperCat



Cat









Home





数分間考えてから、すぐに答えを分析してください。



回答#2:はい ClassCastException



。注釈自体 @SuperCat



は、この注釈でマークされたクラスが何かを継承または実装することを保証しないためです。 Alexから



削除することでこれを確認でき extends Cat



ます。同時に、注釈がいかに役立つかがわかります @Override







回答#3:猫は内なるクラスなので、家が必要です。 Java言語仕様の第15.9.3章のフレームワーク内のすべて



ただし、これらのクラスを静的にするだけで、これを回避できます。しかし、リフレクションを扱うとき、あなたはしばしばこの種のことに出くわすでしょう。そのために、Java仕様を完全に知る必要はありません。これらは非常に論理的であり、親クラスのインスタンスをコンストラクターに渡す必要がある理由を自分で考えることができます non-static







要約して取得しましょう: 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!
      
      





では、何が問題になってい Class.forName



ますか?



彼自身が彼に必要なことを正確に行います。ただし、誤って使用しています。



1000以上のクラスを持つプロジェクトに取り組んでいると想像してください(結局のところ、Javaで記述しています)。そして、classPathで見つけたすべてのクラスをロードすることを想像してください。あなた自身は、メモリやその他のJVMリソースがゴムではないことを理解しています。



注釈を操作する方法



注釈を操作する他の方法がなかった場合、たとえばSpringのように、注釈をクラスラベルとして使用することは、非常に物議を醸すでしょう。



しかし、それでも春はうまくいくようです。それらのために私のプログラムはとても遅いですか?残念ながら、または幸いなことに、いいえ。Springは(この点で)正常に機能します。これは、Springの操作方法が少し異なるためです。



バイトコードに直接



誰もが(私は願っています)どういうわけかバイトコードが何であるかについての考えを持っています。クラスとそのメタデータ(注釈を含む)に関するすべての情報が格納されます。



私たちのことを思い出す時が来ました RetentionPolicy



。前の例では、ランタイムアノテーションであることを示したため、このアノテーションを見つけることができました。したがって、バイトコードで保存する必要があります。



では、なぜそれを(はい、バイトコードから)読み取らないのですか?ただし、別の記事に値するため、ここではバイトコードから読み取るプログラムを実装しません。ただし、自分で行うことはできます。記事の内容を統合するのは素晴らしい方法です。



バイトコードに慣れるために、私の記事から始めることができます ..。そこで、Hello World!で基本的なバイトコードについて説明します。この記事は、バイトコードを直接操作する予定がない場合でも役立ちます。それは質問に答えるのを助ける基本的なポイントを説明します:なぜ正確に?



その後、公式のJVM仕様へようこそ バイトコードを手動で(バイト単位で)解析したくない場合は、ASMJavassistなどのライブラリを検討してください



反射



Reflectionsは、WTFPLライセンス備えたライブラリであり、これ を使用してやりたいことが何でもできます。クラスパスとメタデータを使用したさまざまな作業のためのかなり高速なライブラリ。便利なのは、すでに読み取ったデータの一部に関する情報を保存できるため、時間を節約できることです。内部を掘り下げて、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();
        }
    }
}
      
      





春のコンテキスト



内部的にはjavassistを介して機能するため、Reflectionsライブラリを使用することをお勧めします。これは、バイトコードをロードするのではなく、読み取ることを示します。



ただし、同様に機能するライブラリは他にもたくさんあります。それらはたくさんありますが、今はそのうちの1つだけを分解したいと思います-これ spring-context



。 Springフレームワークでボットを開発している場合は、おそらく最初のものよりも優れています。しかし、ここにはいくつかのニュアンスもあります。



クラスが基本的にマネージドBeanである場合、つまり、Springコンテナ内にある場合は、クラスを再スキャンする必要はありません。コンテナ自体からこれらのBeanに簡単にアクセスできます。



もう1つは、タグ付けされたクラスをBeanにする場合、ClassPathScanningCandidateComponentProvider



ASMを介し手動で実行できます



繰り返しますが、この方法を使用する必要があることは非常にまれですが、オプションとして検討する価値があります。

その上にVK用のボットを書きました。これはあなたが精通している リポジトリですが、私はずっと前にそれを書きました、そして私が記事にリンクを挿入しようとしたとき、VK-Java-SDKを通して私が初期化されていないフィールドを持つメッセージを受け取るのを見ました



おもしろいことに、SDKのバージョンも変更していないので、理由がわかればありがたいです。ただし、コマンド自体のロードは正常に機能します。これは、の操作例を確認したい場合に確認できるものです spring-context







その中のコマンドは次のとおりです。



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

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





注釈付きのコード例 SuperCat



は、この リポジトリにあります



Telegramボットの作成における注釈の実用的なアプリケーション



これはすべてかなり長いものでしたが、注釈を操作するために必要な入門書でした。次に、ボットを実装しますが、記事の目的はそれを作成するためのマニュアルではありません。これは、注釈の実用的なアプリケーションです。ここには、コンソールアプリケーションから、VK、カートなどの同じボットまで、何でもあり得ます。



また、一部の複雑なチェックは、ここでは意図的に実行されません。たとえば、その前は、例には、ロギングは言うまでもなく、nullまたは正しいエラー処理のチェックがありませんでした。



これはすべて、コードを単純化するために行われます。したがって、例からコードを取得する場合は、それを変更するのに怠惰にならないでください。そうすれば、コードをよりよく理解し、ニーズに合わせてカスタマイズできます。



TelegramBotsライブラリを MITライセンスで使用しますテレグラムAPIを使用します。あなたは他のものを使うことができます。「c」(スターター付きのバージョンあり)または「スプリングブーツなし」の両方で機能するため、私はそれを選びました。



実際、私も何らかの抽象化を追加してコードを複雑にしたくありません。必要に応じて、普遍的なことを行うことができますが、それが価値があるかどうかを考えてください。この記事では、これらのライブラリの具体的なクラスを使用して、それらへのコード。



反射



並んでいる最初のボットは、Springなしでリフレクションライブラリに書き込まれたボットです。すべてを分析するのではなく、要点のみを分析します。特に、注釈の処理に関心があります。記事を分析する前に、自分のリポジトリでどのように機能するかを自分で理解でき ます



すべての例で、ボットが複数のコマンドで構成されているという事実に準拠し、これらのコマンドを手動でロードするのではなく、単に注釈を追加します。コマンドの例を次に示します。

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





この場合、パラメータ/hello



value



注釈に書き込まれ ます 値はデフォルトの注釈のようなものです。つまり @Handler("/hello")



= @Handler(value = "/hello")



です。



ロガーも追加します。リクエストを処理する前または後にそれらを呼び出し、それらを組み合わせます。



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





ただし、特定のメッセージに対してロガーをトリガーするパラメーターを追加することもできます。



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

      
      





または、リクエストの処理後にトリガーされます。



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





またはあちこちの両方:



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





executionTime



値の配列を取るため、これを行うことができ ます。操作の原理は単純なので、次の注釈の処理を開始しましょう。



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





実際、value



注釈の値から取得したコマンド名を使用してマップを作成するだけ です。ソースコードは こちらです。



Logでも同じことを行いますが、同じパターンのロガーが複数存在する可能性があるため、データ構造を少し変更します。



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





パターンごとにいくつかのロガーがあります。残りは同じです。

ここで、ボット自体で、executionTime



要求を構成してこれらのクラスにリダイレクトする必要があります



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





自分でコードを見つけてリポジトリを調べるか、IDEから開くのが最善です。このリポジトリは、開始と開始には適していますが、ボットとしては十分ではありません。



まず、チーム間の抽象化が十分ではありません。つまり、各コマンドからのみ戻ることができます SendMessage



。これは、たとえばBotApiMethodMessage



より高いレベルの抽象化を使用することで克服できます が、それでもすべての問題を実際に解決できるわけではありません。



第二に、ライブラリ自体は TelegramBots



、私には思えますが、ボットのそのような作業(アーキテクチャ)に特に焦点を当てていません。この特定のライブラリを使用してボットを開発している場合は、 Ability Bot



これは、ライブラリ自体のwikiにリストされています。しかし、私は本当にそのようなアーキテクチャを備えた本格的なライブラリを見たいと思っています。だからあなたはあなたのライブラリを書き始めることができます!



春のボット



これは、春のエコシステムで作業する場合により意味があります。



  • 注釈を処理することは、スプリングコンテナの一般的な概念に違反しません。
  • 自分でコマンドを作成することはできませんが、コンテナからコマンドを取得して、コマンドをBeanとしてマークします。
  • 春から優れたDIが得られます。


一般に、ボットのフレームワークとしてスプリングを使用することは、別の会話のトピックです。結局のところ、多くの人はこれがボットにとって難しすぎると思うかもしれません(ただし、おそらく、Javaでボットを作成していません)。



しかし、春はエンタープライズ/ Webアプリケーションだけでなく良い環境だと思います。エコシステムの公式ライブラリとユーザーライブラリの両方が含まれているだけです(春までには、Spring Bootを意味します)。



そして最も重要なことは、コンテナによって提供されるさまざまな方法で多くのパターンを実装できることです。



実装



さて、ボット自体に取り掛かりましょう。



スプリングスタックに書き込むため、独自のコマンドコンテナを作成することはできませんが、スプリングにある既存のコンテナを使用します。それらはスキャンできませんが、IoCコンテナから取得され ます。



より独立した開発者は、すぐにコードを読み始めることができ ます。



ここでは、コマンドを読み取るだけで分析しますが、リポジトリ自体には、自分で検討できる興味深い点がいくつかあります。

実装はReflectionsを介したボットと非常に似ているため、注釈は同じです。



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





前の例とは異なり、これはすでにインターフェイスに高レベルの抽象化を使用しています。これはもちろん優れています。また、コマンドインスタンスを自分で作成する必要もありません。



まとめましょう



自分のタスクに最適なものを決定するのはあなた次第です。ほぼ同様のボットの3つのケースを解析しました。



  • 反射。
  • Spring-コンテキスト(Springなし)。
  • SpringのApplicationContext。


しかし、私は私の経験に基づいてあなたにアドバイスを与えることができます:



  1. 春が必要かどうかを検討してください。強力なIoCコンテナとエコシステム機能を提供しますが、すべてに代償が伴います。私は通常、次のように考えています。データベースとクイックスタートが必要な場合は、SpringBootが必要です。ボットが十分に単純な場合は、ボットなしで実行できます。
  2. 複雑な依存関係が必要ない場合は、Reflectionsを自由に使用してください。


たとえば、Spring Dataを使用せずにJPAを実装することは、かなり面倒な作業のように思えますが、マイクロノートやクォークの形で代替案を検討することもできますが、私はそれらについて聞いたことがあり、これについて何かアドバイスするのに十分な経験がありません。



JPAがなくても、ゼロからよりクリーンなアプローチを採用している場合は、 VKとTelegramを介してJDBCを介して機能するこのボットをご覧ください。



そこにフォームの多くのエントリが表示されます。



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





ただし、コードは2年前のものであるため、そこからすべてのパターンを取得することはお勧めしません。そして、一般的に、これを行うことはまったくお勧めしません(JDBCを介して作業します)。



また、個人的には、Hibernateを直接操作するのはあまり好きではありません。私はすでに書くという悲しい経験を DAO



しました HibernateSessionFactoryUtil



(書いた人は私が何を意味するのか理解するでしょう)。



記事自体は短くするように心がけましたが、この記事だけで開発を始められるくらいです。それでも、これは本の章ではなく、ハブレに関する記事です。同じボットを作成するなどして、注釈とリフレクションの一般的な詳細を自分で学ぶことができます。



皆さんお元気で!また、バナーに示されているものにさらに10%の割引を提供するHABRプロモーションコードを忘れないでください。



画像
























All Articles