Skip to content

Разработка бэкенда

Что даёт вам эта страница: карту Kotlin-половины вашего дополнения — входного класса, с которого стартует Pano, его хуков жизненного цикла и того, как Pano находит и строит ваши классы за вас, — плюс указатель на посвящённую страницу для каждой возможности бэкенда (эндпоинты, база данных, конфигурация, события, разрешения).

Бэкенд — это Kotlin-половина вашего дополнения: часть, которая выполняется внутри собственного Java-процесса Pano. Ей принадлежат ваши таблицы базы данных, ваши JSON-эндпоинты, ваши разрешения и ваши журналы админской активности. Эти страницы строят бэкенд-часть Shoutbox — небольшого дополнения, которое мы проносим через документацию, где посетители видят последние «выкрики» на главной странице, а админы публикуют и удаляют их из панели.

Дополнения в коде — это плагины

Везде в тексте мы говорим дополнение, но имена уровня кода используют слово pluginPanoPlugin, pluginId, PluginConfig и так далее. Так и должно быть; ничего не переименовывайте в коде.

Прежде чем начать

У вас уже должно быть переименованное, собирающееся дополнение из Начала работы — та страница также поднимает работающий экземпляр Pano, внутри которого живёт ваше дополнение. Пожалуйста, сначала прочтите Архитектуру, если ещё не сделали этого; весь этот раздел опирается на неё. Одна идея, которую нужно оттуда вынести, простыми словами:

Spring — это библиотека, которую Pano использует для создания ваших классов за вас, так что вы никогда не пишете new. Контекст — это просто коробка готовых объектов, которые Spring заполняет. Pano даёт вашему дополнению собственную коробку и кладёт по одной копии каждого из ваших классов в неё — вы затем просите у коробки то, что вам нужно.

Бэкенд живёт под src/main/kotlin/com/panomc/plugins/shoutbox/. Вот для чего каждая часть, простыми словами:

  • ShoutboxPlugin.kt — входной класс, с которого стартует Pano (эта страница).
  • config/ — файл настроек, который может редактировать владелец сайта.
  • db/ — всё о вашей таблице базы данных, разделённое на model/ (одна строка как объект Kotlin), dao/ (список запросов, которые вы обещаете предоставить), impl/ (реальный SQL, который держит это обещание) и migration/ (шаги, меняющие форму таблицы в более поздней версии).
  • routes/ — ваши URL, разделённые на api/ (публичные) и panel/ (только для админов).
  • permission/ — разрешение, которое ограждает админскую возможность.
  • event/ — код, реагирующий на действия платформы (например, завершение настройки).
  • log/ — запись, вносимая в ленту админской активности.

Вы создаёте эти файлы по одному; каждая посвящённая страница ниже называет файл, который она строит — не создавайте их все сразу.

Как Pano строит ваши классы за вас

Вы никогда не связываете эти классы вручную — никаких new, никаких вызовов «зарегистрируй это». Весь фокус в четырёх простых идеях:

  • Аннотация — это метка, начинающаяся с @ и сидящая прямо над классом, вроде @Endpoint. Это не комментарий — компилятор и Pano оба читают её.
  • Сканирование: когда ваше дополнение загружается, Pano просматривает ваш пакет и находит каждый класс, носящий одну из этих меток — @Endpoint, @Dao, @Migration, @EventListener или @PermissionDefinition.
  • Для каждого найденного Pano создаёт один экземпляр (один объект) и хранит его. Созданный Pano и хранимый Pano объект вроде этого называется bean — вот и всё, что «bean» означает где-либо в этой документации: объект, который Spring создал за вас.
  • Внедрение через конструктор: если один из ваших классов запрашивает другой из ваших bean-ов в своём конструкторе — class GetShoutsAPI(private val shoutDao: ShoutDao) — Pano передаёт вам готовый. Представьте это как службу доставки: вы перечисляете ингредиенты в бланке заказа (параметры конструктора), и они прибывают к вашей двери — вы никогда не вызываете конструктор сами.

Ещё одна вещь, которая избавляет вас от самого частого краха: есть две коробки.

  • Коробка Pano (контекст хоста) содержит собственные сервисы Pano: DatabaseManager, AuthProvider, SetupManager, PluginDatabaseManager.
  • Ваша коробка (контекст плагина) содержит написанные вами классы: ваши эндпоинты, DAO, слушатели.

Внедрение через конструктор дотягивается только до вашей коробки. Чтобы взять что-то из коробки Pano, вы запрашиваете это вручную: applicationContext.getBean(SomeService::class.java). Вы увидите это почти на каждой странице.

Изменения Kotlin никогда не горячие — пересоберите и перезапустите

Правка .kt-файла сама по себе ничего не меняет. Каждый раз, когда вы трогаете Kotlin, вы должны пересобрать jar, скопировать его в папку plugins/ вашего экземпляра и перезапустить Pano:

bash
./gradlew build -Pnoui
cp build/libs/pano-plugin-shoutbox-local-build.jar <your-pano-instance>/plugins/

-Pnoui пропускает пересборку Svelte UI, которая вам не нужна при работе над Kotlin — это делает сборку намного быстрее.

Отключение и повторное включение дополнения из Панель → Дополнения недостаточно: Pano не может подменить уже работающий Java-код, так что только полный перезапуск загружает новый jar. (Техническая причина: загрузчик плагинов PF4J в Pano держит уже загруженный classloader, а работающая JVM не может заменить его на месте.) Svelte UI вашего дополнения горячо перезагружается под bun run dev — но Kotlin никогда. Держите этот шаг пересборки-и-перезапуска в голове для каждой страницы ниже.

Входной класс

У каждого дополнения есть один главный класс, расширяющий PanoPlugin. Наш — ShoutboxPlugin (файл ShoutboxPlugin.kt), и при запуске он делает ровно одну работу: инициализирует конфигурацию и базу данных — но только после того, как завершился собственный мастер настройки Pano.

kotlin
package com.panomc.plugins.shoutbox

import com.panomc.platform.api.PanoPlugin
import com.panomc.platform.api.PluginDatabaseManager
import com.panomc.platform.api.config.PluginConfigManager
import com.panomc.platform.setup.SetupManager
import com.panomc.plugins.shoutbox.config.ShoutboxConfig

class ShoutboxPlugin : PanoPlugin() {
    private val pluginDatabaseManager by lazy { applicationContext.getBean(PluginDatabaseManager::class.java) }
    private val setupManager by lazy { applicationContext.getBean(SetupManager::class.java) }
    private var isInitialized = false

    override suspend fun onStart() {
        startPlugin()
    }

    internal suspend fun startPlugin() {
        if (isInitialized || !setupManager.isSetupDone()) return

        val configManager = PluginConfigManager(this, ShoutboxConfig::class.java)
        pluginBeanContext.beanFactory.registerSingleton(PluginConfigManager::class.java.name, configManager)

        pluginDatabaseManager.initialize(this)
        isInitialized = true
    }

    override suspend fun onDisable() {
        isInitialized = false
    }

    override suspend fun onUninstall() {
        pluginDatabaseManager.uninstall(this)
    }
}

Три кусочка синтаксиса Kotlin, которые вы увидите на всех этих страницах:

  • suspend помечает функцию, которой разрешено ждать — базу данных, сеть — не замораживая весь сервер. Большинство функций, которые вы переопределяете, объявлены suspend, так что оставляйте это, даже если вы сами никогда не пишете код с корутинами. (Единственное исключение, которое вы встретите, — getValidationHandler, который базовый класс объявляет без suspend — всегда точно повторяйте сигнатуру переопределяемой функции.)
  • by lazy { ... } означает «не выполнять это, пока оно реально не понадобится в первый раз».
  • getBean(X::class.java) означает «дай мне готовый объект X от Pano» — он дотягивается до коробки Pano (контекста хоста) сверху. PluginDatabaseManager и SetupManager нельзя внедрить в ваши конструкторы, так что вы извлекаете их так.

Что делает класс, сверху вниз:

  • onStart() выполняется при загрузке дополнения и вызывает startPlugin().
  • PluginConfigManager создаётся один раз и регистрируется как bean в вашей собственной коробке (pluginBeanContext). Никогда не берите PluginConfigManager как параметр конструктора в эндпоинте — его ещё не существует в момент, когда строятся ваши эндпоинты, так что внедрение упадёт. Конфигурация объясняет, почему именно, и показывает безопасный способ чтения конфигурации.
  • pluginDatabaseManager.initialize(this) создаёт ваши таблицы и выполняет любые ожидающие миграции.

Зачем ранний возврат? Если кто-то устанавливает ваше дополнение до того, как завершил мастер первоначальной настройки Pano, базы данных ещё нет — initialize() упал бы. Поэтому startPlugin() возвращается рано, а небольшой слушатель событий повторно запускает его в момент завершения настройки. Этот слушатель-страж настройки, и всё остальное о реагировании на действия платформы, живёт в разделе События.

PluginDatabaseManager против DatabaseManager

Два разных bean-а, оба извлекаются через getBean:

  • PluginDatabaseManager управляет вашими таблицами и миграциями — initialize(plugin) и uninstall(plugin).
  • DatabaseManager — сервис базы данных хоста. Используйте его для общего SQL-клиента (databaseManager.getSqlClient()) и для доступа к встроенным таблицам самого Pano — пользователи, посты, журналы активности, … — через их core DAO (DAO — небольшой класс, единственная задача которого — общение с одной таблицей базы данных), которые вы через него и читаете, и пишете. Работа с собственными таблицами Pano таким образом — это именно то, что делает pano-plugin-bans — посмотрите там этот паттерн.

Контрольная точка: загрузилось ли?

После пересборки и перезапуска следите за консолью Pano — она должна залогировать загрузку вашего дополнения — и откройте Панель → Дополнения: Shoutbox должен быть в списке. Если имя jar в строке cp выше не совпадает с тем, что вы реально собрали, загляните в build/libs/ — имя берётся из вашего pluginId (который вы задали ещё в Начале работы).

Соберите остальное, по одной части за раз

Каждая возможность — это своя посвящённая страница. Обращайтесь к той, что соответствует тому, что вы добавляете:

Shoutbox использует лишь часть поверхности бэкенда. Доступно больше — межаддонные события, подписанные токены и шаблонные письма (pano-plugin-auth-guard), уведомления в панели и для пользователей, коммуникация с Minecraft-сервером, консольные команды и загрузка файлов. Каждая точка расширения каталогизирована в Справочнике Backend API.

Куда дальше