问题
由于工作问题,在开发完业务提交到功能环境,可能出现问题导致需要进行远程调试,而远程的功能环境配置是根据每次打版动态变化ip与端口的,所以每次远程断点调试时都需要去web管理页面获取最新的ip与端口然后配置到IDEA的Remote
现在是已经有脚本可以获取每次打版后的ip与端口,所以本文主要讲的是
根据现有数据动态更新配置的插件开发
分析
- 在我们打开或者创建一个Remote时,是打开了Edit Configuration… 然后点添加“+”去增加一个配置的。由于这里我们不知道idea可能通过哪个操作去实现这个操作的,所以只能通过断点执行去尝试分析了
这里有一点要了解下,idea的所有插件都继承了抽象类AnAction,然后逻辑位于actionPerformed这个抽象函数去进行的,所以只要给这里加个断点就可以拦截所有可能触发的操作了,而这里的断点添加了判断条件
!e.myPlace.equals("ToolbarDecorator")
是因为“+”也会触发一个操作,而断点导致页面失去焦点又会触发隐藏,结果就是操作无法进行下去,所以根据实际情况添加了过滤条件
- 点击“Remote”创建一个配置时,发现并没有预期的进入断点,不仅如此,apply按钮也没有响应,这就意味着,实际上可能在我们点击Edit Configuration…的时候,已经进入了关于保存配置的Action了(IDEA的各种动作都是一个Action)。
- 重新到主界面进行操作,果然可以看到断点进入了
EditRunConfigurationsAction
这个类
public class EditRunConfigurationsAction extends DumbAwareAction {
...
public void actionPerformed(@NotNull AnActionEvent e) {
if (e == null) {
$$$reportNull$$$0(0);
}
Project project = (Project)e.getData(CommonDataKeys.PROJECT);
if (project == null || !project.isDisposed()) {
if (project == null) {
project = ProjectManager.getInstance().getDefaultProject();
}
(new EditConfigurationsDialog(project)).show(); //可以看到这里一个Dialog(弹框)show出来了
}
}
- 所以继续追踪
EditConfigurationsDialog
类
public class EditConfigurationsDialog extends SingleConfigurableEditor implements RunDialogBase {
//之所以没选这个是因为操作并没触发进入这函数,所以不考虑
private void addRunConfiguration(@NotNull ConfigurationFactory factory) {...}
//这里明显就是ok按钮的响应操作了
protected void doOKAction() {
RunConfigurable configurable = (RunConfigurable)this.getConfigurable();
//所以只能往它父类 SingleConfigurableEditor 看看了
super.doOKAction();
if (this.isOK()) {
//本来很开心以为就是这个函数了,但是实际上这里是Kotlin实现的,所以这里没法继续断点跟踪下去
configurable.updateActiveConfigurationFromSelected();
}
}
public Executor getExecutor(){...}
}
-
SingleConfigurableEditor
类里可以看到doOkAction、ApplyAction,稍微断点验证下就能确定配置界面的ok跟apply按钮会响应这些操作,这也意味着我们查找保存配置可能越来越近了,选择doOkAction函数继续追踪
protected void doOKAction() {
try {
//这里可以推测,先判断页面是否是改动,是才会进行更新
if (this.myConfigurable.isModified()) {
this.myConfigurable.apply(); //也就是这里很可能就是我们要找的地方
this.mySaveAllOnClose = true;
}
} catch (ConfigurationException var2) {
if (var2.getMessage() != null) {
if (this.myProject != null) {
Messages.showMessageDialog(this.myProject, var2.getMessage(), var2.getTitle(), Messages.getErrorIcon());
} else {
Messages.showMessageDialog(this.getRootPane(), var2.getMessage(), var2.getTitle(), Messages.getErrorIcon());
}
}
return;
}
super.doOKAction();
}
- 根据
this.myConfigurable.apply()
跳转到UnnamedConfigurable
接口,为apply函数添加断点
public interface UnnamedConfigurable {
void apply() throws ConfigurationException;
}
- 这次修改配置面板的配置再点击apply按钮(因为我们断点的是apply接口),而从
SingleConfigurableEditor
的动作操作也可以看出,修改了配置后(isModified)无论点击ok还是apply都会执行UnnamedConfigurable.apply()
public void actionPerformed(ActionEvent event) {
if (!SingleConfigurableEditor.this.myPerformAction) {
try {
SingleConfigurableEditor.this.myPerformAction = true;
if (SingleConfigurableEditor.this.myConfigurable.isModified()) {
SingleConfigurableEditor.this.myConfigurable.apply();
SingleConfigurableEditor.this.mySaveAllOnClose = true;
SingleConfigurableEditor.this.setCancelButtonText(CommonBundle.getCloseButtonText());
}
} catch (ConfigurationException var6) {
if (SingleConfigurableEditor.this.myProject != null) {
Messages.showMessageDialog(SingleConfigurableEditor.this.myProject, var6.getMessage(), var6.getTitle(), Messages.getErrorIcon());
} else {
Messages.showMessageDialog(SingleConfigurableEditor.this.getRootPane(), var6.getMessage(), var6.getTitle(), Messages.getErrorIcon());
}
} finally {
SingleConfigurableEditor.this.myPerformAction = false;
}
}
}
- 这次断点进入的是
SingleConfigurationConfigurable
类,大概分析下逻辑,可以看出这里的操作是- 获取配置(this.getSettings)
- 如果settings变动了,则进行一系列判断操作
- 把settings加入RunManagerImpl
public void apply() throws ConfigurationException {
RunnerAndConfigurationSettings settings = (RunnerAndConfigurationSettings)this.getSettings();
RunConfiguration runConfiguration = settings.getConfiguration();
settings.setName(this.getNameText());
runConfiguration.setAllowRunningInParallel(this.myIsAllowRunningInParallel);
if (runConfiguration instanceof TargetEnvironmentAwareRunProfile) {
((TargetEnvironmentAwareRunProfile)runConfiguration).setDefaultTargetName(this.myDefaultTargetName);
}
settings.setFolderName(this.myFolderName);
if (this.isStorageModified()) {
switch(this.myRCStorageType) {
case Workspace:
settings.storeInLocalWorkspace();
break;
case DotIdeaFolder:
settings.storeInDotIdeaFolder();
break;
case ArbitraryFileInProject:
if (getErrorIfBadFolderPathForStoringInArbitraryFile(this.myProject, this.myFolderPathIfStoredInArbitraryFile) == null) {
String fileName = getFileNameByRCName(settings.getName());
settings.storeInArbitraryFileInProject(this.myFolderPathIfStoredInArbitraryFile + "/" + fileName);
}
break;
default:
throw new IllegalStateException("Unexpected value: " + this.myRCStorageType);
}
}
super.apply();
RunManagerImpl.getInstanceImpl(this.myProject).addConfiguration(settings);
}
思路
从第8步来看,这里涉及的一个配置大概有:
- RunManagerImpl(疑似配置管理的对象)
- SingleConfigurationConfigurable(疑似配置的操作类)
-
RunnerAndConfigurationSettings(疑似配置的载体)
所以,我们需要确定这几个对象是怎么来的(毕竟不是随便直接new几个对象,就能用上)
实现
RunManagerImpl.getInstanceImpl(this.myProject)
很明显直接静态执行获取即可
虽然SingleConfigurationConfigurable
有2个构造函数,但是对外开放的只有editSettings
这个静态方法
@NotNull
public static <Config extends RunConfiguration> SingleConfigurationConfigurable<Config> editSettings(@NotNull RunnerAndConfigurationSettings settings, @Nullable Executor executor) {
if (settings == null) {
$$$reportNull$$$0(1);
}
SingleConfigurationConfigurable<Config> configurable = new SingleConfigurationConfigurable(settings, executor);
configurable.reset();
if (configurable == null) {
$$$reportNull$$$0(2);
}
return configurable;
}
从这儿看,我们需要RunnerAndConfigurationSettings
,Executor
这两个对象,但是从上下文可以分析Executor
对象在构造时,其实可以为null的,所以我们只需要一个 RunnerAndConfigurationSettings
。
查看RunnerAndConfigurationSettings
这个接口可以发现,只有一个实现类RunnerAndConfigurationSettingsImpl
(虽然它是Kotlin的),但是从语法上还是能大概看出它的构造函数
public constructor(
manager: com.intellij.execution.impl.RunManagerImpl,
_configuration: com.intellij.execution.configurations.RunConfiguration? /* = compiled code */,
isTemplate: kotlin.Boolean /* = compiled code */,
level: com.intellij.execution.impl.RunConfigurationLevel /* = compiled code */) :
kotlin.Cloneable,com.intellij.execution.RunnerAndConfigurationSettings, kotlin.Comparable<kotlin.Any>,
com.intellij.configurationStore.SerializableScheme {
需要四个对象:
- manager(
com.intellij.execution.impl.RunManagerImpl
):这个已经有了 - _configuration(
com.intellij.execution.configurations.RunConfiguration
):很明显这个是我们的配置对象 - isTemplate(
kotlin.Boolean
):明显说的是是否是模板 - false - level(
com.intellij.execution.impl.RunConfigurationLevel
):直接跟着调试级别走 - WORKSPACE
再去看com.intellij.execution.configurations.RunConfiguration
,依然是一个接口(面向接口编程,典型),根据我们断点可以确定我们需要是的一个RemoteConfiguration
对象
看RemoteConfiguration
的构造方法,需要一个工厂类,所以重新断点跑进RemoteConfiguration 的构造函数
public class RemoteConfiguration extends ModuleBasedConfiguration<JavaRunConfigurationModule, Element> implements RunConfigurationWithSuppressedDefaultRunAction, RemoteRunProfile {
public RemoteConfiguration(Project project, ConfigurationFactory configurationFactory) {
super(new JavaRunConfigurationModule(project, true), configurationFactory);
}
调用链路是这样的
往上一个个类看,找到了RemoteConfigurationType
类
public final class RemoteConfigurationType extends SimpleConfigurationType {
public RemoteConfigurationType() {
super("Remote", ExecutionBundle.message("remote.debug.configuration.display.name", new Object[0]), ExecutionBundle.message("remote.debug.configuration.description", new Object[0]), NotNullLazyValue.createValue(() -> {
return RunConfigurations.Remote;
}));
}
@NotNull
public RunConfiguration createTemplateConfiguration(@NotNull Project project) {
if (project == null) {
$$$reportNull$$$0(0);
}
return new RemoteConfiguration(project, this);
}
}
这下子各个对象就齐全了,开始整合。
结果
配置
一个插件的功能就是一个Action,所以随便创建一个Action挂靠在随便一个路径下(这里我挂靠的是VCS->Browser VCS Resposity路径下,纯粹是随意)
<actions>
<action id="RemoteConfiguration" class="AutoRemoteConfiguration(Action类名)"
text="RemoteConfiguration(功能菜单显示内容)" description="Auto add remote configuration">
<add-to-group group-id="Vcs.Browse(这个决定挂靠哪儿)" anchor="first(顺序问题)"/>
</action>
</actions>
创建后大概就是这样了
action
public class AutoRemoteConfiguration extends AnAction {
@Override
public void actionPerformed(AnActionEvent e) {
RunConfiguration setting = new RemoteConfigurationType()
.createTemplateConfiguration(Objects.requireNonNull(e.getProject()));
//这里可以根据自己需要配置配置源
RemoteConfiguration remote = (RemoteConfiguration) setting;
remote.setName("remote");
remote.HOST = "127.0.0.1";
RunManagerImpl runManager = RunManagerImpl.getInstanceImpl(e.getProject());
RunnerAndConfigurationSettingsImpl configurationSettings = new RunnerAndConfigurationSettingsImpl(runManager,
setting, false, RunConfigurationLevel.WORKSPACE);
SingleConfigurationConfigurable singleConfigurationConfigurable = SingleConfigurationConfigurable
.editSettings(configurationSettings, null);
RunnerAndConfigurationSettings settings =
(RunnerAndConfigurationSettings) singleConfigurationConfigurable.getSettings();
runManager.addConfiguration(settings);
}
效果:
说几点
工作用的电脑上的idea是2018版本,而我自己电脑是2020年版本。在工作的时候做完插件后,回宿舍复盘时,遇到几点问题:
- 18版本在创建的时候,部分逻辑跟20版本有差,但是影响不大,而且可以直接执行成功
- 20版本执行debug的时候,会提示找不到类,原因是创建的时候,默认生成的plugin.xml,需要手动的配置上所需要的依赖,这里主要用到的是
com.intellij.java
里面的各个jar包,所以添加上<depends>com.intellij.java</depends>
即可,具体视情况而定