Создаем plugin для IDEA для мониторинга транзакций в Spring
Disclaimer: я не являюсь сотрудником JetBrains (а жаль), поэтому код может являться не оптимальным и служит только для примера и исследовательских целей.
Введение
Часто во время работы со Spring непонятно, правильно ли работает аннотация @Transaction:
в правильном ли месте мы ее поставили
правильно ли объявился interceptor
и т.д.
Самым простым способом для меня было остановиться в debug в IDEA в необходимом методе и исследовать, что возвращает
TransactionSynchronizationManager.isActualTransactionActive();
Но «я же программист» и захотелось это дело хоть как-то автоматизировать, заодно поизучать возможности написания plugin для IDEA.
Хочется иметь семафор, который визуально показывает, активна ли транзакция или нет, а также иметь возможность сделать срез основных свойств для анализа в будущем.
Примерно вот что получилось:
Подробней про транзакции можно прочитать здесь.
С чего начать создания 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-окне
Вычисление значений в debug
IDEA позволяет нам вычислять выражения при debug и взаимодействовать с памятью
И не толькоИнформация взята из twitter Тагира Валеева
В Evaluate даже встроен свой мини-интерпретатор Java, который позволяет выполнять прикольные вещи, например, такие:
Картинка из поста Тагира Валеева (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
)
Теперь мы хотим получить значения из контекста 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.ValueThe 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);
}
}
Само окно можно нарисовать во встроенном редакторе
IDEA сама сгенерирует для нас заготовку класса:
А дальше, вспоминая старый добрый Swing, описываем логику и добавляем необходимые Listener.
Получившееся окно инструментов (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 содержит массу интересного внутри себя, тем более ее код доступен всем.