1. Вы находитесь в сообществе Rubukkit. Мы - администраторы серверов Minecraft, разрабатываем собственные плагины и переводим на различные языки плагины наших коллег из других стран.
    Скрыть объявление
Скрыть объявление
В преддверии глобального обновления, мы проводим исследования, которые помогут нам сделать опыт пользования форумом ещё удобнее. Помогите нам, примите участие!

[TUTORIAL] Dependency Injection в плагинах (при помощи Google Guice)

Тема в разделе "Разработка плагинов для новичков", создана пользователем Reality_SC, 2 авг 2021.

  1. Автор темы
    Reality_SC

    Reality_SC Старожил Пользователь

    Баллы:
    123
    Имя в Minecraft:
    Reality_SC
    Спустя лет 5 решил стряхнуть пыль с майнкрафта и написать для себя простенький плагин. Но опыт кровавого энтерпрайза прочно уселся в голове и руках, и первая же попытка написать плагин без DI начала коробить. Пришлось целых 5 минут пользоваться гуглом и читать местами устаревшие туториалы, чтобы собрать что-то годное. К моему удивлению, на РуБакките не нашлось перевода данной статьи, но, в общем-то, оно настолько просто, что я решил самостоятельно поделиться этим с вами.

    Зачем это нужно?

    Простейший плагин — чаще всего один-два-три класса, сам класс MyPluginMain (extends JavaPlugin), и пусть будет ещё, например, MyCommandHandler (implements CommandExecutor) и MyListener (implements Listener). В таком случае чаще всего пользы от DI не будет видно — в MyPluginMain в методе onEnable создаются два других объекта, а в них есть ссылка на объект плагина. Либо, как вариант, в MyPluginMain есть статическая ссылка на него, а из других классов обращаются к ней (напрямую или через геттер).

    Но стоит плагину немного начать разрастаться, хотя бы ещё на пару классов с логикой, и мы начинаем отчётливо видеть ад: MyCommandHandler и MyListener нужен не только плагин, но и два других класса. Двум другим классам нужен MyListener. Первому классу с логикой нужен второй, а второму — первый. А ещё в MyListener зачем-то понадобился MyCommandHandler ¯\_(ツ)_/¯. Ну, я постарался привести максимум треша для примера. Самое забавное — почти никто из них не нужен в основном классе плагина, но они все создаются в его onEnable, сохраняются в его полях или в локальных переменных метода, передаются друг другу в конструкторы, сеттеры, или, не дай бог, в публичные (и даже статические!) поля.

    DI как раз решает эту проблему. У нас появляется контекст, в который кладутся все необходимые для работы плагина классы. Каждый класс объявляет, что ему нужно нужно, чтобы выполнять свою роль — свои зависимости. Объявляет он их через конструктор/сеттеры/поля, и DI сам ему их предоставит. Ещё раз, это важный момент — ни один класс не должен куда-то обращаться, чтобы получить необходимую зависимость — ему её предоставят. Когда вы напишете свой класс в таком ключе, он станет более независимым от других классов, и его проще будет перенести в другой проект. Вы возразите — Но ведь ему всё равно часто нужен MyPluginMain, а его нет в другом плагине! — Да, но от MyPluginMain нам чаще всего нужен только getLogger(), getServer(), getConfig() и прочее, что есть в базовом классе JavaPlugin.

    Приступим!

    Я буду пользоваться lombok, потому что я не люблю писать бойлерплейт-код сам.

    Добавляю в pom.xml плагина зависимость Google Guice:
    Код:
    <dependency>
        <groupId>com.google.inject</groupId>
        <artifactId>guice</artifactId>
        <version>5.0.1</version>
    </dependency>
    
    Пишем основной класс плагина:
    Код:
    
    public final class MyPluginMain extends JavaPlugin {
    
        @Inject private MyListener myListener;
        @Inject private MyCommands myCommands;
    
        @Override
        public void onEnable() {
            // Dependency Injection.
            MyBinderModule module = new MyBinderModule(this);
            Injector injector = module.createInjector();
            injector.injectMembers(this);
    
            // Регистрируем обработчик команд.
            getServer().getCommand("my-plugin").setExecutor(myCommand);
    
            // Регистрируем обработчик событий.
            getServer().getPluginManager().registerEvents(myListener, this);
        }
    
        @Override
        public void onDisable() {
            getServer().getServicesManager().unregisterAll(this);
            getServer().getScheduler().cancelTasks(this);
        }
    
        public void reload() {
            PluginManager pluginManager = getServer().getPluginManager();
            pluginManager.disablePlugin(this);
            pluginManager.enablePlugin(this);
        }
    }
    
    Смотрим код магического класса MyBinderModule:
    Код:
    @RequiredArgsConstructor
    public class MyBinderModule extends AbstractModule {
    
        private final MyPluginMain plugin;
    
        public Injector createInjector() {
            return Guice.createInjector(this);
        }
    
        @Override
        protected void configure() {
            // Всегда возвращать уже созданный экземпляр.
            bind(MyPluginMain.class) .toInstance(plugin);
        }
    }
    
    Ну тогда уж и двое остальных:
    Код:
    @RequiredArgsConstructor(onConstructor_ = @Inject)
    public class MyCommands implements CommandExecutor {
    
        private final MyPluginMain plugin;
        private final MyListener listener; // Он тут не нужен, но я привёл его для примера.
    
        @Override
        public boolean onCommand(
                @NotNull CommandSender sender,
                @NotNull Command command,
                @NotNull String label,
                @NotNull String[] args
        ) {
            try {
                // Как мы сюда вообще попали?
                if (!"my-plugin".equals(command.getName())) {
                    return false;
                }
    
                // Ты кто такой, давай до свидания!
                if (!sender.hasPermission("my-plugin.reload")) {
                    sender.sendMessage("Нет!");
                    return true;
                }
    
                plugin.reload();
                sender.sendMessage("Плагин перезагружен.");
                return true;
            } catch (Exception ex) {
                plugin.getLogger().severe("Exception in command processing: " + ex.getMessage());
                return true;
            }
        }
    }
    
    Код:
    @RequiredArgsConstructor(onConstructor_ = @Inject)
    public class MyListener implements Listener {
    
        private final MyCommand myCommand; // Не нужен, добавлен для примера.
    
        @EventHandler
        protected void onPlayerJoin(PlayerJoinEvent event) {
            event.getPlayer().sendMessage("Хай!");
        }
    }
    
    Разберёмся, что тут происходит.

    На первых трёх строках в onEnable() мы создаём новый контекст. Сразу передаём в него this, чтобы все классы, которым в будущем нужен будет наш плагин, получили уже существующий объект.
    Далее, мы просим некий "инжектор" проинжектировать в наш плагин зависимости — это поля, помеченные аннотацией @Inject. Он сам их найдёт и попытается создать нужные объекты, MyCommand и MyListener. Из-за того, что наш объект плагина был создан сервером, а не нами, мы не можем выполнить инжекцию через конструктор, поэтому пользуемся аннотированными полями (можно было использовать и сеттеры, но я не увидел смысла). Создавая объекты, которые нужно подложить нам в поля, Guice их одновременно кладёт в и контекст, откуда они будут возвращены при необходимости другим классам.

    У остальных классов я указал @RAC с отдельным указанием поместить на сгенерированный конструктор аннотацию @Inject. Когда Guice требуется создать какой-то класс, он видим, что у него есть такой помеченный конструктор, и передаёт в него все нужные зависимости, попутно создавая и их тоже.

    Дополнительная инфа.

    В методе configure() класса MyBinderModule можно заранее "закинуть" в контекст некоторые объекты, которые в дальнейшем пригодится использовать (инжектировать в другие классы), например:
    Код:
    // Закинем в контекст Gson.
    Gson gson = new GsonBuilder()
            .setPrettyPrinting()
            .create();
    bind(Gson.class).toInstance(gson);
    
    // Закинем в контекст HTTP-клиент.
    OkHttpClient okHttpClient = new OkHttpClient.Builder()
            .connectTimeout(Duration.ofSeconds(2))
            .readTimeout(Duration.ofSeconds(5))
            .callTimeout(Duration.ofSeconds(10))
            .build();
    bind(OkHttpClient.class).toInstance(okHttpClient);
    
    Ссылка на полную документацию Google Guice.

    P. S. Не забудьте, что Guice — рантаймовая зависимость, и его нужно либо указать в plugin.yml (см. ниже спойлер), либо зашейдить в .jar!
    upload_2021-8-2_12-36-50.png

    Вот, как-то так! Всем успешного плагино-писания!
     
    Последнее редактирование: 2 авг 2021
  2. imDaniX

    imDaniX Активный участник Пользователь

    Баллы:
    96
    Имя в Minecraft:
    imDaniX
    По-моему, как раз ничего удивительного - на РуБакките и с обычным DI как паттерном частенько не дружат - нередко приходят с одной и той же проблемой, а-ля "как использовать X в другом классе".
    Для туториала бы не использовать ломбок, ибо может вызвать конфуз у непросвещенных. Впрочем, любые туториалы здесь - любо.
     
  3. Shevchik

    Shevchik Старожил Пользователь

    Баллы:
    173
    Имя в Minecraft:
    _Shevchik_
    Так-то ничего удивительного. Нормально использовать DI можно только если всё приложение реально находится в IoC контейнере, как spring например. А вкручивание контейнера в отдельный плагин всегда приводит к тому что на стыке всегда возникают какие-нибудь проблемы. В итоге проще генерировать все эти конструкторы в IDE а потом писать бойлерпейт инициализации.
     
  4. Автор темы
    Reality_SC

    Reality_SC Старожил Пользователь

    Баллы:
    123
    Имя в Minecraft:
    Reality_SC
    Я так и начал, но на 5-м компоненте уже наплевался :)
    Пока особенных проблем не встретил, кроме того, что Guice автоматически генерирует java.util.Logger для каждого класса, и пробросить "плагиновский" уже не получается — или через кваливаеры (не пробовал, к слову), или просто инжектить плагин и брать нужный логгер всегда из него.
     

Поделиться этой страницей