最近由於工做須要開發了一個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
這裏介紹兩種不一樣應用時機的插件java
AnAction
,這種插件是點擊 Intellij IDEA某個按鈕後執行插件的功能,ProjectComponent
接口,這種插件是在 Intellij IDEA或者Android Studio 打開Project後自動執行插件的功能 兩種插件實際上是公用,可是在開發過程當中,ProjectComponent
類插件限制較多,須要特別注意。新建方式以下圖: android
而後在打開的對話框中填寫插件相關信息以及插件條目出現位置,插件New 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接口的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() {
}
}
複製代碼
在開始真正的開發以前,必須先了解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很是多,大部分均可以在官網文檔上找到相關說明。傳送門 官方默認對Java文件和XML文件支持比較好,解析和修改這兩類文件時能夠直接使用自帶的API,而若是在Android項目中使用,想要支持Kotlin文件,gradle文件,properties文件或者想使用AndroidManifest.xml文件更方便,必須添加額外的依賴了。 以gradle文件爲例。gradle文件內容是用groovy語言編寫的,所以想要解析gradle文件須要添加groovy依賴。這裏添加有兩個步驟。
通過第一步配置,已經可使用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這些文件,則添加如下依賴
接下來咱們用插件來實現一些小功能。
這裏其實能夠分紅兩種狀況,一種是生成一個完整的類文件,一種是添加部分代碼,好比新增某個類並對其調用。
不管是哪一種狀況,都建議使用模板來生成咱們所需的代碼。這裏咱們將模板文件放到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方法中調用。
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);
}
複製代碼
/** * 在自定義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); }); } 複製代碼
/** * 在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;
}
複製代碼
/** * 在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依賴天然須要先找到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;
}
複製代碼
/** * 添加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!");
});
}
複製代碼
/** * 使用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!!");
});
}
複製代碼
若是須要對某個文件進行增刪改,首先就須要解析文件的PsiElement結構。而查看結構IntelliJ IDEA自己就支持了。
此次插件的開發過程簡直是痛並快樂着的一次體驗,由於現有大部分關於IDEA插件的開發文章都是比較簡單的介紹,特別是針對Android文件(包括gradle文件,properties文件,AndroidManifest.xml文件)的修改更是難找。因此,關於這些文件的修改開發,都是靠類比Java文件結構推理,查看IDEA 插件SDK API以及不斷嘗試完成的。