为什么要造轮子
在项目中, 产品提出了新需求, 开发们的开发流程一般是这样:
前后端根据需求讨论接口契约协议 > 后端发布契约 > 前后端各自按照契约编码 > 后端发布正式服务 > 前端调试接口
在讨论契约的过程中会产生很多新的字段甚至是新的实体, 前端要根据这些新字段实体, 原封不动的复制粘贴生成 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()
加了 trycatch, 在主程序中把错误信息直接以对话框的形式弹出:
- if (!TextUtils.isEmpty(result) && !result.equalsIgnoreCase("success")) {
- Messages.showMessageDialog(result, "Error", Messages.getInformationIcon());
- }
错误框长这样:
到此, 大功可以告成也~
最后我们整体感觉一下, 整个流程并不麻烦自认为吧, AS 插件的开发最重要的还是创意, 我们在平时开发的过程中, 肯定会遇到各种各样的痛点, AS 插件可以很好的帮我们解决那些机械性重复的过程只要有心, 我们完全可以让敲代码变成一项轻松炫酷的事情~~
最后, 工程源码在此: android-GenerateModel-plugin-AS
欢迎大家 starissue~
预告
近期还准备重新开一个仓库, 专门放简化日常工作的 AS 插件, 有些插件可能市场上已经有了, 但是还是想自己动手操作, 既放心又舒心~ 这里先预告一下:
本文插件 2.0: 填写类名和字段文本生成类文件
自动生成 equals hashCode
像 AS 中,.if 生成 if 代码块一样, 通过. onclick 生成 setOnClickListener 代码块
感兴趣的可以 star 一下, 或者有其他 idea 的, 可以一起交流一下~
来源: http://blog.csdn.net/qq_27258799/article/details/79295251