AS插件开发:根据特定格式的文本自动生成Java Bean字段

362 阅读8分钟
原文链接: blog.csdn.net

为什么要造轮子

在项目中,产品提出了新需求,开发们的开发流程一般是这样:

前后端根据需求讨论接口契约协议 ——> 后端发布契约 ——> 前后端各自按照契约编码 ——> 后端发布正式服务 ——> 前端调试接口

在讨论契约的过程中会产生很多新的字段、甚至是新的实体,前端要根据这些新字段、实体,原封不动的复制粘贴生成Java bean实体类,这项工作十分枯燥乏味!

作为一个程序猿,秉着能不重复我就偷懒的原则,就开始寻找满足我需求的AS插件,但是市场上大都是根据Json生成Java bean的插件(也可能是我搜的姿势不对……),一怒之下,就根据我们的契约格式撸了一发插件,同时也再练习一下插件开发的流程。

演示效果

先来看看效果(点击可查看大图,演示带注释和不带注释两种情况):

这里写图片描述

这里需要注意,只有满足下面的契约格式,本插件才能正常工作。如果格式有异,我预留了接口,可以自己实现自家的格式解析。

契约格式:

Name Type Desc
name String 姓名
score String 分数

或者省略注释:

Name Type
name String
score String

下面开始进入开发正题,如果还有不清楚如何使用Intellij开发AS插件的同学,请左转先看:
Android Studio插件开发入门篇

开发流程1——可视化界面

首先基于我的需求,我的插件需要一个可视化界面,它大概长这样:

这里写图片描述

有一个面板用来粘贴契约文本,有两个按钮用来选择成员变量的类型,有一个选择:是否自动生成“serialVersionUID”。

下面新建一个对话框:
这里写图片描述

系统会自动生成对应的文件:
这里写图片描述

这里的form文件就相当于Android的Xml文件,需要什么控件就直接拖拽,对照我的草图,结构是这样:

这里写图片描述

由于对这些控件不熟悉,所以属性什么的只能一点一点摸索,不过整体上感觉和Android类似,没什么难度。

有了可视化界面,我们接着就要写事件监听,无非就是一些按钮的点击事件,和Android类似,这里不表,大家可参考源码。

最后在Action中弹出对话框:

GenerateDialog generateDialog = new GenerateDialog();
generateDialog.setOnClickListener(mClickListener);
generateDialog.setTitle("GenerateModelByString");
//默认设置Serializable为false,即不产生:“private static final long serialVersionUID = 1L;”
generateDialog.setCbSerializable(false);
//自动调整对话框大小
generateDialog.pack();
//设置对话框跟随当前windows窗口
generateDialog.setLocationRelativeTo(WindowManager.getInstance().getFrame(e.getProject()));
generateDialog.setVisible(true);

现在我们跑一下代码,看看效果:
这里写图片描述

跟我们预想的效果一样!现在架子已经搭好了,下面开始我们的表演~

开发流程2——整理文本格式

上面一节我们已经搭好了可视化界面,接下来就要思考:如何把粘贴过来的杂乱文本解析成有实际的代码格式。以我的需求为例,我需要做一个这样的格式转换:

契约长这样:

Name Type Desc
name String 姓名
score String 分数
age int 年龄

实体类要长这样:

/**
 *  姓名
 **/
private String name;
/**
 *  分数
 **/
private String score;
/**
 *  年龄
 **/
private int age;

我们首先要把大段文本按行整理成小元组,这里按换行符\n把文本切割成多行,然后对每行文本按\t再切割,得到每行的有效文本。

private List<List<String>> convertToList(String str) {
   List<List<String>> modelList = new ArrayList<>();
   String[] lines = str.split("\n");
   for (String singleLine : lines) {
       if (TextUtils.isEmpty(singleLine)) {
           continue;
       }
       String[] stringArr = singleLine.split("\t");
       List<String> singleLineList = new ArrayList<>();
       for (String s : stringArr) {
           if (!TextUtils.isEmpty(s)) {
               singleLineList.add(s);
           }
       }
       modelList.add(singleLineList);
   }
   return modelList;
}

接着就要对每个文本小元组进行拼接,由于各个公司习惯不同,所以这里我抽了一个接口,便于大家实现自己的逻辑,如果格式和我们的相同(我们的格式:字段名 字段类型 注释),就可以直接拿走用了:

public interface ISpliceField {
    /**
     * @param fields 格式化后各行的文本元组
     * @param project 当前工程
     * @param psiClass 当前类
     * @param isSerializable 是否序列化
     * @param memberType 成员变量类型
     */
    void onSplice(List<List<String>> fields, Project project, PsiClass psiClass, boolean isSerializable, String memberType);
}

在工具类中使用接口:

public class CodeWriter {
  private static CodeWriter INSTANCE;

    public static CodeWriter getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new CodeWriter();
        }
        return INSTANCE;
    }

   /**
    * 自定义实现文本拼接逻辑
    */
    private ISpliceField spliceHelper = new ZtSpliceHelper();

    public String write(AnActionEvent event, List<List<String>> list, String type, boolean isSerializable) {
        //获取当前编辑的文件
        PsiFile psiFile = event.getData(LangDataKeys.PSI_FILE);
        if (psiFile == null) {
            return "PsiFile can not be null";
        }
        final String[] resultMessage = {"success"};
        //自动生成代码
        WriteCommandAction.runWriteCommandAction(event.getProject(), () -> {
            Editor editor = event.getData(PlatformDataKeys.EDITOR);
            if (editor == null) {
                resultMessage[0] = "Editor can not be null!";
                return;
            }
            Project project = editor.getProject();
            if (project == null) {
                resultMessage[0] = "Project can not be null!";
                return;
            }
            //获取当前编辑的class对象
            PsiElement element = psiFile.findElementAt(editor.getCaretModel().getOffset());
            PsiClass psiClass = PsiTreeUtil.getParentOfType(element, PsiClass.class);
            if (psiClass == null) {
                resultMessage[0] = "class can not be null!";
                return;
            }
            if (psiClass.getNameIdentifier() == null) {
                return;
            }
            try {
                //字符串拼接过程
                spliceHelper.onSplice(list, project, psiClass, isSerializable, type);
            } catch (Exception e) {
                resultMessage[0] = e.getMessage();
            }
        });
        return resultMessage[0];
    }
}

自己实现拼接字符串的过程,这中间没什么技术含量,就是按照格式,依次拼接注释、成员类型、变量类型、变量名,唯一需要注意的就是处理一下特殊情况:没有注释、变量类型可能不是标准的Java类型等。

//拼接注释
if (strings.size() == 3) {
     sb.append("/**\n *  ").append(strings.get(2)).append("\n*/\n");
 }

/**
 * 拼接成员变量类型
 **/
private void appendMemberType(StringBuilder sb) {
    if (mType == null) {
        mType = "private";
    }
    sb.append(mType).append(" ");
}

/**
 * 拼接变量类型
 **/
private void appendFieldType(List<String> strings, StringBuilder sb) {
    sb.append(modifyClassType(strings));
}

/**
 * 服务端契约中的类型跟我们用的类型有差别,这里修正一下
 * bool -> boolean
 * string -> String
 * decimal -> double
 */
private String modifyClassType(List<String> strings) {
    if (strings.size() > 1) {
         String type = strings.get(1);
         if ("boolean".contains(type)) {
             return "boolean";
         } else if ("decimal".equalsIgnoreCase(type)) {
             return "double";
         } else if (type.contains("string")) {
             type.replace("string", "String");
         } else {
             return type;
         }
    }
    return "";
}

因为我们的契约没有更标准的格式,我只是根据位置粗略的判断谁是注释、谁是变量名等。所以如果契约中缺失了关键字段,比如变量类型,那生成的代码肯定也是不标准的,这也没办法。如果契约格式能更标准化的话,解析过程就可以写的优雅很多。

开发流程3——自动生成代码

现在我们已经有了整理好的文本,最后需要做的就是把它们原封不动的写到目标类中。

这一块应该是整篇的核心,难也不难,主要是要熟悉系统API。网上资料不多,最快的方法就是找一个类似的自动生成插件的源码,照葫芦画瓢,这也是我们学习新知识时经常用的技巧。

下面我们一点一点捋一下。

首先我们需要获取当前编辑的文件:

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

模拟写代码可以调用这个方法:

WriteCommandAction.runWriteCommandAction(event.getProject(), () -> {
      Editor editor = event.getData(PlatformDataKeys.EDITOR);
      if (editor == null) {
          //resultMessage用于插件出错时,弹出错误提示框
          resultMessage[0] = "Editor can not be null!";
          return;
      }
      Project project = editor.getProject();
      if (project == null) {
          resultMessage[0] = "Project can not be null!";
          return;
      }
      //获取当前编辑的class对象
      PsiElement element = psiFile.findElementAt(editor.getCaretModel().getOffset());
      PsiClass psiClass = PsiTreeUtil.getParentOfType(element, PsiClass.class);
      if (psiClass == null) {
          return;
      }
      if (psiClass.getNameIdentifier() == null) {
          return;
      }
      try {
          spliceHelper.onSplice(list, project, psiClass, isSerializable, type);
      } catch (Exception e) {
          resultMessage[0] = e.getMessage();
      }
  });

根据文本生成代码片:

 ArrayList<PsiField> psiFields = new ArrayList<>();
 PsiField field = factory.createFieldFromText(“目标文本”, psiClass);
 psiFields.add(field);

最后把所有代码片交给目标类:

for (int i = 0; i < psiFields.size(); i++) {
    psiClass.add(psiFields.get(i));
}

由此也可以看出,我们这个插件 只适用于已经有类文件 的场景,也就是只适合追加字段。如果是从无到有生成新文件,那就是另一个插件的事了,这里不表,后续我会陆续更新新插件。

值得注意的是:我们在开发插件过程中,一定要注意处理错误,至少要知道是什么错,如果不处理,系统就直接卡死没反应,这样连改进都会无处下手。

所以我把生成代码的核心过程spliceHelper.onSplice(…)加了try…catch,在主程序中把错误信息直接以对话框的形式弹出:

if (!TextUtils.isEmpty(result) && !result.equalsIgnoreCase("success")) {
    Messages.showMessageDialog(result, "Error", Messages.getInformationIcon());
}

错误框长这样:
这里写图片描述

到此,大功可以告成也~

最后我们整体感觉一下,整个流程并不麻烦。自认为吧,AS插件的开发最重要的还是创意,我们在平时开发的过程中,肯定会遇到各种各样的痛点,AS插件可以很好的帮我们解决那些“机械性重复”的过程。只要有心,我们完全可以让敲代码变成一项轻松、炫酷的事情~~

最后,工程源码在此:android-GenerateModel-plugin-AS
插件已经上传到市场,直接搜GenerateModelPlugin即可下载。
这里写图片描述
欢迎大家star、issue~

预告

近期还准备重新开一个仓库:Android-EasyCodePlugins-master

专门放简化日常工作的AS插件,有些插件可能市场上已经有了,但是还是想自己动手操作,既放心又舒心~这里先预告一下:

  • 本文插件2.0:填写类名和字段文本生成类文件

  • 自动生成equals hashCode

  • 像AS中,.if生成if代码块一样,通过.onclick生成setOnClickListener代码块

感兴趣的可以star一下,或者有其他idea的,可以一起交流一下~