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:
<actions> <action id="TransactionStatusAction" class="com.github.pyltsin.sniffer.debugger.TransactionStatusAction" icon="SnifferIcons.RUNNING" text="Current Transaction Status"> <add-to-group group-id="XDebugger.ToolWindow.TopToolbar" anchor="last"/> </action></actions>
После этого должна была появиться новая кнопка на панели инструментов в debug-окне
Вычисление значений в debug
IDEA позволяет нам вычислять выражения при debug и взаимодействовать с памятью
И не толькоИнформация взята из twitter Тагира Валеева
В Evaluate даже встроен свой мини-интерпретатор Java, который позволяет выполнять прикольные вещи, например, такие:
Картинка из поста Тагира Валеева (http://personeltest.ru/aways/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?.sourcePositioncurrentSession.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 новое окно
<toolWindow id="TransactionView" secondary="true" icon="AllIcons.General.Modified" anchor="right"factoryClass="com.github.pyltsin.sniffer.ui.MyToolWindowFactory"/>
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<ChangeActionNotifier> 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 содержит массу интересного внутри себя, тем более ее код доступен всем.