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 插件開發教程

相關文章
相關標籤/搜索