最近因为工作需要开发了一个Intellij Idea插件,可用于解析Android中的文件结构并进行修改,实现一键引入依赖和初始化,过程各种心酸事,如今记录一下作个备忘,如果能帮助到他人也是一种荣幸。
新建插件工程
1.下载最新的Intellij idea并安装 2. New Project ->选择Intellij Platform Plugin->Project SDK选择 Intellij IDEA Community Edition IC-[版本号] 3. 点击Next并填写Project Name和Project Location 点击完成即可生成最初的插件工程,目录结构如下

给插件添加功能
这里介绍两种不同应用时机的插件
- 一种是继承自
AnAction,这种插件是点击 Intellij IDEA某个按钮后执行插件的功能, - 一种是实现
ProjectComponent接口,这种插件是在 Intellij IDEA或者Android Studio 打开Project后自动执行插件的功能 两种插件其实是公用,但是在开发过程中,ProjectComponent类插件限制较多,需要特别注意。
AnAction插件
新建方式如下图:

- 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节点中
public class ExampleAction extends AnAction {
@Override
public void actionPerformed(AnActionEvent e) {
// TODO: insert action logic here
}
}
新建Action继承AnAction类,插件实现的操作实现actionPerformed方法即可。
以上方式是新建一个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和SecondAction
ProjectComponent插件
这种类型插件需要实现ProjectComponent接口的projectOpened方法和projectClosed方法,主要是在projectOpened方法中即工程打开这个时机执行我们需要的操作,注意,这里执行的操作是在每次工程打开都会执行,因此需要十分慎重,个人建议最好添加条件判断十分每次打开都要执行,避免无谓的重复操作。ps.这里对写操作有很大的限制,需要特别注意。
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.
API
插件开发相关的API非常多,大部分都可以在官网文档上找到相关说明。传送门 官方默认对Java文件和XML文件支持比较好,解析和修改这两类文件时可以直接使用自带的API,而如果在Android项目中使用,想要支持Kotlin文件,gradle文件,properties文件或者想使用AndroidManifest.xml文件更方便,必须添加额外的依赖了。 以gradle文件为例。gradle文件内容是用groovy语言编写的,因此想要解析gradle文件需要添加groovy依赖。这里添加有两个步骤。
- 打开Project Structure,点击SDKs项,选中Intellij IDEA,然后点击右侧+号,选中[Intellij IDEA安装目录]/plugins/Groovy/lib/Groovy.jar并添加。


经过第一步配置,已经可以使用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这些文件,则添加以下依赖


接下来我们用插件来实现一些小功能。
通过插件生成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方法中调用。
- 找到自定义的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);
}
- 添加方法
/**
* 在自定义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依赖
要添加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!");
});
}
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本身就支持了。


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

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