Intellij Idea插件开发--Android文件修改

最近由于工做须要开发了一个Intellij Idea插件,可用于解析Android中的文件结构并进行修改,实现一键引入依赖和初始化,过程各类心酸事,现在记录一下做个备忘,若是能帮助到他人也是一种荣幸。css

新建插件工程

1.下载最新的Intellij idea并安装 2. New Project ->选择Intellij Platform Plugin->Project SDK选择 Intellij IDEA Community Edition IC-[版本号]  3. 点击Next并填写Project Name和Project Location 点击完成便可生成最初的插件工程,目录结构以下html

enter description here

给插件添加功能

这里介绍两种不一样应用时机的插件java

  • 一种是继承自 AnAction,这种插件是点击 Intellij IDEA某个按钮后执行插件的功能,
  • 一种是实现ProjectComponent接口,这种插件是在 Intellij IDEA或者Android Studio 打开Project后自动执行插件的功能 两种插件实际上是公用,可是在开发过程当中,ProjectComponent类插件限制较多,须要特别注意。

AnAction插件

新建方式以下图: android

enter description here
而后在打开的对话框中填写插件相关信息以及插件条目出现位置,插件New Action说明以下:

  • id:做为标签的惟一标识。通常以<项目名>.<类名>方式。
  • class:即咱们自定义的AnAction类
  • text:显示的文字,如咱们自定义的插件放在菜单列表中,这个文字就是对应的菜单项
  • description:对这个AnAction的描述
  • Groups:表示插件入口出现的位置,好比可让插件出如今Tools菜单上,则能够选择ToolsMenu,若是想让插件出如今编辑框Generate菜单则能够选择GenerateGroup
  • Actions: 已有的Action,即已有的插件功能,选定这里的Action配合Anchor选项,能够指定咱们新建的Action出如今已有的Action以前或是以后。
  • Anchor:用来指定动做选项在Groups中的位置,Frist就是最上面、Last是最下面,也能够设在某个选项的上/下方 Keyboard Shortcuts:调用插件Action的快捷键,能够不填,填了要注意热键冲突

填写完必要信息后,能够看到resources/META-INF/plugin.xml文件多了一个Actions节点,新建的Action添加到了Action节点中app

public class ExampleAction extends AnAction {

    @Override
    public void actionPerformed(AnActionEvent e) {
        // TODO: insert action logic here
    }
}
复制代码

新建Action继承AnAction类,插件实现的操做实现actionPerformed方法便可。dom

以上方式是新建一个Action,实际还能够新建一个ActionGroup,目前好像只支持手动修改resources/META-INF/plugin.xml添加ActionGroup。举个例子:编辑器

<actions>
    <group id="com.example.MyGroup" text="MyGroup" popup="true">
        <add-to-group group-id="HelpMenu" anchor="first"/>
        <action  id="MyGroup.FirstAction" class="com.example.plugin.FirstAction" text="FirstAction"/>
        <action id="MyGroup.SecondAction"  class="com.example.plugin.SecondAction" text="SecondAction"/>
    </group>
</actions>
复制代码

经过以上配置即会在HelpMenu第一位添加一个MyGroup菜单,而且MyGroup菜单会有两个子菜单FirstAction和SecondActionide

ProjectComponent插件

这种类型插件须要实现ProjectComponent接口的projectOpened方法和projectClosed方法,主要是在projectOpened方法中即工程打开这个时机执行咱们须要的操做,注意,这里执行的操做是在每次工程打开都会执行,所以须要十分慎重,我的建议最好添加条件判断十分每次打开都要执行,避免无谓的重复操做。ps.这里对写操做有很大的限制,须要特别注意。post

public class OppoProjectComponent implements ProjectComponent {
    private Project mProject;

    public OppoProjectComponent(Project inProject) {
        mProject = inProject;
    }

    @Override
    public void projectOpened() {
    }

    @Override
    public void projectClosed() {
    }
}
复制代码

插件开发一些相关概念和API

概念

在开始真正的开发以前,必须先了解Intellij Idea SDK中的经常使用概念和API. Intellij Idea SDK中有两种File概念,一个是VitrualFile,一种是PSIFile(psi: Program Structure Interface)。 VitrualFile能够近似地认为是Java中的File,传统的文件操做方法VirtualFile都支持。 在说PsiFile是什么以前先说PSI Element是什么,PSI Element是PSI系统下不一样类型对象的一个统称,PSI系统中一切皆是PSI Element,包括一个方法,一个左括号,一个空格,一个换行符都是PSI Element。而处于一个文件中全部的PSI Element集合就是PSIFile.gradle

API

插件开发相关的API很是多,大部分均可以在官网文档上找到相关说明。传送门 官方默认对Java文件和XML文件支持比较好,解析和修改这两类文件时能够直接使用自带的API,而若是在Android项目中使用,想要支持Kotlin文件,gradle文件,properties文件或者想使用AndroidManifest.xml文件更方便,必须添加额外的依赖了。 以gradle文件为例。gradle文件内容是用groovy语言编写的,所以想要解析gradle文件须要添加groovy依赖。这里添加有两个步骤。

  1. 打开Project Structure,点击SDKs项,选中Intellij IDEA,而后点击右侧+号,选中[Intellij IDEA安装目录]/plugins/Groovy/lib/Groovy.jar并添加。
    enter description here
    enter description here

通过第一步配置,已经可使用Groovy相关的插件API了,此时编译打包都不会有问题,然而真正使用的时候会报Groovy相关插件API没找到的问题。所以还须要第二步添加depends

2.在resources/META-INF/plugin.xml添加depends

<depends>org.intellij.groovy</depends>
复制代码

经过以上两步才能正常使用Groovy的插件API。

所以若是针对Android Studio开发须要解析Kotlin,gradle,properties和AndroidManifest.xml这些文件,则添加如下依赖

enter description here
enter description here

接下来咱们用插件来实现一些小功能。

经过插件生成Java代码

这里其实能够分红两种状况,一种是生成一个完整的类文件,一种是添加部分代码,好比新增某个类并对其调用。

不管是哪一种状况,都建议使用模板来生成咱们所需的代码。这里咱们将模板文件放到resouces目录下,能够经过

xxxx.class.getClassLoader().getResource(templateFileName);
复制代码

方式获取到文件的流。

生成完整的类文件

以生成自定义Application类为例。这里的自定义的Application类须要先从AndroidManifest.xml解析 是否已有自定义Application类,这里先假设解析出来尚未自定义的Application类。 整个流程能够分为如下几步: 1.从resources中读取模板文件

/** * 读取模板文件中的字符内容 * * @param fileName 模板文件名 * @return */
    private String readTemplateFile(String fileName) {
        InputStream in = null;
        String content = "";
        try {
            in = ApplicationClassOperator.class.getClassLoader().getResource(fileName).openStream();
            content = StreamUtil.inputStream2String(in);
        } catch (IOException e) {
            Loger.error("getResource error");
            e.printStackTrace();
        }
        return content;
    }
复制代码

2.替换模板中的信息

/** * 替换模板中字符 * * @return */
    private String replaceTemplateContent(String source, String packageName) {
        source = source.replace("$packageName", packageName);
        return source;
    }
复制代码

3.生成java文件

/** * 生成java文件 * * @param content 类中的内容 * @param classPath 类文件路径 * @param className 类文件名称 */
    private void writeToFile(String content, String classPath, String className) {
        try {
            File folder = new File(classPath);
            if (!folder.exists()) {
                folder.mkdirs();
            }

            File file = new File(classPath + "/" + className);
            if (!file.exists()) {
                file.createNewFile();
            }

            FileWriter fw = new FileWriter(file.getAbsoluteFile());
            BufferedWriter bw = new BufferedWriter(fw);
            bw.write(content);
            bw.close();
        } catch (IOException e) {
            e.printStackTrace();
            Loger.error("write to file error! "+e.getMessage());
        }

    }
复制代码

在原有基础上进行修改,添加部分代码

添加部分代码与添加完整文件其实彻底不同,由于上面的添加并无用到PSI相关的API. 一样以修改Application为例,这里添加一个方法并在onCreate方法中调用。

  1. 找到自定义的Application类 这里的自定义的Application类须要从AndroidManifest.xml解析获取,AndroidManifest.xml解析暂时先放一放,后文再说。先假设拿到了AndroidManifest.xml中的application中android:name,经过这个android:name便可找到自定义的Application类。
/** * 若是已有自定义application类,检查是否须要添加模板初始化方法 * * @param manifestModel * @return 若是application.java已有initTemplate方法返回true + 自定义Application类 */
    public Pair<Boolean, PsiClass> checkApplication(Project project, ManifestModel manifestModel) {
        PsiClass appClass = null;
        if (manifestModel.applicationName != null) {
            String fullApplicationName = manifestModel.applicationName;
            if (manifestModel.applicationName.startsWith(".")) {
                fullApplicationName = manifestModel.packageName + manifestModel.applicationName;
            }

            appClass = JavaPsiFacade.getInstance(project).findClass(fullApplicationName, GlobalSearchScope.projectScope(project));
            PsiMethod[] psiMethods = appClass.getAllMethods();
            for (PsiMethod method : psiMethods) {
                if (Constants.INIT_METHOD_IN_APP.equals(method.getName())) {
                    return new Pair<>(true, appClass);
                }
            }
        }
        return new Pair<>(false, appClass);
    }
复制代码
  1. 添加方法
/** * 在自定义Application类中添加initTemplate()方法 */
    public void addInitTemplateMethod(Project project, PsiClass psiClass) {
        String method = null;
        try {
            InputStream in = getClass().getClassLoader().getResource("/templates/initTemplateInAppMethod.txt").openStream();
            method = StreamUtil.inputStream2String(in);
        } catch (IOException e) {
            e.printStackTrace();
        }
        if (method == null) {
            throw new RuntimeException("initTemplateInAppMethod shuold not be null");
        }
        final String finalMethod = method.replace("\r\n", "\n");
        WriteCommandAction.runWriteCommandAction(project, () -> { PsiMethod psiMethod = PsiElementFactory.SERVICE.getInstance(project).createMethodFromText(finalMethod, psiClass); psiClass.add(psiMethod); }); } 复制代码
  1. 找到调用的位置
/** * 在onCreate中找到super.onCreate();所在位置 * * @param psiClass * @return */
    public static PsiElement findCallPlaceInOnCreate(PsiClass psiClass) {
        PsiMethod[] psiMethods = psiClass.getAllMethods();

        for (PsiMethod psiMethod : psiMethods) {
            if ("onCreate".equals(psiMethod.getName())) {
                PsiCodeBlock psiCodeBlock = psiMethod.getBody();
                if (psiCodeBlock == null) {
                    return null;
                }
                for (PsiElement psiElement : psiCodeBlock.getChildren()) {
                    if ("super.onCreate();".equals(psiElement.getText())) {
                        return psiElement;
                    }
                }
            }
        }
        return null;
    }
复制代码
  1. 添加调用语句
/** * 在onCreate方法中添加调用initTemplate();语句 * * @param project * @param psiClass * @param anchor */
    public static void addCallInitMethod(Project project, PsiClass psiClass, PsiElement anchor) {
        WriteCommandAction.runWriteCommandAction(project, () -> {
            PsiStatement psiStatement = PsiElementFactory.SERVICE.getInstance(project)
                    .createStatementFromText(Constants.INIT_METHOD_CALL_IN_APP_ONCREATE, psiClass);
            psiClass.addAfter(psiStatement, anchor);
        });
    }
复制代码

经过插件添加Gradle依赖

要添加gradle依赖天然须要先找到app module的build.gradle文件,而后找到Dependencies节点,并添加须要的依赖。

1.找到app module的build.gradle文件

/** * 获取apply plugin: 'com.android.application'所在的PisFile中的 buildscript 的PsiElement * * @param project * @return */
    public static PsiElement getAppBuildScriptFile(Project project) {
        PsiFile[] psiFiles = FilenameIndex.getFilesByName(project, "build.gradle", GlobalSearchScope.projectScope(project));
        for (PsiFile psiFile : psiFiles) {
            PsiElement[] psiElements = psiFile.getChildren();
            for (PsiElement psiElement : psiElements) {
                if ( psiElement instanceof GrMethodCallExpressionImpl && "buildscript".equals(psiElement.getFirstChild().getText())) {
                    return psiElement;
                }
            }
        }
        return null;
    }
复制代码

2.找到Dependencies节点

/** * 找出buildscript节点下的dependencies节点 * * @param buildscriptElement * @return */
    public static PsiElement findBuildScriptDependencies(PsiElement buildscriptElement) {
        //buildscriptElement 最后一个child 是 codeBlock
        PsiElement[] psiElements = buildscriptElement.getLastChild().getChildren();
        for (PsiElement psiElement : psiElements) {
            if (psiElement instanceof GrMethodCallExpressionImpl && "dependencies".equals(psiElement.getFirstChild().getText())) {
                return psiElement;
            }
        }
        return null;
    }
复制代码
  1. 添加依赖
/** * 添加Dependencies, * * @param project * @param dependenciesElement 整个Dependencies父节点 */
    public static void addDependencies(Project project, PsiElement dependenciesElement, List<String> depends) {
        WriteCommandAction.runWriteCommandAction(project, () -> {
            for (String depend : depends) {
                GrStatement statement = GroovyPsiElementFactory.getInstance(project).createStatementFromText(depend);
                PsiElement dependenciesClosableBlock = dependenciesElement.getLastChild();
                //添加依赖项在 } 前,即在dependencies 末尾添加新的依赖项
                dependenciesClosableBlock.addBefore(statement, dependenciesClosableBlock.getLastChild());
            }
            Loger.info("addDependencies success!");
        });
    }
复制代码

AndroidManifest.xml解析

/** * 使用Dom方式解析 Manifest.xml * * @param project * @return */
    public static ManifestModel resolveManifestModel(Project project) {
        DomManager manager = DomManager.getDomManager(project);
        ManifestModel manifestModel = new ManifestModel();
        PsiFile[] psiFiles = FilenameIndex.getFilesByName(project, "AndroidManifest.xml", GlobalSearchScope.projectScope(project));
        if (psiFiles.length <= 0) {
            Loger.error("this project is not an Android Project!");
        }
        for (PsiFile psiFile : psiFiles) {
            if (!(psiFile instanceof XmlFile)) {
                Loger.error("this file cannot cast to XmlFile,just ignore!");
                continue;
            }
            DomFileElement<Manifest> domFileElement = manager.getFileElement((XmlFile) psiFile, Manifest.class);
            if (domFileElement != null) {
                Manifest manifest = domFileElement.getRootElement();
                if (manifest.getPackage().getXmlAttributeValue() != null) {
                    manifestModel.packageName = manifest.getPackage().getXmlAttributeValue().getValue();
                }
                Application application = manifest.getApplication();

                manifestModel.application = application;
                if (application.exists()) {
                    //application存在则说明这是主app module的AndroidManifest.xml
                    if (application.getName().exists()) {
                        //android:name已经存在,无需重复添加
                        Loger.info("application.getName()==" + application.getName().getRawText());
                        manifestModel.applicationName = application.getName().getRawText();
                    }
                } else {
                    Loger.info("application section not exist,just ignore this xml file!");
                }
            }
        }
        return manifestModel;
    }
复制代码

若是有须要添加android:name,则能够这样子作:

/** * 在AndroidManifest.xml添加自定义application属性 * @param project * @param application */
    public static void addApplicationName(Project project, Application application) {
        WriteCommandAction.runWriteCommandAction(project, () -> {
            application.getXmlTag().setAttribute("android:name", "."+Constants.APPLICATION_CLASS_NAME);
            CommonUtils.refreshProject(project);
            Loger.info("addApplicationName success!!");
        });
    }
复制代码

一点小技巧

查看文件的PSI结构

若是须要对某个文件进行增删改,首先就须要解析文件的PsiElement结构。而查看结构IntelliJ IDEA自己就支持了。

enter description here
enter description here

plugin工程运行调试可直接运行调试

enter description here
plugin工程可直接运行调试,点击运行后会打开一个沙盒Intellij IDEA编辑器,用这个编辑器打开工程便可运行调试插件。可是这个沙盒Intellij IDEA比较弱,对于额外的API依赖不支持,所以Gradle 插件API运行会失败。若是有更好的办法欢迎告知。

总结

此次插件的开发过程简直是痛并快乐着的一次体验,由于现有大部分关于IDEA插件的开发文章都是比较简单的介绍,特别是针对Android文件(包括gradle文件,properties文件,AndroidManifest.xml文件)的修改更是难找。因此,关于这些文件的修改开发,都是靠类比Java文件结构推理,查看IDEA 插件SDK API以及不断尝试完成的。

参考资料

IntelliJ Platform SDK DevGuide

AndroidStudio插件超详细教程

Android Studio Plugin 插件开发教程

相关文章
相关标签/搜索