最近由于工做须要开发了一个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
填写完必要信息后,能够看到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以及不断尝试完成的。