0
点赞
收藏
分享

微信扫一扫

编写一个IDEA插件之:自动生成Java代码


我很喜欢​​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​​有所了解

自动生成Java源代码

我们模仿​​IDEA​​​提供的自动生成代码功能,给右键弹出菜单的​​Generate...​​​菜单添加一个子菜单:​​GeneratedInvokePayMethod​​,在插件使用者点击该菜单时自动生成一串代码,并且生成的代码插入到当前光标所在位置。

首先需要编写一个对应​​GeneratedInvokePayMethod​​​菜单的​​Action​​​,并实现​​actionPerformed​​方法,代码如下。


public class GeneratedInvokePayMethodAction extends AnAction {
@Override
public void actionPerformed(@NotNull AnActionEvent event) {
}
}


​actionPerformed​​方法在菜单被点击时调用,该方法只有一个参数:

  • ​event​​​:这个参数封装了很多有用的信息,比如我们可以从该参数获取当前文件的​​PsiFile​​​实例、获取当前光标落在的​​PsiElement​​等。

其次,我们需要注册​​Action​​​,将​​Action​​​放到右键弹出菜单的​​GenerateGroup​​​。需要在​​plugin.xml​​文件添加如下配置信息:


<actions>
<action id="xxx.action.GeneratedInvokePayMethodAction" class="com.xxx.plugin.action.GeneratedInvokePayMethodAction"
text="GeneratedInvokePayMethod">
<!-- 将action放在哪 -->
<add-to-group group-id="GenerateGroup" anchor="first"/>
</action>
</actions>


效果如下图所示。



编写一个IDEA插件之:自动生成Java代码_python


现在我们继续完成​​GeneratedInvokePayMethodAction​​​的​​actionPerformed​​方法。

由于​​Intellij Platform​​​不允许插件在主线程中进行实时的文件写入,只能通过异步任务来完成写入,因此,我们需要通过​​WriteCommandAction.runWriteCommandAction​​来执行一个后台写入操作,如下代码所示。


public class GeneratedInvokePayMethodAction extends AnAction {

@Override
public void actionPerformed(@NotNull AnActionEvent event) {
// 立即执行一个后台任务
WriteCommandAction.runWriteCommandAction(editor.getProject(), () -> {
// do ...
});
}
}


想要在当前光标所在的位置插入一行代码,那么我们需要做这些事情:

  • 1、先判断当前文件是否是一个​​Java​​​文件,借助​​actionPerformed​​​的​​event​​​参数可取得当前文件的​​PsiFile​​​实例,判断​​PsiFile​​​实例的类型是否为​​PsiJavaFile​​​,如果不是,说明这不是一个​​Java​​代码文件,什么也不需要做(或者可以给出对话框提示);

// AnActionEvent event
PsiFile psiFile = event.getData(LangDataKeys.PSI_FILE);


  • 2、通过第一步获取的​​PsiFile​​​,查找当前光标所在位置的​​PsiElement​​实例;

PsiElement element = psiFile.findElementAt(editor.getCaretModel().getOffset());


其中​​editor.getCaretModel().getOffset()​​为获取当前光标位置;

另外,可以使用​​AnActionEvent#getData​​​方法获取当前光标所在的​​PsiElement​​,代码如下:


// AnActionEvent event
PsiElement psiElement = event.getData(LangDataKeys.PSI_ELEMENT);


但这种方式不适用于当前场景,如果将光标放在一行代码的​​;​​​后面,那么该方法就会返回​​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<Object> invocation = Invocation.<Object>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
// 参数2:位置参照的PsiElement
codeBlock.addAfter(newElement, element);


完整示例代码如下。


public class GeneratedInvokePayMethodAction extends AnAction {

@Override
public void actionPerformed(@NotNull AnActionEvent event) {
WriteCommandAction.runWriteCommandAction(editor.getProject(), () -> {
PsiFile psiFile = event.getData(LangDataKeys.PSI_FILE);
// 查找当前光标停留在的元素
PsiElement element = psiFile.findElementAt(editor.getCaretModel().getOffset());
// 获取当前方法的PsiCodeBlock元素
PsiElement codeBlock = element;
while (!(codeBlock instanceof PsiCodeBlock)) {
codeBlock = codeBlock.getParent();
}
// 使用PsiElementFactory创建表达式元素
PsiElement newElement = PsiElementFactory.getInstance(element.getProject())
.createExpressionFromText("Invocation<Object> invocation = Invocation.<Object>builder()\n" +
" .scope(scope)\n" +
" .service(payType)" +
" .operate(\"" + method + "\")\n" +
" .body(merchantNo)\n" +
" .build()", element.getContext());
// 将新创建的表达式元素插入到光标停留在的元素的后面
codeBlock.addAfter(newElement, element);
});
}
}


后记

实际要实现一个插件可能没有那么简单,例如本篇没有介绍到的​​UI​​​部分,笔者省略了一些步骤:当点击菜单时,先弹出一个​​Dialog​​​,提供一些选项,在完成选项点击​​ok​​后再生成代码。

编写插件​​UI​​​其实与开发​​Android​​​应用编辑​​UI​​​布局类似,如果你开发过​​Android​​应用,那么也就不难理解。

原作者:wujiuye



编写一个IDEA插件之:自动生成Java代码_java_02


举报

相关推荐

0 条评论