【开发记录】idea插件 —— 动态生成Configurations配置

问题

由于工作问题,在开发完业务提交到功能环境,可能出现问题导致需要进行远程调试,而远程的功能环境配置是根据每次打版动态变化ip与端口的,所以每次远程断点调试时都需要去web管理页面获取最新的ip与端口然后配置到IDEA的Remote

现在是已经有脚本可以获取每次打版后的ip与端口,所以本文主要讲的是根据现有数据动态更新配置的插件开发

分析

  1. 在我们打开或者创建一个Remote时,是打开了Edit Configuration… 然后点添加“+”去增加一个配置的。由于这里我们不知道idea可能通过哪个操作去实现这个操作的,所以只能通过断点执行去尝试分析了
    【开发记录】idea插件 —— 动态生成Configurations配置

这里有一点要了解下,idea的所有插件都继承了抽象类AnAction,然后逻辑位于actionPerformed这个抽象函数去进行的,所以只要给这里加个断点就可以拦截所有可能触发的操作了,而这里的断点添加了判断条件!e.myPlace.equals("ToolbarDecorator")是因为“+”也会触发一个操作,而断点导致页面失去焦点又会触发隐藏,结果就是操作无法进行下去,所以根据实际情况添加了过滤条件

  1. 点击“Remote”创建一个配置时,发现并没有预期的进入断点,不仅如此,apply按钮也没有响应,这就意味着,实际上可能在我们点击Edit Configuration…的时候,已经进入了关于保存配置的Action了(IDEA的各种动作都是一个Action)。
  2. 重新到主界面进行操作,果然可以看到断点进入了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出来了
      }
  }
  1. 所以继续追踪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(){...}
}
  1. 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();
    }
  1. 根据this.myConfigurable.apply()跳转到UnnamedConfigurable接口,为apply函数添加断点
public interface UnnamedConfigurable {
    void apply() throws ConfigurationException;
}
  1. 这次修改配置面板的配置再点击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;
                }

            }
        }
  1. 这次断点进入的是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

RunManagerImpl.getInstanceImpl(this.myProject)

很明显直接静态执行获取即可

  • SingleConfigurationConfigurable

虽然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 { 

需要四个对象:

  1. manager(com.intellij.execution.impl.RunManagerImpl):这个已经有了
  2. _configuration(com.intellij.execution.configurations.RunConfiguration):很明显这个是我们的配置对象
  3. isTemplate(kotlin.Boolean):明显说的是是否是模板 - false
  4. level(com.intellij.execution.impl.RunConfigurationLevel):直接跟着调试级别走 - WORKSPACE

再去看com.intellij.execution.configurations.RunConfiguration,依然是一个接口(面向接口编程,典型),根据我们断点可以确定我们需要是的一个RemoteConfiguration对象
【开发记录】idea插件 —— 动态生成Configurations配置
RemoteConfiguration的构造方法,需要一个工厂类,所以重新断点跑进RemoteConfiguration 的构造函数

public class RemoteConfiguration extends ModuleBasedConfiguration<JavaRunConfigurationModule, Element> implements RunConfigurationWithSuppressedDefaultRunAction, RemoteRunProfile {
    public RemoteConfiguration(Project project, ConfigurationFactory configurationFactory) {
        super(new JavaRunConfigurationModule(project, true), configurationFactory);
    }

调用链路是这样的
【开发记录】idea插件 —— 动态生成Configurations配置
往上一个个类看,找到了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>

创建后大概就是这样了
【开发记录】idea插件 —— 动态生成Configurations配置

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插件 —— 动态生成Configurations配置

说几点

工作用的电脑上的idea是2018版本,而我自己电脑是2020年版本。在工作的时候做完插件后,回宿舍复盘时,遇到几点问题:

  1. 18版本在创建的时候,部分逻辑跟20版本有差,但是影响不大,而且可以直接执行成功
  2. 20版本执行debug的时候,会提示找不到类,原因是创建的时候,默认生成的plugin.xml,需要手动的配置上所需要的依赖,这里主要用到的是com.intellij.java里面的各个jar包,所以添加上<depends>com.intellij.java</depends>即可,具体视情况而定
    【开发记录】idea插件 —— 动态生成Configurations配置
上一篇:Springboot原理分析


下一篇:SpringBoot 自动配置原理