Semperante
4 уровень

Java @Аннотации. Что это и как этим пользоваться?

Пост из группы Random
937272 участников
Данная статья предназначена для людей, которые никогда не работали с Аннотациями, но хотели бы разобраться что это и с чем его едят. Если же вы имеете опыт в данной сфере, не думаю, что эта статья как-то расширить ваши знания (да и собственно такую цель я не преследую).
И так, приступим. Аннотаций в Java, являются своего рода метками в коде, описывающими метаданные для функции/класса/пакета. Например, всем известная Аннотация @Override, обозначающая, что мы собираемся переопределить метод родительского класса. Да, с одной стороны можно и без неё, но если у родителей не окажется этого метода, существует вероятность, что мы зря писали код, т.к. конкретно этот метод может и не вызваться никогда, а с Аннотацией @Override компилятор нам скажет, что: "Я не нашел такого метода в родителях... что-то здесь не чисто". Однако Аннотации могут нести в себе не только смысл "для надежности", в них можно хранить какие-то данные, которые после будут использоваться. Собственно, думаю, хватит абстрактной теоритической болтавни, давайте разберем на примере бота. Допустим вы хотите написать бота для какой-то соц. сети. У всех крупных сетей, таких как: ВК, Facebook, Discord, есть свои API которые позволяют написать бота. Для этих же сетей есть уже написанные библиотеки для работы с API, на языке Java в том числе. Поэтому не будем углубляться в работу какой-либо API, либо же библиотеки. Всё что нам нужно знать в данном примере, что наш бот умеет реагировать на сообщения отправленные в чат, в котором, собственно, наш бот находится. Т.е допустим у нас есть класс MessageListener с функцией:
public class MessageListener
{
    public void onMessageReceived(MessageReceivedEvent event)
    {
    }
}
Которая отвечает за обработку принятого сообщения. Всё что нам нужно от класса MessageReceivedEvent, это строка полученного сообщения (например "Привет" или "Бот, привет"). (стоит учесть, в разных библиотеках эти классы называются по-разному. Я использовал библиотеку для Discord). И вот мы хотим сделать так, чтобы бот реагировал на какие-то команды, начинающиеся с "Бот" (с запятой или без решайте сами, для урока решим, что запятой там быть не должно). т.е Уже наша функция будет начинаться с чего-то вроде:
public void onMessageReceived(MessageReceivedEvent event)
{
    String message = event.getMessage().toLowerCase(); //Убираем чувстительность к регистру (БоТ, бОт и т.д.)
    if (message.startsWith("бот"))
    {

    }
}
И вот теперь перед нами есть множество путей, а как же реализовать ту или иную команду. Бесспорно для начала нужно отделить команду от её аргументов, т.е разбить на массив.
public void onMessageReceived(MessageReceivedEvent event)
{

    String message = event.getMessage().toLowerCase(); //Убираем чувстительность к регистру (БоТ, бОт и т.д.)
    if (message.startsWith("бот"))
    {
        try
        {
            String[] args = message.split(" "); //получим массив {"Бот", "(команду)", "аргумент1", "аргумент2",... "аргументN"};
            //Для удобства уберем "бот" и отделим команду от аргументов
            String command = args[1].toLowerCase();
            String[] nArgs = Arrays.copyOfRange(args, 2, args.length);
            //Получили command = "(команда)"; nArgs = {"аргумент1", "аргумент2",..."аргументN"}; Данный массив может быть пустым
        }
        catch (ArrayIndexOutOfBoundsException e)
        {
            //Вывод списка команд или какого-либо сообщения, в случае если просто написать "Бот"
        }
    }
}
Данного куска кода нам никак не избежать, потому что отделение команды от аргументов нужно всегда. А вот дальше уже у нас есть выбор:
  • Сделать if(command.equalsIngnoreCase("..."))
  • Сделать switch(command)
  • Сделать ещё какой-то способ обработки...
  • Либо же прибегнуть к помощи Аннотаций.
И вот мы наконец дошли до практической части использования Аннотаций. Давайте рассмотрим код аннотации для нашей задачи (он может отличаться, конечно же).
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME) //Указывает, что наша Аннотация может использована во время выполнения через Reflection (нам как раз это нужно).
@Target(ElementType.METHOD) //Указывает, что целью нашей Аннотации является метод (не класс, не переменная, не поле, а именно метод).
public @interface Command //Описание. Заметим, что перед interface стоит @;
{
    String name(); //Команда за которую будет отвечать функция (например "привет");

    String args(); //Аргументы команды, использоваться будут для вывода списка команд

    int minArgs() default 0; //Минимальное количество аргументов, сразу присвоили 0 (логично)

    String desc(); //Описание, тоже для списка

    int maxArgs() default Integer.MAX_VALUE; //Максимальное число аргументов. Вцелом необязательно, но тоже можно использовать

    boolean showInHelp() default true; //Показывать ли команду в списке (вовсе необязательная строка, но мало ли, пригодится!)

    String[] aliases(); //Какие команды будут считаться эквивалентными нашей (Например для "привет", это может быть "Здаров", "Прив" и т.д., под каждый случай заводить функцию - не рационально

}
Важно! Каждый параметр описывается как функция (с круглыми скобками). В качестве параметров могут быть использованы только примитвы, String, Enum. Нельзя написать List<String> args(); - ошибка. Теперь, когда мы описали Аннотацию, давайте заведем класс, назовем его CommandListener.
public class CommandListener
{
    @Command(name = "привет",
            args = "",
            desc = "Будь культурным, поздоровайся",
            showInHelp = false,
            aliases = {"здаров"})
    public void hello(String[] args)
    {
        //Какой-то функционал, на Ваше усмотрение.
    }

    @Command(name = "пока",
            args = "",
            desc = "",
            aliases = {"удачи"})
    public void bie(String[] args)
    {
         // Функционал
    }

    @Command(name = "помощь",
            args = "",
            desc = "Выводит список команд",
            aliases = {"help", "команды"})
    public void help(String[] args)
    {
        StringBuilder sb = new StringBuilder("Список команд: \n");
        for (Method m : this.getClass().getDeclaredMethods())
        {
            if (m.isAnnotationPresent(Command.class))
            {
                Command com = m.getAnnotation(Command.class);
                if (com.showInHelp()) //Если нужно показывать команду в списке.
                {
                    sb.append("Бот, ").append(com.name()).append(" ").append(com.args()).append(" - ").append(com.desc()).append("\n");
                }
            }
        }
        //Отправка sb.toString();

    }
}
Стоит отметить одно небольшое неудобство: т.к. мы сейчас боримся за универсальность, все функции должны иметь одинаковый список формальных параметров, поэтому даже если у команды нет аргументов, у функции должен быть параметр String[] args. Мы сейчас описали 3 команды: привет, пока, помощь. Теперь давайте модифицируем наш MessageListener так, чтобы он как-то с этим работал. Для удобства и скорости работы, будем сразу хранить наши команды в HashMap:
public class MessageListner
{
    private static final Map<String, Method> commands = new HashMap<>(); //Map который хранит как ключ команду ("привет"), а как значение функцию которая будет обрабатывать команду
    private static final CommandListener listener = new CommandListener(); //Объект класса с командами (по сути нужен нам для рефлекции)

    static
    {

        for (Method m : listener.getClass().getDeclaredMethods()) //Берем список всех методов в классе CommandListener
        {
            if (m.isAnnotationPresent(Command.class)) //Смотрим, есть ли у метода нужная нам Аннотация @Command
            {
                Command cmd = m.getAnnotation(Command.class); //Берем объект нашей Аннотации
                commands.put(cmd.name(), m); //Обращаемся к аргументу name (чтобы использовать его как ключ), m - переменная хранящая наш метод
                for (String s : cmd.aliases())  //Также заносим каждый элемент aliases как ключ указывающий на тот же самый метод.
                {
                    commands.put(s, m);
                }
            }
        }
    }

    public void onMessageReceived(MessageReceivedEvent event)
    {

        String message = event.getMessage().toLowerCase();
        if (message.startsWith("бот"))
        {
            try
            {
                String[] args = message.split(" ");
                String command = args[1].toLowerCase();
                String[] nArgs = Arrays.copyOfRange(args, 2, args.length);
                Method m = commands.get(command);
                if (m == null)
                {
                    //(вывод помощи)
                    return;
                }
                Command com = m.getAnnotation(Command.class);
                if (nArgs.length < com.minArgs())
                {
                    //что-то если аргументов меньше чем нужно
                }
                else if (nArgs.length > com.maxArgs())
                {
                    //что-то если аргументов больше чем нужно
                }
                m.invoke(listener, nArgs); //Через рефлекцию вызываем нашу функцию-обработчик (именно потому что мы всегда передаем nArgs у функции должен быть параметр String[] args - иначе она просто не будет найдена);
            }
            catch (ArrayIndexOutOfBoundsException e)
            {
                //Вывод списка команд или какого-либо сообщения, в случае если просто написать "Бот"
            }
        }
    }
}
Вот собственно и всё что нужно, чтобы наши команды работали. Теперь добавление новой команды, это не новый if, не новый case в которых нужно было бы заново переучесть количество аргументов, также пришлось бы переписывать help добавляя в него новые строки. Теперь же, чтобы добавить команду, нам нужно просто в классе CommandListener добавить новую функцию с аннотацией @Command и всё, команда добавлена, случаи учтены, help дополнен автоматически. Абсолютно бесспорно, что данную задачу можно решить множеством других путей. Да, всё что можно сделать при помощи аннотаций/рефлекций можно сделать и без них, вопрос лишь в удобстве, оптимальности и размерах кода, конечно же, совать Аннотацию везде где есть малейший намек на то, что получится её использовать - тоже не самый рациональный вариант, во всем нужно знать меру =). Но при написании API, Библиотек или программ, в которых возможно повторение однотипного (но не совсем одинакового) кода, аннотации - бесспорно оптимальное решение.
Комментарии (1)
  • популярные
  • новые
  • старые
Для того, что бы оставить комментарий вы должны авторизироваться
Viacheslav 3 уровень, Санкт-Петербург
22 июня, 13:01
Нехватает про использование множества аннотаций над элементов (одной и той же, собираемой в группу). Очень нехватает разбора того, как это всё работает. Например, как тут: How do annotations work internally.