我很喜欢IDEA
的一键自动生成代码功能,例如自动生成构造方法、字段的Get/Set
方法、ToString
方法等等,除此之外,也有一些插件提供自动生成代码的功能,例如我们所熟悉的GsonFormat
插件,使用该插件可以为我们快速的解析json
字符串生成一个对应的Java
类,这在对接一些第三方API
时很有帮助。
笔者写过一个运行时根据json
自动生成Class
的工具包:json-class-generator,与GsonFormat
不同的是,该工具使用ASM
在运行时解析json
结构树生成类的字节码,而GsonFormat
生成的是Java
源代码。当时写json-class-generator
目的是实现一个第三方API
自动对接框架,由于该框架涉及到业务,所以没有开源。
虽然json-class-generator
与GsonFormat
实现的功能不同,但原理相似。
上一篇我们了解到,Java
源代码编译后生成的Class
文件有固定的结构,而在IDEA
中,Java
源代码也同样有固定的结构:PSI
程序结构。与使用ASM
操作字节码修改一个Class
文件一样,我们也可以通过编辑一个Java
源代码的PSI
程序结构的元素修改Java
代码。
读懂本篇的前提是你已经对PSI
有所了解,如果尚未了解PSI
,可以先看下笔者写的上一篇《编写一个IDEA
插件之:PSI
分析Java
源代码》。
自动生成Java源代码
我们模仿IDEA
提供的自动生成代码功能,给右键弹出菜单的Generate...
菜单添加一个子菜单:GeneratedInvokePayMethod
,在插件使用者点击该菜单时自动生成一串代码,并且生成的代码插入到当前光标所在位置。
首先需要编写一个对应GeneratedInvokePayMethod
菜单的Action
,并实现actionPerformed
方法,代码如下。
public
actionPerformed
方法在菜单被点击时调用,该方法只有一个参数:
event
:这个参数封装了很多有用的信息,比如我们可以从该参数获取当前文件的PsiFile
实例、获取当前光标落在的PsiElement
等。
其次,我们需要注册Action
,将Action
放到右键弹出菜单的GenerateGroup
。需要在plugin.xml
文件添加如下配置信息:
<actions>
效果如下图所示。
现在我们继续完成GeneratedInvokePayMethodAction
的actionPerformed
方法。
由于Intellij Platform
不允许插件在主线程中进行实时的文件写入,只能通过异步任务来完成写入,因此,我们需要通过WriteCommandAction.runWriteCommandAction
来执行一个后台写入操作,如下代码所示。
public
想要在当前光标所在的位置插入一行代码,那么我们需要做这些事情:
1、先判断当前文件是否是一个
Java
文件,借助actionPerformed
的event
参数可取得当前文件的PsiFile
实例,判断PsiFile
实例的类型是否为PsiJavaFile
,如果不是,说明这不是一个Java
代码文件,什么也不需要做(或者可以给出对话框提示);
// AnActionEvent event
2、通过第一步获取的
PsiFile
,查找当前光标所在位置的PsiElement
实例;
PsiElement element = psiFile.findElementAt(editor.getCaretModel().getOffset());
其中editor.getCaretModel().getOffset()
为获取当前光标位置;
另外,可以使用AnActionEvent#getData
方法获取当前光标所在的PsiElement
,代码如下:
// AnActionEvent event
但这种方式不适用于当前场景,如果将光标放在一行代码的;
后面,那么该方法就会返回null
值。
3、根据光标所在的
PsiElement
,获取该PsiElement
所在方法的PsiCodeBlock
(一个方法只有一个PsiCodeBlock
);
PsiElement codeBlock = element;
while (!(codeBlock instanceof PsiCodeBlock)) {
codeBlock = codeBlock.getParent();
}
4、创建新的
PsiElement
,该PsiElement
就是需要自动生成的代码;例如创建一个表达式元素(PsiExpression
),可使用PsiElementFactory#createExpressionFromText
方法创建,代码如下。
PsiElement newElement = PsiElementFactory.getInstance(element.getProject())
.createExpressionFromText("Invocation invocation = Invocation.builder()\n" +
" .scope(scope)\n" +
" .service(payType)" +
" .operate(\"" + method + "\")\n" +
" .body(merchantNo)\n" +
" .build()", element.getContext());
PsiElementFactory
使用工厂模式生产PsiElement
,提供了大量的API
,例如创建字段的createField
、创建方法的createMethod
、创建类的createClass
,创建关键字的createKeyword
。
5、最后,将新创建的
PsiElement
添加到光标所在PsiElement
的后面;
// 参数1:新增的PsiElement
完整示例代码如下。
public
后记
实际要实现一个插件可能没有那么简单,例如本篇没有介绍到的UI
部分,笔者省略了一些步骤:当点击菜单时,先弹出一个Dialog
,提供一些选项,在完成选项点击ok
后再生成代码。
编写插件UI
其实与开发Android
应用编辑UI
布局类似,如果你开发过Android
应用,那么也就不难理解。