[置顶] AS插件开发:根据特定格式的文本自动生成Java Bean文件或字段
2018-02-09 11:25
656 查看
本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布
前后端根据需求讨论接口契约协议 ——> 后端发布契约 ——> 前后端各自按照契约编码 ——> 后端发布正式服务 ——> 前端调试接口
在讨论契约的过程中会产生很多新的字段、甚至是新的实体,前端要根据这些新字段、实体,原封不动的复制粘贴生成契约Java bean类,这项工作十分枯燥乏味!
作为一个程序猿,秉着能不重复我就偷懒的原则,就开始寻找满足我需求的AS插件,但是市场上大都是根据Json生成Java bean的插件(也可能是我搜的姿势不对……),一怒之下,就根据我们的契约格式撸了一发插件,同时也再练习一下插件开发的流程。
该插件已经上传市场:
File –> Settings –> Plugins –> Browse repositories…
先看看追加字段:
接着是新建实体:
这里需要注意,只有满足下面的契约格式,本插件才能正常工作。如果格式有异,我预留了接口,可以自己实现自家的格式解析。
契约格式:
或者省略注释:
下面开始进入开发正题,如果还有不清楚如何使用Intellij开发AS插件的同学,请左转先看:
Android Studio插件开发入门篇
下面详细讲解下两种模式,我会重点介绍追加字段,因为新建实体,本质上就是新建空文件然后再追加字段。
有一个面板用来粘贴契约文本,有两个按钮用来选择成员变量的类型,有一个选择:是否自动生成“serialVersionUID”。
下面新建一个对话框:
系统会自动生成对应的文件:
这里的form文件就相当于Android的Xml文件,需要什么控件就直接拖拽,对照我的草图,结构是这样:
由于对这些控件不熟悉,所以属性什么的只能一点一点摸索,不过整体上感觉和Android类似,没什么难度。
有了可视化界面,我们接着就要写事件监听,无非就是一些按钮的点击事件,和Android类似,这里不表,大家可参考源码。
最后在Action中弹出对话框:
现在我们跑一下代码,看看效果:
跟我们预想的效果一样!现在架子已经搭好了,下面开始我们的表演~
契约长这样:
实体类要长这样:
我们首先要把大段文本按行整理成小元组,这里按换行符
当然可能不同公司有不同的契约模板,这里我把解析和拼接字符串的过程抽成了接口,大家可以自行实现逻辑。
生成代码的过程都使用接口:
我们只需要定义自己的ICodeGenerator就可以了,这里默认内置的是适合我们项目契约模板的生成器,字符串解析过程长这样:
我这里着重处理了多行注释的场景:
这时候,如果不作处理,生成的代码是这样:
所以每当遇到字符串元组只有一个元素时,就认为是遇到了多行注释的下一行,就把它拼接到上一行注释后面,最终经过拼接就可以得到正确结果:
默认生成器中的拼接过程长这样:
自己实现拼接字符串的过程,这中间没什么技术含量,就是按照格式,依次拼接注释、成员类型、变量类型、变量名,唯一需要注意的就是处理一下特殊情况:没有注释、变量类型可能不是标准的Java类型等。
因为我们的契约没有更标准的格式,我只是根据位置粗略的判断谁是注释、谁是变量名等。所以如果契约中缺失了关键字段,比如变量类型,那生成的代码肯定也是不标准的,这也没办法。如果契约格式能更标准化的话,解析过程就可以写的优雅很多。
这一块应该是整篇的核心,难也不难,主要是要熟悉系统API。网上资料不多,最快的方法就是找一个类似的自动生成插件的源码,照葫芦画瓢,这也是我们学习新知识时经常用的技巧。
下面我们一点一点捋一下。
首先我们需要获取当前编辑的文件:
模拟写代码可以调用这个方法:
根据文本生成代码片:
最后把所有代码片交给目标类:
值得注意的是:我们在开发插件过程中,一定要注意处理错误,至少要知道是什么错,如果不处理,系统就直接卡死没反应,这样连改进都会无处下手。
所以我把生成代码的核心过程
错误框长这样:
到此,大功可以告成也~ 接着我们看看如何根据文本直接生成新实体。
由于是新建实体,我把action放在了NewGroup中,配置文件长这样:
对应效果:
值得一提是,如何找到
很惭愧,并没有找到帮助资料,我是一行一行自行分析的,新建文件嘛,应该不是
如果大家有这一块的资料,十分欢迎评论告知。
我们继续正题,下面重点介绍如何使用模板新建文件。
首先我们在
我的模板文件长这样:
这个模板相对简单,都能理解。有了模板文件,下面我们开始敲代码。
在所选的工程路径下新建文件:
到此,我们就可以新建出一个空文件啦。
值得注意的是,
一般都是没找到模板文件的缘故,请检查文件目录是否是
其中
这里需要注意的是,我们拿到新生成的
最后我们整体感觉一下,整个流程并不麻烦。自认为吧,AS插件的开发最重要的还是创意,我们在平时开发的过程中,肯定会遇到各种各样的痛点,AS插件可以很好的帮我们解决那些“机械性重复”的过程。只要有心,我们完全可以让敲代码变成一项轻松、炫酷的事情~~
最后,工程源码在此:Android-EasyJavaBean-Plugin
欢迎大家star、issue~
专门放简化日常工作的AS插件,本文插件已经收录。有些插件可能市场上已经有了,但是还是想自己动手操作,既放心又舒心~这里先预告一下:
自动生成equals hashCode
像AS中,.if生成if代码块一样,通过.onclick生成setOnClickListener代码块
感兴趣的可以star一下,或者有其他idea的,可以一起交流一下~
为什么要造轮子
在项目中,产品提出了新需求,开发们的开发流程一般是这样:前后端根据需求讨论接口契约协议 ——> 后端发布契约 ——> 前后端各自按照契约编码 ——> 后端发布正式服务 ——> 前端调试接口
在讨论契约的过程中会产生很多新的字段、甚至是新的实体,前端要根据这些新字段、实体,原封不动的复制粘贴生成契约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 |
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再切割,得到每行的有效文本。
当然可能不同公司有不同的契约模板,这里我把解析和拼接字符串的过程抽成了接口,大家可以自行实现逻辑。
public interface ICodeGenerator { /** * 解析字符串 * @param str 粘贴的字符串 * @return 各行字符串 */ List<List<String>> onParse(String str); /** * @param fields 格式化后的,各行的文本元组 * @param project 当前工程 * @param psiClass 当前类 * @param memberType 成员变量类型 */ void onSplice(List<List<String>> 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<List<String>> onParse(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> 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<String> 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<List<String>> fields, Project project, PsiClass psiClass, String memberType) { if (psiClass == null) { return; } PsiElementFactory factory = JavaPsiFacade.getInstance(project).getElementFactory(); for (List<String> 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<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(spliceHelper.onParse(pastedStr), 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()); }
错误框长这样:
到此,大功可以告成也~ 接着我们看看如何根据文本直接生成新实体。
根据文本新建实体
这个过程跟追加字段的流程大致一样,唯一不同的是,我们首先需要在工程目录下新建一个文件,然后再在文件中执行上面介绍的“追加字段”操作。由于是新建实体,我把action放在了NewGroup中,配置文件长这样:
<action id="GenerateJavaBeanBySting" class="actions.generateJavaBean.GenerateJavaBeanAction" text="New Java Bean File By String" description="Generate JavaBean File By String" icon="/icons/ic_logo.png"> <add-to-group group-id="NewGroup" anchor="first"/> </action>
对应效果:
值得一提是,如何找到
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<String, String> 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<List<String>> 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
欢迎大家star、issue~
后记
近期重新开了一个仓库:Android-EasyCodePlugins-master专门放简化日常工作的AS插件,本文插件已经收录。有些插件可能市场上已经有了,但是还是想自己动手操作,既放心又舒心~这里先预告一下:
自动生成equals hashCode
像AS中,.if生成if代码块一样,通过.onclick生成setOnClickListener代码块
感兴趣的可以star一下,或者有其他idea的,可以一起交流一下~
相关文章推荐
- [置顶] iOS 高效开发之,自动生成数据模型文件
- 类似Lazy Android的插件,根据layout的xml文件自动生成findViewById代码
- [jQuery插件开发][dynamicTable2.0]根据JSON数据自动生成HTML Table
- 使用maven根据JSON文件自动生成Java POJO类(Java Bean)源文件
- 使用jsonschema2pojo根据JSON文件自动生成Java POJO类(Java Bean)源文件
- hibernate 自动生成数据库(根据hibernate配置文件)
- 怎么根据linux c/c++ 开发的d-bus服务生成java 的调用接口文件
- 开发一款自动指向特定页面元素的jQuery插件:jQuery PointPoint
- Silverlight4-RIAServices开发记事1-自动生成的web.config配置文件
- properties文件自动存盘为Unicode格式(Eclipse插件)
- 使用Hibernate-tools中的hbm2java和hbm2ddl根据hbm文件自动生成pojo和数据库脚本
- (iPhone/iPad开发)根据文本的字数自动调整UILabel的宽高
- PHP FOR MYSQL 代码生成助手(根据Mysql里的字段自动生成类文件的)
- matlab处理txt文本文件---数据格式要有规律性,否则要用编写特定方式进行读取
- jquery 左侧多级菜单 根据xml文件自动生成
- 开发一款自动指向特定页面元素的jQuery插件:jQuery PointPoint
- C++根据头文件自动生成实现文件框架(支持模版)
- eclipse中的Java文件自动根据svn版本号生成注释
- 使用Hibernate-tools中的hbm2java和hbm2ddl根据hbm文件自动生成pojo和数据库脚本
- 根据.wsdl文件,自动生成webservice的调用客户端