- Баллы
- 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:
Пишем основной класс плагина:
Смотрим код магического класса MyBinderModule:
Ну тогда уж и двое остальных:
Разберёмся, что тут происходит.
На первых трёх строках в onEnable() мы создаём новый контекст. Сразу передаём в него this, чтобы все классы, которым в будущем нужен будет наш плагин, получили уже существующий объект.
Далее, мы просим некий "инжектор" проинжектировать в наш плагин зависимости — это поля, помеченные аннотацией @Inject. Он сам их найдёт и попытается создать нужные объекты, MyCommand и MyListener. Из-за того, что наш объект плагина был создан сервером, а не нами, мы не можем выполнить инжекцию через конструктор, поэтому пользуемся аннотированными полями (можно было использовать и сеттеры, но я не увидел смысла). Создавая объекты, которые нужно подложить нам в поля, Guice их одновременно кладёт в и контекст, откуда они будут возвращены при необходимости другим классам.
У остальных классов я указал @RAC с отдельным указанием поместить на сгенерированный конструктор аннотацию @Inject. Когда Guice требуется создать какой-то класс, он видим, что у него есть такой помеченный конструктор, и передаёт в него все нужные зависимости, попутно создавая и их тоже.
Дополнительная инфа.
В методе configure() класса MyBinderModule можно заранее "закинуть" в контекст некоторые объекты, которые в дальнейшем пригодится использовать (инжектировать в другие классы), например:
Ссылка на полную документацию Google Guice.
P. S. Не забудьте, что Guice — рантаймовая зависимость, и его нужно либо указать в plugin.yml (см. ниже спойлер), либо зашейдить в .jar!
Вот, как-то так! Всем успешного плагино-писания!
Зачем это нужно?
Простейший плагин — чаще всего один-два-три класса, сам класс 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);
P. S. Не забудьте, что Guice — рантаймовая зависимость, и его нужно либо указать в plugin.yml (см. ниже спойлер), либо зашейдить в .jar!
Вот, как-то так! Всем успешного плагино-писания!
Последнее редактирование:
