Для пользователей explain.tensor.ru — нашего сервиса визуализации PostgreSQL-планов, мы создали плагин «Explain PostgreSQL» для всех IDE от JetBrains, теперь есть возможность форматировать запросы и анализировать планы непосредственно в IDE.
Как использовать плагин и детали о его разработке читайте ниже.
Установка плагина
К сожалению, попытка опубликовать плагин в JetBrains Marketplace завершилась отказом:
На основании имеющейся у нас информации, Вы являетесь лицом, связанным с организацией, находящейся в Российской Федерации, и/или с Правительством Российской Федерации.
По этой причине, а также в связи с недавно опубликованной поправкой к санкционному законодательству ЕС (8-й пакет санкций ЕС), JetBrains не может предоставлять Вам услуги ИТ‑консалтинга, включая техническую поддержку или помощь. Проверки и публикация плагинов на Маркетплейсе JetBrains попадают под определение предоставления технической поддержки.
Поэтому установку плагина надо делать из локального файла — скачиваем его с сайта explain.tensor.ru (раздел Download / Plugins ) и в Settings > Plugins выбираем Install plugin from Disk .
Подключение к БД
создаем новый datasource:
скачиваем JDBC-драйвера PostgreSQL и подключаемся к БД:
Форматирование запроса
в консоли набираем текст запроса и нажимаем Ctrl-Q F или в контекстном меню SQL Format:
Анализ запроса
В контекстном меню выбираем Explain Plan > Explain Analyze (Tensor) , при этом выполнится запрос EXPLAIN (ANALYZE, BUFFERS)
полученный план отправится в сервис explain.tensor.ru через публичное API , результат откроется в новом окне:
Для IDE без плагина Database Tools
В WebStorm для работы с БД требуется установка плагина Database Tools and SQL for WebStorm . Если этот плагин не установлен или у вас community-версия IDE то открыть сайт explain.tensor.ru можно прямо в IDE:
Настройка плагина
Для тех кто развернул сервис локально, используя вариант self hosted, или использует сайт explain-postgresql.com , можно поменять сайт в настройках IDE в Tools > Explain PostgreSQL:
О разработке плагина для JetBrains IDE
Создаем новый проект «IDE plugin».
В окне есть ссылка на официальную документацию IntelliJ Platform SDK , в ней приведены основные сведения, используемые при разработке.
Создание Tool Window
-
создаем класс, реализующий интерфейс ToolWindowFactory и переопределяем метод createToolWindowContent , который будет вызываться при нажатии на кнопку окна — здесь в окне создается новая вкладка с сайтом explain.tensor.ru и кнопка новой вкладки:
public class ExplainToolWindowFactory implements ToolWindowFactory, DumbAware { @Override public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindow toolWindow) { createNewTab(project); ButtonNewTab newTabBtn = new ButtonNewTab("New Tab", null, AllIcons.Toolbar.AddSlot); ((ToolWindowEx) toolWindow).setTabActions(newTabBtn); } public static void createNewTab(Project project) { ApplicationManager.getApplication().invokeLater((new Runnable() { @Override public void run() { ExplainBrowser browser = new ExplainBrowser(true); ToolWindow toolWindow = ToolWindowManager.getInstance(project).getToolWindow(explainWindow); Content content = ContentFactory.getInstance().createContent(browser.getComponent(), "Explain", false); content.setDisposer(browser); @NotNull ContentManager contentManager = toolWindow.getContentManager(); contentManager.addContent(content); contentManager.setSelectedContent(content); } })); } }
-
кнопка создания новой вкладки — наследуем AnActionButton и переопределяем методы:
actionPerformed — выполняется при нажатии на кнопку, здесь мы создаем новую вкладку или открываем новое окно браузера если нажали вместе с Ctrl
updateButton — выполняется для определения доступности кнопки, в нашем случае если JDK с поддержкой JCEF браузера и открыто tool window
public class ButtonNewTab extends AnActionButton { public ButtonNewTab(String text, String descr, Icon icon) { super(text, descr, icon); } @Override public void actionPerformed(@NotNull AnActionEvent e) { if (e.getInputEvent().isControlDown()) { BrowserUtil.browse(AppSettingsState.getInstance().getExplainUrl()); } else { ExplainToolWindowFactory.createNewTab(e.getProject()); } } @Override public void updateButton(@NotNull AnActionEvent e) { super.updateButton(e); boolean enabled = ExplainBrowser.isSupported && ExplainToolWindowFactory.getToolWindow(e.getProject()).isVisible(); e.getPresentation().setEnabled(enabled); } }
-
окно с JCEF-браузером
JCEF — это реализация Chromium Embedded Framework в Java, определяем возможность его использования методом isSupported и если нет, то вместо компонента с окном браузера выдаем сообщение:
public class ExplainBrowser implements Disposable { public static final String EXPLAIN_URL = "https://explain.tensor.ru"; public static final boolean isSupported = JBCefApp.isSupported(); private JBCefBrowser browser = null; private JPanel panel = null; public ExplainBrowser(boolean loadOnStart) { if (isSupported) { browser = new JBCefBrowser(EXPLAIN_URL); } else { panel = new JPanel(new BorderLayout()); JLabel label = new JLabel("JCEF browser is not supported"); panel.add(label, BorderLayout.CENTER); } } public JComponent getComponent() { if (browser == null) { return panel; } else { return browser.getComponent(); } } }
-
добавляем описание tool window в plugin.xml в разделе extensions, в параметре factoryClass указываем созданный в п.1 класс. IDE при старте прочитает plugin.xml и создаст наше окно снизу на панели инструментов
<extensions defaultExtensionNs="com.intellij"> <toolWindow factoryClass="com.mgorkov.toolwindow.ExplainToolWindowFactory" id="Explain PostgreSQL" anchor="bottom" secondary="true" canCloseContents="true" largeStripeAnchor="true" icon="AllIcons.Xml.Browsers.Chrome" doNotActivateOnStart="true" /> </extensions>
Форматирование запроса
Создаем класс SQLFormatter и наследуем от AnAction , в нем также как и в AnActionButton надо переопределить методы actionPerformed и update.
В actionPerformed мы получаем текст документа и, используя API explain.tensor.ru , форматируем и заменяем исходный текст.
public class SQLFormatter extends AnAction { private Document document = null; private String formatted = null; @Override public void actionPerformed(@NotNull AnActionEvent e) { Editor editor = e.getData(CommonDataKeys.EDITOR); if (editor != null) { document = editor.getDocument(); String text = document.getText(); formatted = explainApi.beautifier(e, text).join(); if (formatted != null) { WriteCommandAction.runWriteCommandAction(e.getProject(), () -> { document.deleteString(0, document.getTextLength()); document.insertString(0, formatted); }); } } } }
Чтобы IDE узнала об этом действии нужно зарегистрировать его в plugin.xml в разделе actions , указав реализующий класс, место размещения и комбинацию клавиш. Здесь мы размещаем действие в EditorPopupMenu и оно доступно при нажатии Ctrl Q F
<actions> <action id="SQLFormat" class="com.mgorkov.actions.SQLFormatter" text="SQL Format"> <add-to-group group-id="EditorPopupMenu" anchor="last"/> <keyboard-shortcut keymap="$default" first-keystroke="ctrl Q" second-keystroke="F"/> </action> </actions>
При наличии синтаксических ошибок выдается предупреждение:
Это реализовано в методе showErrorMessage сервиса ExplainApiService:
private void showErrorMessage(@NotNull AnActionEvent e, String msg) { ApplicationManager.getApplication().invokeLater(() -> { HintManager.getInstance().showErrorHint(Objects.requireNonNull(e.getData(CommonDataKeys.EDITOR)), msg); }); }
Анализ запроса
Для выполнение запросов к базе используем плагин JetBrains Database Tools, он встроен во все IDE , кроме community edition и WebStorm (можно установить отдельно).
Здесь мы наследуем класс ExplainActionBase.Ui и в нем переопределяем метод explainStatement , в котором выполняем запрос и выводим окно браузера с результатом анализа в новой вкладке редактора:
FileEditor[] editors = FileEditorManager.getInstance(console.getProject()).openFile(file, true); Arrays.stream(editors).filter(e -> e instanceof PgPlanEditor).forEach(e -> { PgPlanEditor editor = (PgPlanEditor) e; editor.getExplainWindow().updatePlan(mdl.getJson()); });
Далее регистрируем новые действия в контекстном меню JDBC-консоли. Чтобы плагин мог работать в IDE без Database Tools добавим в plugin.xml опциональную зависимость от него:
<depends optional="true" config-file="plugin-withdatabase.xml">com.intellij.database</depends>
и создадим plugin-withdatabase.xml :
<actions> <action id="com.mgorkov.actions.ExplainAnalyzeAction" class="com.mgorkov.actions.ExplainAnalyzeAction" text="Explain Analyze (Tensor)" icon="DatabaseIcons.ConsoleShowPlan"> <add-to-group group-id="Console.Jdbc.ExplainGroup" anchor="last"/> </action> <action id="com.mgorkov.actions.ExplainAction" class="com.mgorkov.actions.ExplainAction" text="Explain Plan (Tensor)" icon="DatabaseIcons.ConsoleShowPlan"> <add-to-group group-id="Console.Jdbc.ExplainGroup" anchor="last"/> </action> </actions>
Настройка плагина
В IDE есть понятие сервисов, это компоненты плагина, которые загружаются по требованию при вызове getService() . При этом сервисы реализуют паттерн singleton, т.е. при вызове getService() всегда возвращается один и тот же компонент. Хранение настроек плагина реализуется сервисом типа applicationService (всего их три).
Реализация настроек выполнена по схеме MVC.
Роль модели выполняет класс AppSettingsState, реализующий PersistentStateComponent, при этом в аннотации надо указать имя параметра и файла, в котором сохраняются настройки:
@State( name = "com.mgorkov.settings.AppSettingsState", storages = @Storage("ExplainPostgreSQLSettingsPlugin.xml") ) public final class AppSettingsState implements PersistentStateComponent<AppSettingsState> { private String ExplainUrl = "https://explain.tensor.ru"; public static AppSettingsState getInstance() { return ApplicationManager.getApplication().getService(AppSettingsState.class); } public void setExplainUrl(String explainUrl) { ExplainUrl = explainUrl; } }
Роль контроллера — класс AppSettingsConfigurable реализующий интерфейс Configurable , в котором переопределены методы:
getDisplayName — выдает название пункта меню Settings, используется при поиске
public class AppSettingsConfigurable implements Configurable { private AppSettingsComponent appSettingsComponent; @Override public @NlsContexts.ConfigurableName String getDisplayName() { return "Explain PostgreSQL"; } @Override public @Nullable JComponent createComponent() { appSettingsComponent = new AppSettingsComponent(); return appSettingsComponent.getPanel(); } @Override public void apply() throws ConfigurationException { AppSettingsState settings = AppSettingsState.getInstance(); String explainUrl = appSettingsComponent.getExplainUrl(); try { new URL(explainUrl).openStream().close(); settings.setExplainUrl(explainUrl); } catch (Exception e) { throw new ConfigurationException(e.getMessage()); } } }
apply — выполняет проверку на валидность URL и возможность подключиться к нему, в случае ошибок бросает исключение ConfigurationException , текст из которого выводится снизу на странице настроек.
createComponent — создает страницу настроек, о ней ниже.
Роль представления реализует класс AppSettingsComponent , в котором создается форма с текстовым полем и меткой:
public class AppSettingsComponent { private final JPanel panel; private final JBTextField explainUrlTextField = new JBTextField(); public AppSettingsComponent() { panel = FormBuilder.createFormBuilder() .addLabeledComponent(new JBLabel("Explain PostgreSQL site: "), explainUrlTextField, 1, false) .addComponentFillVertically(new JPanel(), 0) .getPanel(); } public JPanel getPanel() { return panel; } public String getExplainUrl() { return explainUrlTextField.getText(); } public void setExplainUrl(@NotNull String url) { explainUrlTextField.setText(url); } }
Регистрируем модель и контроллер в plugin.xml , для контроллера необходимо указать пункт меню Settings, в котором будут наши настройки, т.е. tools:
<extensions defaultExtensionNs="com.intellij"> <applicationConfigurable parentId="tools" instance="com.mgorkov.settings.AppSettingsConfigurable" id="com.mgorkov.settings.AppSettingsConfigurable" displayName="Explain PostgreSQL" /> <applicationService serviceImplementation="com.mgorkov.settings.AppSettingsState"/> </extensions>
Отладка плагина
Для вывода сообщений в лог используем Logger :
private static final Logger log = Logger.getInstance(ExplainApiService.class); log.debug("POST JSON: " + jsonObject);
Лог хранится в файле idea.log , открыть который можно через меню Help -> Open Log in Editor
По умолчанию в лог пишутся все события уровня INFO.
Чтобы записать события с уровнем DEBUG надо добавить список имен в Help -> Diagnostic Tools -> Debug Log Settings :
Для включения внутренних команд в IDE надо добавить опцию idea.is.internal=true в Help -> Edit Custom Properties
После перезапуска появится дополнительный пункт меню Tools -> Internal Actions
Теперь при одновременном нажатии Ctrl-Alt-A и щелчке мыши на любом элементе можно посмотреть его состав, как в DevTools браузера
Сборка и публикация плагина
Сборка осуществляется Gradle, при скачивании зависимостей в защищенной корпоративной среде могут быть ошибки типа «not found», для решения надо добавить сертификат в хранилище, используя стандартный пароль «changeit», и перезапустить IDE:
keytool -import -trustcacerts -alias root -file <файл с сертификатом> -keystore <путь к IDE>/jbr/lib/security/cacerts
Конфигурация сборки задается в файле build.gradle.kts , при необходимости можно поменять параметры по умолчанию. Например для варианта сборки с библиотекой json версии 20231013 и тестового запуска плагина в IDE IntelliJ IDEA Ultimate Edition версии 2022.2.5 :
dependencies { implementation("org.json:json:20231013") } intellij { version.set("2022.2.5") type.set("IU") // Target IDE Platform plugins.set(listOf("com.intellij.database")) }
Описание всех опций сборки можно почитать здесь.
Gradle создает набор задач, для которых можно поменять конфигурацию запуска, например чтобы при тестовом запуске IDE лог-файл idea.log отображался в консоли в отдельной вкладке надо поменять конфигурацию задачи runIde и добавить idea.log , путь к которому можно посмотреть в Help -> Show Log in Files:
Для подписи плагина потребуется поменять конфигурацию задачи signPlugin и добавить несколько переменных окружения:
Эти переменные используются при сборке и уже прописаны в build.gradle.kts :
signPlugin { certificateChain.set(System.getenv("CERTIFICATE_CHAIN")) privateKey.set(System.getenv("PRIVATE_KEY")) password.set(System.getenv("PRIVATE_KEY_PASSWORD")) }
Процесс создания подписей есть в документации, но там нет описания для получения значений в виде одной строки, а это можно сделать так:
# строка для переменной PRIVATE_KEY openssl genpkey -aes-256-cbc -algorithm RSA -pkeyopt rsa_keygen_bits:4096 | openssl rsa | tee private.pem | openssl base64 | tr -d '\n' # строка для CERTIFICATE_CHAIN openssl req -key private.pem -new -x509 -days 365 | openssl base64 | tr -d '\n'
Для проверки совместимости плагина с разными версиями версиями IDE можно использовать задачу verifyPlugin и добавить параметры для нее в конфиг сборки (build.gradle.kts) , например мы хотим проверить плагин в DataGrip версий 2023.3 и 3.1 :
runPluginVerifier { ideVersions.set(listOf( "DG-2023.3.1", "DG-2023.3" )) }
По умолчанию верификация плагина происходит на сайте plugins.jetbrains.com при загрузке новой версии плагина, на каких версиях производится проверка задается в build.gradle.kts :
patchPluginXml { sinceBuild.set("222") untilBuild.set("233.*") }
Можно также запустить проверку на сайте для конкретной версии, например для версий 221.* плагин не совместим из-за отсутствия методов:
Для публикации плагина первичная загрузка производится вручную, собранный файл находится в build/distributions (его кстати можно установить прямо с диска, для этого зайти в Settings -> Plugins -> Install Plugin from Disk)
В дальнейшем можно автоматизировать публикацию, для этого на сайте plugins.jetbrains.com в своем профиле во вкладке Tokens сгенерить токен и прописать его в переменной PUBLISH_TOKEN в конфигурации задачи publishToken.
В статье написал обо всех этапах создания плагина, пропустив детали реализации, не относящиеся непосредственно к теме разработки плагинов в IDE JetBrains.
Код плагина опубликован в github под лицензией MIT.
Дополнение: в комментариях к статье обнаружилась непонятная проблема с внешней библиотекой json на M1 MacOS 14 Sonoma — ошибка NoClassDefFoundError при создании
new JSONObject();
Проблему решили заменой библиотеки на Gson , которая входит в состав библиотек во всех IDE JetBrains.
ссылка на оригинал статьи https://habr.com/ru/articles/782150/
Добавить комментарий