Для данного туториала необходимо уметь:
1. Работать с конфигами. Туториал
Что такое SqlService
? Это простой сервис, позволяющий получить соединение с JDBC-совместимой БД.
Реализация SqlService
уже содержит в себе пул соединений. (HikariCP)
Я покажу Вам, как использовать данный сервис на примере простого плагина для логгирования сообщений игроков в чат.
Что будет в данном плагине:
1. Запись сообщений игрока из чата в БД
2. Поддержка механизма Sponge для перезагрузки плагина
3. Правильная реализация смены сервиса: сервисы в губке, как и в bukkit, можно переназначить - сменить реализацию. Для правильной работы плагина, необходимо правильно реализовать замену SqlService
в нашем плагине.
Для начала создайте заготовку под плагин.
Далее нам необходимо получить:
1. Logger
- для логгирования действий
2. PluginContainer
- для SqlService
3. ConfigurationLoader
- место для хранения алиаса соединения(объясню позднее)
Для получения выбранных вещей, необходимо использовать механизм инжекции.
@Inject
public SqlServiceExample(Logger logger,
PluginContainer container,
@DefaultConfig(sharedRoot = true) ConfigurationLoader<CommentedConfigurationNode> configLoader) {
this.logger = logger;
this.container = container;
this.configLoader = configLoader;
this.storageManager = null;
}
Далее нам необходимо получить сам SqlService
. Сделать это можно так:
SqlService sqlService = Sponge.getServiceManager().provideUnchecked(SqlService.class);
Создаём слушателя для эвента GamePreInitializationEvent
.
@Listener
public void onGamePreInit(GamePreInitializationEvent event) throws IOException {
// код тут
}
Что нам необходимо сделать в этом event'е?
1. Получить SqlService
2. Получить алиас для строки подключения
3. Соединиться с БД и создать таблицу для плагина, если она еще не создана
4. Сообщить об успешности этого процесса
Что такое алиас для строки подключения? Это ярлык(как на рабочем столе - ярлык для игры, программы) для строки подключения!
Соответсвие алиас - jdbc url хранится в конфиге sponge под названием global.conf
. Как добавлять алиасы, смотрите в самом конце туториала.
Название алиаса мы будем хранить в конфиге плагина. Для этого нам и нужен ConfigurationLoader
.
Давайте создадим вспомогательный метод, который будет:
1. Получать конфиг
2. Получать алиас из конфига
3. Если алиас не найдет, он должен записать алиас по-умолчанию. Алиас по-умолчанию - id плагина.
Назовоём его getJdbcAliasAndSaveDefaults
.
private static String getJdbcAliasAndSaveDefaults(PluginContainer container, ConfigurationLoader<CommentedConfigurationNode> configLoader)
throws IOException {
ConfigurationNode rootNode = configLoader.load(ConfigurationOptions.defaults().setShouldCopyDefaults(true));
String urlAlias = rootNode.getNode("alias").getString(container.getId());
configLoader.save(rootNode);
return urlAlias;
}
Не понимаешь, что делает этот код? Тогда прочитай туториал про конфиги. Ссылка на туториал в самом верху этого туториала.
Далее нам необходимо получить JDBC url с помощью алиаса и создать таблицу плагина.
Получаем алиас:
String jdbcAlias = getJdbcAliasAndSaveDefaults(container, configLoader);
Далее нам необходимо создать таблицу плагина.
Нельзя нагромождать всё в один класс. Давайте создадим вспомогательный класс, который будет работать с БД.
Что он будет делать:
1. Получать соединение с базой по алиасу
2. Создавать таблицу
3. Записывать сообщения игроков в таблицу
Назовём этот класс StorageManager
.
Вернёмся к таблице. Мы будем хранить в ней:
1. UUID игрока
2. Ник
3. Время сообщения
4. Само сообщение
Напишем запрос для создания таблицы:
CREATE TABLE IF NOT EXISTS `sqlservice_chat_logs` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`uuid` CHAR(36) NOT NULL, # uuid игрока
`name` VARCHAR(16) NOT NULL, # ник игрока
`time` DATETIME NOT NULL, # время отправки сообщения
`message` VARCHAR(256) # само сообщение, 256 - максимальная длина сообщения в майнкрафте
);
Теперь напишем запрос для вставки строки в таблицу:
INSERT INTO `sqlservice_chat_logs` (`uuid`, `name`, `time`, `message`) VALUES (?, ?, ?, ?);
Отлично, 50% работы c StorageManage
сделано! Начнём писать StorageManager
.
В StorageManager
нам необходим конструктор, который принимает:
1. PluginContainer
2. SqlService
3. Алиас для jdbc url
С помощью этих объектов, он должен будет получить DataSource
- то, откуда будем брать соединение с базой(Connection
).
Создадим вспомогательный метод, который получает алиас, SqlService и возвращает jdbc url.
private static String getJdbcUrl(SqlService sqlService, String jdbcAlias) throws IllegalArgumentException {
return sqlService.getConnectionUrlFromAlias(jdbcAlias)
.orElseThrow(() -> new IllegalArgumentException(String.format("JDBC alias with name '%s' not found!", jdbcAlias)));
}
Что делает этот код? Вызывает метод getConnectionUrlFromAlias(String)
у SqlService
, передавая ему наш алиас и получает нужный нам jdbc url. Если jdbc url не найдет, то метод выбрасывает ошибку. (orElseThrow(...)
)
Вот код получившегося конструктора:
private StorageManager(PluginContainer container, SqlService sqlService, String jdbcAlias) throws SQLException {
Objects.requireNonNull(container, "container"); // проверка на null
Objects.requireNonNull(sqlService, "sqlService"); // проверка на null
this.jdbcAlias = Objects.requireNonNull(jdbcAlias, "jdbcAlias"); // необходимо для корректной обработки смены сервиса
String jdbcUrl = getJdbcUrl(sqlService, Objects.requireNonNull(jdbcAlias, "jdbcAlias")); // получаем jdbc url
this.dataSource = sqlService.getDataSource(container, jdbcUrl); // получаем DataSource
}
Напишем метод, который создаст таблицу плагина:
private void createTables() throws SQLException {
try(Connection connection = dataSource.getConnection();
PreparedStatement stmt = connection.prepareStatement(CREATE_TABLE)) {
stmt.execute();
}
}
Напишем метод, который будет записывать сообщение в таблицу:
void insertChatMessage(Text message, Player source) throws SQLException {
try(Connection connection = dataSource.getConnection();
PreparedStatement stmt = connection.prepareStatement(INSERT_MESSAGE)) {
stmt.setString(1, source.getUniqueId().toString());
stmt.setString(2, source.getName());
stmt.setObject(3, Instant.now());
stmt.setString(4, message.toPlain());
stmt.executeUpdate();
}
}
Еще напишем вспомогательный метод, который будет создавать наш StorageManager
, создавать таблицу и возвращать StorageManager
.
static StorageManager createStorageManagerAndTable(PluginContainer container, SqlService sqlService, String jdbcAlias) throws SQLException {
StorageManager storageManager = new StorageManager(container, sqlService, jdbcAlias);
storageManager.createTables(); // создаём таблицу
return storageManager;
}
Полный код нашего StorageManager
:
код
package com.spongeapi.tutorial.SqlServiceExample;
import org.spongepowered.api.entity.living.player.Player;
import org.spongepowered.api.plugin.PluginContainer;
import org.spongepowered.api.service.sql.SqlService;
import org.spongepowered.api.text.Text;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.time.Instant;
import java.util.Objects;
public final class StorageManager {
private static final String CREATE_TABLE =
"CREATE TABLE IF NOT EXISTS `sqlservice_chat_logs` (" +
" `id` BIGINT PRIMARY KEY AUTO_INCREMENT," +
" `uuid` CHAR(36) NOT NULL," +
" `name` VARCHAR(16) NOT NULL," +
" `time` DATETIME NOT NULL," +
" `message` VARCHAR(256)" +
");";
private static final String INSERT_MESSAGE = "INSERT INTO `sqlservice_chat_logs` (`uuid`, `name`, `time`, `message`) VALUES (?, ?, ?, ?);";
private final DataSource dataSource;
private final String jdbcAlias;
private StorageManager(PluginContainer container, SqlService sqlService, String jdbcAlias) throws SQLException {
Objects.requireNonNull(container, "container");
Objects.requireNonNull(sqlService, "sqlService");
this.jdbcAlias = Objects.requireNonNull(jdbcAlias, "jdbcAlias");
String jdbcUrl = getJdbcUrl(sqlService, Objects.requireNonNull(jdbcAlias, "jdbcAlias"));
this.dataSource = sqlService.getDataSource(container, jdbcUrl);
}
String getJdbcAlias() {
return jdbcAlias;
}
private void createTables() throws SQLException {
try(Connection connection = dataSource.getConnection();
PreparedStatement stmt = connection.prepareStatement(CREATE_TABLE)) {
stmt.execute();
}
}
void insertChatMessage(Text message, Player source) throws SQLException {
try(Connection connection = dataSource.getConnection();
PreparedStatement stmt = connection.prepareStatement(INSERT_MESSAGE)) {
stmt.setString(1, source.getUniqueId().toString());
stmt.setString(2, source.getName());
stmt.setObject(3, Instant.now());
stmt.setString(4, message.toPlain());
stmt.executeUpdate();
}
}
private static String getJdbcUrl(SqlService sqlService, String jdbcAlias) throws IllegalArgumentException {
return sqlService.getConnectionUrlFromAlias(jdbcAlias)
.orElseThrow(() -> new IllegalArgumentException(String.format("JDBC alias with name '%s' not found!", jdbcAlias)));
}
static StorageManager createStorageManagerAndTable(PluginContainer container, SqlService sqlService, String jdbcAlias) throws SQLException {
StorageManager storageManager = new StorageManager(container, sqlService, jdbcAlias);
storageManager.createTables();
return storageManager;
}
}
Теперь вернёмся к нашему главному классу плагина. Ищем слушаетля GamePreInitializationEvent
, которого мы недавно написали.
Что нам нужно дописать?
1. Получение SqlService
2. Получение алиаса
3. Созданеие StorageManager
'а
@Listener
public void onGamePreInit(GamePreInitializationEvent event) throws IOException, SQLException {
logger.info("Preparing database");
SqlService sqlService = Sponge.getServiceManager().provideUnchecked(SqlService.class); // получаем SqlService
String jdbcAlias = getJdbcAliasAndSaveDefaults(container, configLoader); // получаем алиас. Метод `getJdbcAliasAndSaveDefaults` мы создали в самом начале туториала
this.storageManager = StorageManager.createStorageManagerAndTable(container, sqlService, jdbcAlias); // если произойдёт ошибка
// во время создания StorageManager, то будет выброшено исключение SQLException и присвоения не будет(StorageManager будет null)
logger.info("Database prepared!"); // если будет ошибка в предыдущем вызове, то эта строка не выполнится
}
Уже готово 75% плагина! Осталось совсем немного, а если точнее:
1. Перезагрузка плагина
2. Поддержка смены сервиса SqlService
3. Запись сообщений в БД
Для реализации перезагрузки плагина, нам нужно слушать эвент GameReloadEvent
. Этот эвент вызывается при выполнении команды /sponge plugins reload
.
@Listener
public void onPluginReload(GameReloadEvent event) throws IOException, SQLException {
logger.info("Reloading configuration and database connections");
String jdbcAlias = getJdbcAliasAndSaveDefaults(container, configLoader); // получаем алиас
SqlService sqlService = Sponge.getServiceManager().provideUnchecked(SqlService.class); // получаем SqlService
this.storageManager = null; // обнуляем предыдущий StorageManager, чтобы не было багов, если новый StorageManager не создался
this.storageManager = StorageManager.createStorageManagerAndTable(container, sqlService, jdbcAlias); // если произойдёт ошибка
// во время создания StorageManager, то будет выброшено исключение SQLException и присвоения не будет(StorageManager будет null)
logger.info("Reloaded!"); // если будет ошибка в предыдущем вызове, то эта строка не выполнится
}
Теперь нужно реализовать поддержку смены SqlService
. Для этого нужно слушать эвент ChangeServiceProviderEvent
.
@Listener
public void onServiceChange(ChangeServiceProviderEvent event) throws IOException {
if (event.getService() == SqlService.class) { // проверяем, какой сервис изменяется
logger.info("Changing SqlService");
SqlService sqlService = (SqlService) event.getNewProvider(); // получаем новый SqlService
String jdbcAlias; // получаем алиас
if (storageManager != null) { // Если у нас уже есть рабочий StorageManager, то берём алиас из него
jdbcAlias = storageManager.getJdbcAlias(); // StorageManager будет равен нулю,
// если произошла какая-то ошибка во время подключения к базе или создания таблицы
} else { // иначе берём его из конфига
jdbcAlias = getJdbcAliasAndSaveDefaults(container, configLoader);
}
this.storageManager = null; // обнуляем старую ссылку
this.storageManager = StorageManager.createStorageManagerAndTable(container, sqlService, jdbcAlias); // если произойдёт ошибка
// во время создания StorageManager, то будет выброшено исключение SQLException и присвоения не будет(StorageManager будет null)
logger.info("Reloaded!"); // если будет ошибка в предыдущем вызове, то эта строка не выполнится
}
}
Осталось самое простое! При вызове эвента MessageChannelEvent.Chat
, записывать сообщение в БД!
@Listener
public void onPlayerChat(MessageChannelEvent.Chat event, @Root Player player) {
if (storageManager == null) { // если StorageManager null, то пропускаем запись, иначе будут ошибки в консоле
return;
}
Task.builder().async().execute(() -> { // записываем данные в БД асинхронно, если так не делать, то будет падение TPS
try {
storageManager.insertChatMessage(event.getRawMessage(), player); // записываем сообщение
} catch (SQLException e) {
logger.error("Error while saving chat message!", e); // если произошла ошибка, логгируем её.
}
}).submit(this);
}
Полный главный класс:
код
package com.spongeapi.tutorial.SqlServiceExample;
import ninja.leaping.configurate.ConfigurationNode;
import ninja.leaping.configurate.ConfigurationOptions;
import ninja.leaping.configurate.commented.CommentedConfigurationNode;
import ninja.leaping.configurate.loader.ConfigurationLoader;
import org.slf4j.Logger;
import org.spongepowered.api.Sponge;
import org.spongepowered.api.config.DefaultConfig;
import org.spongepowered.api.entity.living.player.Player;
import org.spongepowered.api.event.Listener;
import org.spongepowered.api.event.filter.cause.Root;
import org.spongepowered.api.event.game.GameReloadEvent;
import org.spongepowered.api.event.game.state.GamePreInitializationEvent;
import org.spongepowered.api.event.message.MessageChannelEvent;
import org.spongepowered.api.event.service.ChangeServiceProviderEvent;
import org.spongepowered.api.plugin.Plugin;
import org.spongepowered.api.plugin.PluginContainer;
import org.spongepowered.api.scheduler.Task;
import org.spongepowered.api.service.sql.SqlService;
import javax.inject.Inject;
import java.io.IOException;
import java.sql.SQLException;
@Plugin(
id = "sqlserviceexample",
name = "SqlServiceExample",
version = "0.1-SNAPSHOT",
description = "Краткое описание SqlService",
url = "https://spongeapi.com",
authors = "Xakep_SDK"
)
public class SqlServiceExample {
private final Logger logger;
private final PluginContainer container;
private final ConfigurationLoader<CommentedConfigurationNode> configLoader;
private StorageManager storageManager;
@Inject
public SqlServiceExample(Logger logger,
PluginContainer container,
@DefaultConfig(sharedRoot = true) ConfigurationLoader<CommentedConfigurationNode> configLoader) {
this.logger = logger;
this.container = container;
this.configLoader = configLoader;
this.storageManager = null;
}
@Listener
public void onGamePreInit(GamePreInitializationEvent event) throws IOException, SQLException {
logger.info("Preparing database");
SqlService sqlService = Sponge.getServiceManager().provideUnchecked(SqlService.class);
String jdbcAlias = getJdbcAliasAndSaveDefaults(container, configLoader);
this.storageManager = StorageManager.createStorageManagerAndTable(container, sqlService, jdbcAlias);
logger.info("Database prepared!");
}
@Listener
public void onServiceChange(ChangeServiceProviderEvent event) throws IOException, SQLException {
if (event.getService() == SqlService.class) {
logger.info("Changing SqlService");
SqlService sqlService = (SqlService) event.getNewProvider();
String jdbcAlias;
if (storageManager != null) {
jdbcAlias = storageManager.getJdbcAlias();
} else {
jdbcAlias = getJdbcAliasAndSaveDefaults(container, configLoader);
}
this.storageManager = null;
this.storageManager = StorageManager.createStorageManagerAndTable(container, sqlService, jdbcAlias);
logger.info("SqlService changed!");
}
}
@Listener
public void onPlayerChat(MessageChannelEvent.Chat event, @Root Player player) {
if (storageManager == null) {
return;
}
Task.builder().async().execute(() -> {
try {
storageManager.insertChatMessage(event.getRawMessage(), player);
} catch (SQLException e) {
logger.error("Error while saving chat message!", e);
}
}).submit(this);
}
@Listener
public void onPluginReload(GameReloadEvent event) throws IOException, SQLException {
logger.info("Reloading configuration and database connections");
String jdbcAlias = getJdbcAliasAndSaveDefaults(container, configLoader);
SqlService sqlService = Sponge.getServiceManager().provideUnchecked(SqlService.class);
this.storageManager = null;
this.storageManager = StorageManager.createStorageManagerAndTable(container, sqlService, jdbcAlias);
logger.info("Reloaded!");
}
private static String getJdbcAliasAndSaveDefaults(PluginContainer container, ConfigurationLoader<CommentedConfigurationNode> configLoader)
throws IOException {
ConfigurationNode rootNode = configLoader.load(ConfigurationOptions.defaults().setShouldCopyDefaults(true));
String urlAlias = rootNode.getNode("alias").getString(container.getId());
configLoader.save(rootNode);
return urlAlias;
}
}
Почти всё готово! Осталось установить плагин и добавить алиас.
Открываем /config/sponge/global.conf
Ищем там sponge.sql
:
В виде текста:
sponge {
# ...
sql {
sqlserviceexample="jdbc:mysql://root@localhost/sqlservice"
}
# ...
}
Правила записи алиаса:
алиас="jdbc url"
Правила записи jdbc url:
jdbc:protocol://[username[:password]@]host[:port]/database
Например:
jdbc:mysql://dbUser:dbPass@localhost:3306/database
Устанавливаем и проверяем плагин!
Название алиаса можно сменить в конфиге нашего плагина: /config/sqlserviceexample.conf
alias=sqlserviceexample # название алиаса из конфига губки
Исходники