Practical application of annotation in Java on the example of creating a Telegram bot

Reflection in Java is a special API from the standard library that allows you to access information about a program at runtime.



Most programs use reflection in one way or another in its various forms, because its capabilities are difficult to fit into one article.



Many answers end there, but what is more important is understanding the general concept of reflection. We are chasing short answers to questions in order to successfully pass the interview, but we do not understand the basics - where it came from and what exactly is meant by reflection.



In this article we will touch on all these issues in relation to annotations and with a live example we will see how to use, find and write your own.








Reflection





I believe it would be a mistake to think that Java reflection is limited to just a package in the standard library. Therefore, I propose to consider it as a term, without binding to a specific package.



Reflection vs Introspection



Along with reflection, there is also the concept of introspection. Introspection is the ability of a program to obtain data about the type and other properties of an object. For example, this instanceof



:



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





This is a very powerful technique, without which Java would not be what it is. Nevertheless, he does not go further than receiving data, and reflection comes into play.



Some possibilities of reflection



More specifically, reflection is the ability of a program to examine itself at runtime and use it to change its behavior.



Therefore, the example shown above is not reflection, but only introspection of the type of object. But what, then, is reflection? For example, creating a class or calling a method, but in a very peculiar way. Below is an example.



Let's imagine that we do not have any knowledge about the class that we want to create, but only information about where it is located. In this case, we cannot create a class in the obvious way:



Object obj = new Cat();    //    ?
      
      





Let's use reflection and create an instance of the class:



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





Let's also call its method through reflection:



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





From theory to practice:



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





You can play with it in Jdoodle .

Despite its simplicity, there are quite a lot of complex things going on in this code, and often the programmer lacks only simple use getDeclaredMethod and then invoke



.



Question # 1

Why, in the invoke method in the example above, should we pass an object instance?



I will not go further, since we will go far from the topic. Instead, I will leave a link to an article by senior colleague Tagir Valeev .



Annotations



Annotations are an important part of the Java language. This is some kind of descriptor that can be hung on a class, field or method. For example, you might have seen the annotation @Override



:



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

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

}
      
      





Have you ever wondered how it works? If you do not know, then before reading further, try to guess.



Types of annotations



Consider the above annotation:



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

}
      
      





@Target



 - indicates what the annotation applies to. In this case, to the method.



@Retention



 - the lifetime of the annotation in the code (not in seconds, of course).



@interface



- is the syntax for creating annotations.



If the first and last more or less clear (see.  @Target



 In the  documentation ), then  @Retention



 let's look at now, as it will be divided into several types of annotations, which is very important to understand.



This annotation can take three values:





In the first case, the annotation will be written into the bytecode of your code, but should not be persisted by the virtual machine at runtime.



In the second case, the annotation will be available at runtime, so we can process it, for example, get all the classes that have this annotation.



In the third case, the annotation will be removed by the compiler (it will not be in the bytecode). These are usually annotations that are only useful to the compiler.



Returning to the annotation  @Override



, we see that it has,  RetentionPolicy.SOURCE



 which is generally logical, given that it is used only by the compiler. In runtime, this annotation really does not provide anything useful.



SuperCat



Let's try to add our own annotation (this will come in handy for us during development).



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





Let us have two cats in our house: Tom and Alex. Let's create an annotation for the super cat:



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

}

// ...

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

// ...
      
      





At the same time, we will leave Tom as an ordinary cat (the world is unfair). Now let's try to get the classes that were annotated with this element. It would be nice to have a method like this on the annotation class itself:



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





But, unfortunately, there is no such method yet. Then how do we find these classes?



ClassPath



This is a parameter that points to custom classes.



I hope you are familiar with them, and if not, then hurry up to study it, as this is one of the fundamental things.


So, having found out where our classes are stored, we can load them through the ClassLoader and check the classes for this annotation. Let's go straight to the 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()
                );
            }
        }
    
    }
}
      
      





I do not recommend using this in your program. The code is for informational purposes only!



This example is indicative, but only used for educational purposes because of this:



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





We'll find out why later. For now, let's take a look at the lines from above:



// ...

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

// ...
      
      





To figure out where we get these files from, consider the JAR archive that is created when we run the application:



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





Thus, classes



these are just our compiled files as bytecode. Nevertheless, File



this is not yet a downloaded file, we only know where they are, but we still cannot see what is inside them.



So let's load each 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);
}
      
      





Everything that was done earlier was only to call this method Class.forName, which will load the class we need. So the final part is getting all the annotations used on the repoClass and then checking if they are annotations @SuperCat



:



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





And you're done! Now that we have the class itself, we get access to all reflection methods.



Reflecting



As in the example above, we can simply create a new instance of our class. But before that, let's look at a few formalities.



  • First, cats need to live somewhere, so they need a home. In our case, they cannot exist without a home.
  • Second, let's create a list of supercoats.


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





So the processing takes its final form:



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





And again the heading of questions:



Question # 2

What happens if we mark a @SuperCat



class that does not inherit from Cat



?



Question # 3

Why do we need a constructor that takes an argument type Home



?


Think for a couple of minutes, and then immediately analyze the answers:



Answer # 2 : Yes ClassCastException



, because the annotation itself @SuperCat



does not guarantee that the class marked with this annotation will inherit or implement something.



You can check this by removing extends Cat



from Alex. At the same time, you will see how useful annotation can be @Override



.



Answer # 3 : Cats need a home because they are inner classes. Everything is within the framework of The Java Language Specification chapter 15.9.3 .



However, you can avoid this simply by making these classes static. But when working with reflection, you will often come across this kind of thing. And you don't really need to know the Java specification thoroughly for that. These things are quite logical, and you can think of yourself why we should pass an instance of the parent class to the constructor, if it is non-static



.



Let's summarize and get: 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!
      
      





So what's wrong with Class.forName



?



He himself does exactly what is required of him. However, we are using it incorrectly.



Imagine that you are working on projects with 1000 or more classes (after all, we write in Java). And imagine loading every class you find in classPath. You yourself understand that memory and other JVM resources are not rubber.



Ways to work with annotations



If there was no other way to work with annotations, then using them as class labels, as, for example, in Spring, would be very, very controversial.



But Spring seems to work. Is my program so slow because of them? Unfortunately or fortunately, no. Spring works fine (in this regard) because it uses a slightly different way to work with them.



Straight to bytecode



Everyone (I hope) somehow has an idea of ​​what a bytecode is. It stores all the information about our classes and their metadata (including annotations).



It's time to remember ours RetentionPolicy



. In the previous example, we were able to find this annotation because we indicated that it is a runtime annotation. Therefore, it must be stored in bytecode.



So why don't we just read it (yes, from bytecode)? But here I will not implement a program to read it from bytecode, as it deserves a separate article. However, you can do it yourself - it will be a great practice that will consolidate the material of the article.



To familiarize yourself with the bytecode, you can start with my article... There I describe the basic bytecode things with the Hello World! The article will be useful even if you are not going to directly work with bytecode. It describes the fundamental points that will help answer the question: why exactly?



After that, welcome to the official JVM specification . If you do not want to parse the bytecode manually (by bytes), then look towards libraries such as ASM and Javassist .



Reflections



Reflections is a library with a WTFPL license that allows you to do whatever you want with it. A fairly fast library for various work with classpath and metadata. The useful thing is that it can save information about some of the data already read, which saves time. You can dig inside and find the Store class.



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





spring-context



I would recommend using the Reflections library, as internally it works through javassist, which indicates that it is reading bytecode, not loading it.



However, there are many other libraries that work in a similar way. There are a lot of them, but now I want to disassemble only one of them - this spring-context



. It is perhaps better than the first one when you are developing a bot in the Spring framework. But there are also a couple of nuances here.



If your classes are essentially managed beans, that is, they are in a Spring container, then you don't need to re-scan them. You can simply access these beans from the container itself.



Another thing is if you want your tagged classes to be beans, then you can do it manually through ClassPathScanningCandidateComponentProvider



that works through ASM.



Again, it is quite rare that you will need to use this method, but it is worth considering as an option.

I wrote a bot for VK on it. Here is a repository that you can familiarize yourself with, but I wrote it a long time ago, and when I went to look to insert a link into the article, I saw that through the VK-Java-SDK I receive messages with uninitialized fields, although everything worked before.



The funny thing is that I haven't even changed the SDK version, so if you find what the reason was, I will be grateful. However, loading the commands themselves works fine, which is exactly what you can look at if you want to see an example of working with spring-context



.



The commands in it are as follows:



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

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





SuperCat



You can find annotated code examples in this repository .



Practical application of annotations in creating a Telegram bot



This was all a rather long, but necessary introduction to working with annotations. Next, we will implement a bot, but the purpose of the article is not a manual for creating it. This is a practical application of annotations. There could be anything here: from console applications to the same bots for VK, cart and other things.



Also, here some complex checks will not be deliberately performed. For example, before that, the examples did not have any checks for null or correct error handling, not to mention their logging.



All this is done to simplify the code. Therefore, if you take the code from the examples, then do not be lazy to modify it, so you better understand it and customize it to suit your needs.



We will use the TelegramBots library with an MIT licenseto work with the telegram API. You can use any other. I chose it because it could work both "c" (has a version with a starter) or "without" a spring boot.



Actually, I also don't want to complicate the code by adding some kind of abstraction, if you want, you can do something universal, but think about whether it's worth it, so for this article we will often use concrete classes from these libraries, binding our code to them.



Reflections



The first bot in line is a bot written in the reflections library, without Spring. We will not analyze everything, but only the main points, in particular we are interested in the processing of annotations. Before parsing it in the article, you yourself can figure out how it works in my repository .



In all the examples, we will adhere to the fact that the bot consists of several commands, and we will not load these commands manually, but will simply add annotations. Here's an example command:

@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 this case, the parameter /hello



will be written to value



in the annotation. value is something like the default annotation. That is @Handler("/hello")



= @Handler(value = "/hello")



.



We will also add loggers. We will call them either before processing the request, or after, and also combine them:



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





But we can also add a parameter to trigger the logger for certain messages:



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

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

      
      





Or triggered after processing the request:



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





Or both there and there:



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





We can do this because it executionTime



takes an array of values. The principle of operation is simple, so let's start processing these annotations:



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

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

final Class<RequestHandler> requiredInterface = RequestHandler.class;

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

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

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





In fact, we just create a map with the command name, which we take from the value value



in the annotation. The source code is here .



We do the same with Log, only there can be several loggers with the same patterns, so we slightly change our data structure:



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





There are several loggers for each pattern. The rest is the same.

Now, in the bot itself, we need to configure executionTime



and redirect requests to these classes:



public final class CommandService {

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

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

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





It is best to find out the code yourself and look in the repository, or even better open it through the IDE. This repository is good for getting started and getting started, but not good enough as a bot.



First, there is not enough abstraction between teams. That is, you can only return from each command SendMessage



. This can be overcome by using a higher level of abstraction, for example BotApiMethodMessage



, but this does not really solve all the problems.



Secondly, the library itself TelegramBots



, as it seems to me, is not particularly focused on such work (architecture) of the bot. If you are developing a bot using this particular library, you can use Ability Bot



which is listed in the wiki of the library itself. But I really want to see a full-fledged library with such an architecture. So you can start writing your library!



Spring bot



This makes more sense when working with the spring ecosystem:



  • Working through annotations does not violate the general concept of the spring container.
  • We can not create commands ourselves, but get them from the container, marking our commands as beans.
  • We get excellent DI from the spring.


In general, the use of a spring as a framework for a bot is a topic for another conversation. After all, many may think that this is too difficult for a bot (although, most likely, they do not write bots in Java either).



But I think spring is a good environment not only for enterprise / web applications. It just contains a lot of both official and user libraries for its ecosystem (by spring, I mean Spring Boot).



And most importantly, it allows you to implement a lot of patterns in different ways provided by the container.



Implementation



Well, let's get down to the bot itself.



Since we write on the spring stack, we can not create our own container of commands, but use the existing one in the spring. They can not be scanned, but obtained from the IoC container .



More independent developers can start reading code right away .



Here I will analyze just reading commands, although there are a couple of interesting points in the repository itself that you can consider on your own.

The implementation is very similar to the bot through Reflections, so the annotations are the same.



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





Unlike the previous example, this already uses a higher level of abstraction for interfaces, which, of course, is good. We also don't need to create command instances ourselves.



Let's sum up



It's up to you to decide what is best for your task. I have parsed three cases for roughly similar bots:



  • Reflections.
  • Spring-Context (no Spring).
  • ApplicationContext from Spring.


However, I can give you advice based on my experience:



  1. Consider if you need Spring. It provides a powerful IoC container and ecosystem capabilities, but everything comes at a price. I usually think like this: if you need a database and a quick start, then you need Spring Boot. If the bot is simple enough, then you can do without it.
  2. If you don't need complex dependencies, then feel free to use Reflections.


Implementing, for example, JPA without Spring Data seems to me a rather time-consuming task, although you can also look at alternatives in the form of micronaut or quarkus, but I have only heard about them and do not have enough experience to advise something on this.



If you are an adherent of a cleaner approach from scratch, even without JPA, then look at this bot, which works through JDBC via VK and Telegram.



There you will see many entries of the form:



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





But the code is two years old, so I don't recommend taking all the patterns from there. And in general, I would not recommend doing this at all (work through JDBC).



Also, personally, I don't really like working directly with Hibernate. I already had the sad experience of writing DAO



and HibernateSessionFactoryUtil



(those who wrote will understand what I mean).



As for the article itself, I tried to keep it short, but enough so that with only this article in hand, you can start developing. Still, this is not a chapter in the book, but an article on Habré. You can learn more about annotations and reflection in general yourself, for example, by creating the same bot.



Good luck to all! And do not forget about the HABR promo code, which gives an additional 10% discount to the one indicated on the banner.



image
























All Articles