Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Disclaimer: я не являюсь сотрудником JetBrains (а жаль), поэтому код может являться не оптимальным и служит только для примера и исследовательских целей.
Введение
Часто во время работы со Spring непонятно, правильно ли работает аннотация @Transaction:
в правильном ли месте мы ее поставили
правильно ли объявился interceptor
и т.д.
Самым простым способом для меня было остановиться в debug в IDEA в необходимом методе и исследовать, что возвращает
TransactionSynchronizationManager.isActualTransactionActive();
Но "я же программист" и захотелось это дело хоть как-то автоматизировать, заодно поизучать возможности написания plugin для IDEA.
Хочется иметь семафор, который визуально показывает, активна ли транзакция или нет, а также иметь возможность сделать срез основных свойств для анализа в будущем.
Примерно вот что получилось:
Подробней про транзакции можно прочитать здесь.
С чего начать создания plugin?
Про настройку проекта для создание plugin можно прочитать здесь.
Повторюсь, что главными отправными точками будут:
Документация по разработке плагинов
Исходный код IDEA
Шаблон проект
Набор примеров
Реализация
Исходный код получившегося решения размещен здесь.
Создание 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, который позволяет выполнять прикольные вещи, например, такие:
Поэтому, используя 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.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 новое окно
<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.
Способы передачи данных
Вернемся к нашему 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 содержит массу интересного внутри себя, тем более ее код доступен всем.
Ссылки
Документация по разработке плагинов
Исходный код IDEA
Шаблон для плагинов
Примеры простых плагинов
Использованный в статье исходный код