Создаем plugin для IDEA для мониторинга транзакций в Spring

Disclaimer: я не являюсь сотрудником JetBrains (а жаль), поэтому код может являться не оптимальным и служит только для примера и исследовательских целей.

Введение

Часто во время работы со Spring непонятно, правильно ли работает аннотация @Transaction:

  • в правильном ли месте мы ее поставили

  • правильно ли объявился interceptor

  • и т.д.

Самым простым способом для меня было остановиться в debug в IDEA в необходимом методе и исследовать, что возвращает

TransactionSynchronizationManager.isActualTransactionActive();

Но «я же программист» и захотелось это дело хоть как-то автоматизировать, заодно поизучать возможности написания plugin для IDEA.

Хочется иметь семафор, который визуально показывает, активна ли транзакция или нет, а также иметь возможность сделать срез основных свойств для анализа в будущем.

Примерно вот что получилось:

936d2028ae0f64bba89526c8e0341562.gif

Подробней про транзакции можно прочитать здесь.

С чего начать создания plugin?

Про настройку проекта для создание plugin можно прочитать здесь.

Повторюсь, что главными отправными точками будут:

Реализация

Исходный код получившегося решения размещен здесь.

Создание action

Прочитать про action можно здесь. Под action, в основном, понимается кнопка или элемент меню.

Сначала определим новый action. У action есть два метода update и actionPerformed.

  • update — вызывается idea несколько раз в секунду для того, чтобы установить корректное состояние (включен/не включен, виден/не виден)

  • actionPerformed — вызывается idea при выполнении действия (нажатия на кнопку).

public class PopupDialogAction extends AnAction {

  @Override
  public void update(AnActionEvent e) {
    // Using the event, evaluate the context, and enable or disable the action.
  }

  @Override
  public void actionPerformed(@NotNull AnActionEvent e) {
    // Using the event, implement an action. For example, create and show a dialog.
  }
}

Для того, чтобы зарегистрировать новый action, требуется прописать в plugin.xml:


  
    
  

После этого должна была появиться новая кнопка на панели инструментов в debug-окне

9d04b78aaf7e1245d8f601ebe3500e2f.png

Вычисление значений в debug

IDEA позволяет нам вычислять выражения при debug и взаимодействовать с памятью

c2356365a50dc9e935bbe101fb601c91.pngИ не только

Информация взята из twitter Тагира Валеева

В Evaluate даже встроен свой мини-интерпретатор Java, который позволяет выполнять прикольные вещи, например, такие:

Картинка из поста Тагира Валеева (https://twitter.com/tagir_valeev/status/1360512527218728962)Картинка из поста Тагира Валеева (https://twitter.com/tagir_valeev/status/1360512527218728962)

Поэтому, используя API IDEA, мы легко сможем узнать, когда транзакция активна, выполнив:

const val TRANSACTION_ACTIVE: String =
    "org.springframework.transaction.support.TransactionSynchronizationManager"+
  	".actualTransactionActive.get()==Boolean.TRUE"

Для начала сделаем так, чтобы наше действие было недоступно, если мы не находимся в режиме debug. Для этого получим XDebugSession и сравним с null

override fun update(e: AnActionEvent) {
  val presentation = e.presentation
  val currentSession: XDebugSession? = getCurrentSession(e)
  if (currentSession == null) {
    setDisabled(presentation)
    return
  }
}

private fun getCurrentSession(e: AnActionEvent): XDebugSession? {
  val project = e.project
  return if (project == null) null else 
  XDebuggerManager.getInstance(project).currentSession
}

Многие вещи в idea реализованы через статические методы и паттерн singleton (хотя это уже почти считается антипаттерном — это очень удобно, что мы можем получить требуемые значения из любого места через статические методы, например, XDebuggerManager.getInstance)

Евгений Борисов не одобряет singleton как pattern, когда есть SpringЕвгений Борисов не одобряет singleton как pattern, когда есть Spring

Теперь мы хотим получить значения из контекста Spring из текущей сессии Java. Для этого можно воспользоваться следующим методом:

public abstract class XDebuggerEvaluator {

  public abstract void evaluate(@NotNull String expression, 
                                @NotNull XEvaluationCallback callback, 
                                @Nullable XSourcePosition expressionPosition);
}

Например, так

val currentSourcePosition: XSourcePosition? = currentSession.currentStackFrame?.
													sourcePosition

currentSession.debugProcess.evaluator?.evaluate(
  TRANSACTION_ACTIVE, object : XDebuggerEvaluator.XEvaluationCallback {
    override fun errorOccurred(errorMessage: String) {
      TODO("Not yet implemented")
    }

    override fun evaluated(result: XValue) {
      TODO("Not yet implemented")
    }
  },
  currentSourcePosition
)

В XValue теперь хранится вычисленное значение. Чтобы посмотреть его, можно выполнить:

(result as JavaValue).descriptor.value

Он возвращает объект класса — com.sun.jdi.Value

Часть JavaDoc для com.sun.jdi.Value

The mirror for a value in the target VM. This interface is the root of a value hierarchy encompassing primitive values and object values.

Мы научились вычислять значения в debug с использованием API IDEA.

Рабочая панель (Tool Window)

Теперь попробуем их вывести в рабочую панель (Tool Window). Как всегда начинаем с документации и примера.

Объявляем в plugin.xml новое окно


public class MyToolWindowFactory implements ToolWindowFactory {

  public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindow toolWindow) {
    MyToolWindow myToolWindow = new MyToolWindow(toolWindow, project);
    ContentFactory contentFactory = ContentFactory.SERVICE.getInstance();
    Content content = contentFactory.createContent(myToolWindow.getContent(), "", false);

    final ContentManager contentManager = toolWindow.getContentManager();
    contentManager.addContent(content);
  }
}

Само окно можно нарисовать во встроенном редакторе

52c693c08d55af14cf534fd2ff46d6aa.png

IDEA сама сгенерирует для нас заготовку класса:

7071f9e286f8b305fe1dba864ca9e578.png

А дальше, вспоминая старый добрый Swing, описываем логику и добавляем необходимые Listener.

Получившееся окно инструментов (Tool Windows)Получившееся окно инструментов (Tool Windows)

Способы передачи данных

Вернемся к нашему action. При нажатии на кнопку вызывается метод actionPerformed.

Как из этого метода достучаться до нашего окна?

Самый простой способ — снова воспользоваться статическим методом:

val toolWindow: ToolWindow? =
ToolWindowManager.getInstance(project).getToolWindow("TransactionView")

И передать туда требуемые значения.

IDEA предоставляет еще один способ — Message Bus (детальное описание лучше смотреть в документации). Один из вариантов использования следующий:

Объявить интерфейс:

public interface ChangeActionNotifier {

    Topic CHANGE_ACTION_TOPIC = Topic.create("custom name", ChangeActionNotifier.class)

    void beforeAction(Context context);
    void afterAction(Context context);
}

В месте, где принимаем сообщения:

bus.connect().subscribe(ActionTopics.CHANGE_ACTION_TOPIC, 
new ChangeActionNotifier() {
        @Override
        public void beforeAction(Context context) {
            // Process 'before action' event.
        }
        @Override
        public void afterAction(Context context) {
            // Process 'after action' event.
        }
});

В месте, где отправляем сообщения:

ChangeActionNotifier publisher = myBus.syncPublisher(
  ActionTopics.CHANGE_ACTION_TOPIC);
publisher.beforeAction(context);

В любом случае необходимо быть аккуратным с многопоточностью и не выполнять долгие операции на UI Thread (подробности).

Осталось собрать все вместе и протестировать.

Исходный код получившегося решения размещен здесь.

Краткий «как бы» вывод

IDEA очень гибкий и удобный инструмент для разработки, который можно быстро расширить требуемыми функциями. IDEA содержит массу интересного внутри себя, тем более ее код доступен всем.

Ссылки

© Habrahabr.ru