一起来动手做个 AS 插件,自动生成Java Bean文件

发布时间:2023-04-26 12:00



今日科技快讯


近日,国家广播电视总局在督察“今日头条”网站整改工作中,发现该公司组织推送的“内涵段子”客户端软件和相关公众号存在导向不正、格调低俗等突出问题,引发网民强烈反感。总局责令“今日头条”永久关停“内涵段子”客户端软件及公众号,并要求该公司举一反三,全面清理类似视听节目产品。


作者简介


本篇来自老司机  披萨大叔 的投稿,分享了在 AndroidStudio 中根据特定格式的文本自动生成 Java Bean 文件或字段的实践,一起来看看!希望大家喜欢。

 披萨大叔  的博客地址:

https://blog.csdn.net/qq_27258799


前言


为什么要造轮子

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

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

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

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

该插件已经上传市场:

File –> Settings –> Plugins –> Browse repositories…

\"一起来动手做个


演示效果


先来看看效果,该插件其实是两个插件集合,一种是在已有的Java Bean文件中生成字段,一种是生成Java Bean文件,分别对应在旧实体中追加字段和新建实体两种场景。

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

契约格式:

Name Type Desc
name String 姓名
score String 分数

或者省略注释:

Name Type
name String
score String

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

https://blog.csdn.net/qq_27258799/article/details/78093734

下面详细讲解下两种模式,我会重点介绍追加字段,因为新建实体,本质上就是新建空文件然后再追加字段。


在已有实体中追加字段


开发流程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 再切割,得到每行的有效文本。

当然可能不同公司有不同的契约模板,这里我把解析和拼接字符串的过程抽成了接口,大家可以自行实现逻辑

public interface ICodeGenerator {
   /**
    * 解析字符串
    * @param str 粘贴的字符串
    * @return 各行字符串
    */

   List> onParse(String str);

   /**
    * @param fields 格式化后的,各行的文本元组
    * @param project 当前工程
    * @param psiClass 当前类
    * @param memberType 成员变量类型
    */

   void onSplice(List> fields, Project project, PsiClass psiClass, String memberType);
}

生成代码的过程都使用接口:

ICodeGenerator codeGenerator = new DefaultCodeGenerator();
……
codeGenerator.onSplice(codeGenerator.onParse(pastedStr), project, psiClass, type);

我们只需要定义自己的 ICodeGenerator 就可以了,这里默认内置的是适合我们项目契约模板的生成器,字符串解析过程长这样:

public class DefaultCodeGenerator implements ICodeGenerator {
   @Override
   public List> onParse(String str) {
       List> modelList = new ArrayList<>();
       //首先按行分割
       String[] lines = str.split(\"\\n\");
       for (String singleLine : lines) {
           if (TextUtils.isEmpty(singleLine)) {
               continue;
           }
           String[] stringArr = singleLine.split(\"\\t\");
           //如果该行只有一个字符串,认为是上一行的注释有多行
           List lastLine;
           if (modelList.size() == 0) {
               lastLine = new ArrayList<>();
           } else {
               lastLine = modelList.get(modelList.size() - 1);
           }
           if (stringArr.length == 1 && lastLine.size() != 0) {
               String newLine = lastLine.get(lastLine.size() - 1) + \"\\n*\\t\" + stringArr[0];
               //多行注释,复制过来会带引号,去掉它们
               lastLine.set(lastLine.size() - 1, newLine.replaceAll(\"\\\"\",\"\"));
           } else {
               List singleLineList = new ArrayList<>();
               for (String s : stringArr) {
                   if (!TextUtils.isEmpty(s)) {
                       singleLineList.add(s);
                   }
               }
               modelList.add(singleLineList);
           }
       }
       return modelList;
   }
   ……
}

我这里着重处理了多行注释的场景:

\"一起来动手做个

这时候,如果不作处理,生成的代码是这样:

/**
*  点评描述文字:超棒、很好
**/

private String commentRemark;

所以每当遇到字符串元组只有一个元素时,就认为是遇到了多行注释的下一行,就把它拼接到上一行注释后面,最终经过拼接就可以得到正确结果:

/**
*  点评描述文字:超棒,很好4.9/5.0分 极赞
*  4.6/4.7/4.8 超棒
*  4.3/4.4/4.5 很好
*  4.0/4.1/4.2 不错
*  4.0以下 空
*/

private String commentRemark;

默认生成器中的拼接过程长这样:

@Override
 public void onSplice(List> fields, Project project, PsiClass psiClass, String memberType) {
      if (psiClass == null) {
          return;
      }

      PsiElementFactory factory = JavaPsiFacade.getInstance(project).getElementFactory();
      for (List strings : fields) {
          StringBuilder sb = new StringBuilder();
          if (strings.size() == 0 || strings.size() == 1) {
              continue;
          }
          //注释
          CommonUtil.appendAnnotation(strings, sb);
          //字段类型:int,字段类型不为空,再追加成员类型
          String fieldType = CommonUtil.appendFieldType(strings, sb);
          if (!TextUtils.isEmpty(fieldType)) {
              //成员类型:private
              CommonUtil.appendMemberType(memberType, sb);
              sb.append(fieldType);
          }
          //字段名
          CommonUtil.appendField(strings, sb);
          PsiField field = factory.createFieldFromText(sb.toString(), psiClass);
          psiClass.add(field);
      }
  }

自己实现拼接字符串的过程,这中间没什么技术含量,就是按照格式,依次拼接注释、成员类型、变量类型、变量名,唯一需要注意的就是处理一下特殊情况:没有注释、变量类型可能不是标准的 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 strings, StringBuilder sb) {
   sb.append(modifyClassType(strings));
}

/**
* 服务端契约中的类型跟我们用的类型有差别,这里修正一下
* bool -> boolean
* string -> String
* decimal -> double
*/

private String modifyClassType(List 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(spliceHelper.onParse(pastedStr), project, psiClass, isSerializable, type);
     } catch (Exception e) {
         resultMessage[0] = e.getMessage();
     }
 });

根据文本生成代码片:

 ArrayList 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());
}

错误框长这样:

\"一起来动手做个

到此,大功可以告成也~ 接着我们看看如何根据文本直接生成新实体。


根据文本新建实体


这个过程跟追加字段的流程大致一样,唯一不同的是,我们首先需要在工程目录下新建一个文件,然后再在文件中执行上面介绍的“追加字段”操作。

由于是新建实体,我把action放在了NewGroup中,配置文件长这样:

\"GenerateJavaBeanBySting\" class=\"actions.generateJavaBean.GenerateJavaBeanAction\" text=\"New Java Bean File By String\"
       description=\"Generate JavaBean File By String\" icon=\"/icons/ic_logo.png\">
   \"NewGroup\" anchor=\"first\"/>

对应效果:

\"一起来动手做个

值得一提是,如何找到 add-to-group group-id 是一件很蛋疼的事。在 AS 插件入门篇中,我们介绍过 id 是在新建 Action 选择的:

\"一起来动手做个

很惭愧,并没有找到帮助资料,我是一行一行自行分析的,新建文件嘛,应该不是 createXX就是 newXX,然后就找到了 NewGroup。

如果大家有这一块的资料,十分欢迎评论告知。我们继续正题,下面重点介绍如何使用模板新建文件。


使用模板新建文件


我们新建的很多文件都有特定的初始格式,比如我们新建 EmptyActivity,AS 会自动给我们创建 onCreate 方法,这就是使用预制的模板创建文件。

首先我们在 src 目录下新建 fileTemplates 目录,然后新建模板文件。

\"一起来动手做个

我的模板文件长这样:

#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != \"\")
package ${PACKAGE_NAME};
#end
#if (${INTERFACES} != \"\")
import java.io.Serializable;
#end
/**
* Created by ${USER} on ${DATE} ${TIME}
*/

public class ${NAME} ${INTERFACES}{
#if (${INTERFACES} != \"\")
   private static final long serialVersionUID = 1L;

#end
}

${NAME} 等是外部传入的参数,${NAME} 是文件名,${INTERFACES} 是接口,因为这里我只有序列化的需求,所以如果新建的实体需要序列化,${INTERFACES} = “implements Serializable”,自然需要引用 import java.io.Serializable;

这个模板相对简单,都能理解。有了模板文件,下面我们开始敲代码。

在所选的工程路径下新建文件:

JavaDirectoryService directoryService = JavaDirectoryService.getInstance();
//当前工程
Project project = actionEvent.getProject();
//鼠标右键选择的路径
IdeView ideView = actionEvent.getRequiredData(LangDataKeys.IDE_VIEW);
PsiDirectory directory = ideView.getOrChooseDirectory();
//检查文件是否已经存在
if (directory.findFile(fileName + \".java\") != null) {
   return \"Generation failed, \" + fileName + \" already exists\";
}
//模板文件参数
Map map = new HashMap<>();
map.put(\"NAME\", fileName);
if(isSerializable){
   map.put(\"INTERFACES\", \"implements Serializable\");
}else{
   map.put(\"INTERFACES\", \"\");
}
//PACKAGE暂时未用
map.put(\"PACKAGE\", CommonUtil.getPackageName(project));
//使用模板生成文件,GenerateFileByString是模板文件的名字
PsiClass psiClass = directoryService.createClass(directory, fileName, \"GenerateFileByString\", false, map);

到此,我们就可以新建出一个空文件啦。

值得注意的是,directoryService.createClass(…) 很容易抛出异常:

This template did not produce a Java class or an interface

一般都是没找到模板文件的缘故,请检查文件目录是否是 fileTemplates,目录下的模板文件后缀是 .java.ft,代码中模板文件名是否正确。


在空文件中追加字段


我们已经生成了新文件,剩余的操作大家是不是似曾相识?没错,正是我们最开始介绍的追加字段操作。

//使用模板生成文件
PsiClass psiClass = directoryService.createClass(directory, fileName, \"GenerateFileByString\", false, map);
//根据粘贴的文本生成字段
List> modelList = CommonUtil.convertToList(pasteStr);
WriteCommandAction.runWriteCommandAction(project, () -> generateModelField(serializable, member, project, psiClass, modelList));

其中 generateModelField 就是上文介绍“追加字段”时的 spliceHelper.onSplice(list, project, psiClass, isSerializable, type);。

这里需要注意的是,我们拿到新生成的 psiClass 以后,不能使用 psiClass.add(field) 添加代码,要调用 WriteCommandAction.runWriteCommandAction 写代码,否则会抛出异常:

Must not change PSI outside command or undo-transparent action.

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

最后,工程源码在此 Android-EasyJavaBean-Plugin:

https://github.com/unclepizza/Android-EasyJavaBean-Plugin

欢迎大家star、issue~


总结


近期重新开了一个仓库 Android-EasyCodePlugins-master:

https://github.com/unclepizza/Android-EasyCodePlugins-master

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

  • 自动生成equals hashCode

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

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


欢迎长按下图 -> 识别图中二维码

或者 扫一扫 关注我的公众号

\"640.png?\"

\"一起来动手做个

ItVuer - 免责声明 - 关于我们 - 联系我们

本网站信息来源于互联网,如有侵权请联系:561261067@qq.com

桂ICP备16001015号