Protobuf不易用
关于Protobuf协议的介绍可以参看我之前的日志一篇搞定Java序列化 中的protobuf篇
相对于其他数据格式如json、xml, protobuf有着数据量小,解析快的显著优势
但是这意味着开发者需要写proto文件,需要了解proto message的语法,并且目前IDE还不支持proto的智能提示,十分不便
基于此,我开发了一个ProtoUtils工程
(先放上GitHub地址)
ProtoUtils的用途
ProtoUtils是一个Java工程,集成了写proto文件、proto转化为java类的功能
开发者不需要关注proto的写法,而是直接参照json的方式写java数据类,拷贝到这个工程里
然后配置好输出参数,运行后可以直接获得对接proto协议的java类,再拷贝回你希望使用的工程里
ProtoUtils的用法
- 把想要转化的java原生bean拷贝到这个工程
- 配置ProtoUtils.main方法
1 2 3 4 5 6 7 8
| public static void main(String[] args) throws IOException { String outerName = "ProtoBean"; String packageName = "com.efan.proto"; genProtoFile(packageName, outerName, ArrayProto.class, TestProto.class); String outputPath = packageName.replace(".", File.separator); squareCompile(outputPath, outerName); }
|
- 右键Run这个方法,会弹出cmd
- 转化成功按任意键会弹出最终java类所在的文件夹
- 将java类拷贝到你希望使用的工程
ProtoUtils的具体实现原理
大致思路是,递归地遍历java数据类,通过反射取得java数据类需要转化的变量名、变量类型以及其他信息
为不同的变量类型指定对应的写proto代码的方法,把proto代码输出到文本,再启动cmd命令,调用对应的jar包转化proto代码为java类
Talk is cheap,直接上code
递归遍历java数据类,通过反射获取变量信息
非枚举类将会写成一个message结构
tab方法是控制代码缩进的
static和transient变量是根据proto协议过滤掉的
而java语言特性使得内部类自动持有外部类的引用,这个外部类引用也要过滤掉
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| * 非枚举类的转化 * @param beanClass 类 * @param step 第几层嵌套 */ private static String cls2Proto(Class<?> beanClass, int step){ StringBuilder sb = new StringBuilder(); tab(sb, step); sb.append("message ").append(beanClass.getSimpleName()).append(" {\n\n"); Class<?>[] classes = beanClass.getDeclaredClasses(); for (Class<?> cls: classes) { String string = cls.isEnum() ? enum2Proto(cls, step + 1) : cls2Proto(cls, step + 1); sb.append(string); } Field[] fields = beanClass.getDeclaredFields(); TagRecorder tagRecorder = new TagRecorder(); for (Field field : fields){ if (!isTransientOrStatic(field) && !isEnclosing(field, beanClass)){ String str = field2Proto(field, tagRecorder); tab(sb, step + 1); sb.append(str).append("\n"); } } tab(sb, step); sb.append("}\n\n"); return sb.toString(); }
|
变量的转化
列表和数组类型用repeated修饰,普通变量用required或optional修饰
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| * 变量转化 * @param field 变量 * @param tagRecorder 标记记录器 */ private static String field2Proto(Field field, TagRecorder tagRecorder){ Class<?> cls = field.getType(); StringBuilder sb = new StringBuilder(); if (List.class.isAssignableFrom(cls)){ sb.append("repeated ").append(list2Proto(field)); } else if (isArray(cls)){ sb.append("repeated ").append(array2Proto(field)); } else { sb.append(checkRequired(field)).append(getTypeString(cls)); } int tagNumber = tagRecorder.tag(getTagNumber(field)); sb.append(" ").append(field.getName()).append(" = ").append(tagNumber).append(";\n"); return sb.toString(); }
|
然后是判断变量的类型,基本类型各有对应的proto数据类型,注意byte数组有对应的bytes类型
非基本类型直接取类名
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| private static String getTypeString(Class<?> cls){ if (cls.isAssignableFrom(Double.class) || cls.isAssignableFrom(double.class)) { return "double"; } else if (cls.isAssignableFrom(Float.class) || cls.isAssignableFrom(float.class)) { return "float"; } else if (cls.isAssignableFrom(Long.class) || cls.isAssignableFrom(long.class)) { return "int64"; } else if (cls.isAssignableFrom(Integer.class) || cls.isAssignableFrom(int.class)) { return "int32"; } else if (cls.isAssignableFrom(Boolean.class) || cls.isAssignableFrom(boolean.class)) { return "bool"; } else if (cls.isAssignableFrom(String.class)) { return "string"; } else if (cls.isAssignableFrom(byte[].class)) { return "bytes"; } else { return cls.getSimpleName(); } }
|
proto数据代码有一个tag值的概念,是用来在二进制编码里唯一确认当前域的
可以由开发者自定义,默认情况变量从1开始增加,枚举从0开始增加
TagRecorder内部保存一个map,tag方法用于检索可用的tag值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| private static class TagRecorder { private int offset = 1; private SparseArrayCompat<Boolean> record = new SparseArrayCompat<>(); * 设置标记值 * @return 最终的标记值 */ int tag(int num) { if (num <= 0) { while (record.get(offset, false) && offset < Integer.MAX_VALUE) { offset++; } if (offset == Integer.MAX_VALUE) { throw new IllegalStateException("tag reaches MAX Integer"); } record.put(offset, true); return offset; } else if (record.get(num, false)) { throw new IllegalStateException("tag is duplicated"); } else { record.put(num, true); return num; } } }
|
枚举类型的转化
java枚举类的变量当中有枚举常量数组,需要过滤掉
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| * 枚举类的转化 * @param beanClass 枚举类 * @param step 第几层嵌套 */ private static String enum2Proto(Class<?> beanClass, int step){ StringBuilder sb = new StringBuilder(); tab(sb, step); sb.append("enum ").append(beanClass.getSimpleName()).append(" {\n"); Field[] fields = beanClass.getDeclaredFields(); int tagNum = 0; for (Field field : fields){ if (field.isEnumConstant()){ tab(sb, step + 1); sb.append(field.getName()).append(" = ").append(tagNum).append(";\n"); tagNum ++; } } tab(sb, step); sb.append("}\n\n"); return sb.toString(); }
|
生成proto文件
head方法是编写proto的头部属性
convert方法调用的就是前面的cls2Proto
1 2 3 4 5 6 7 8 9 10 11 12 13
| * 生成Proto文件 * @param outerName 输出文件名 * @param classes 需要转化的java类 */ private static void genProtoFile(String packageName, String outerName, Class<?>... classes) { StringBuilder sb = new StringBuilder(); sb.append(Java2Proto.head(packageName, outerName)); for (Class<?> clazz : classes){ sb.append(Java2Proto.convert(clazz)); } IOUtils.write(DIR_WORKSPACE + outerName + ".proto", sb.toString()); }
|
执行cmd转化
最后是通过cmd调用谷歌转化工具或者Square Wire工具把proto文件转化为Java类
执行cmd命令是把命令数组写成bat文件,然后调用Java的Runtime执行bat
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| * 采用Square工具转Java * @throws IOException */ private static void squareCompile(String outputPath, String outerName) throws IOException { runCmd("cd ./proto", "java -jar -Dfile.encoding=UTF-8 wire-compiler-2.2.0-jar-with-dependencies.jar " + "--proto_path=. --java_out=./ "+ outerName + ".proto", "pause", "start" + "." + File.separator + outputPath, "exit"); } * 采用谷歌工具转java * @throws IOException */ private static void googleCompile(String outputPath, String outerName) throws IOException { runCmd("cd ./proto", "protoc-java.exe --java_out=./ " + outerName + ".proto", "pause", "start" + "." + File.separator + outputPath, "exit"); } * 多条命令生成bat并执行 * @param commands 命令数组 * @throws IOException */ private static void runCmd(String... commands) throws IOException { File file = new File(DIR_WORKSPACE + "command.bat"); BufferedWriter writer = new BufferedWriter(new FileWriter(file)); for (String command : commands) { writer.write(command); writer.newLine(); } writer.close(); Runtime.getRuntime().exec("cmd /c start " + file.getAbsolutePath()); }
|
结语
回头看ProtoUtils把写proto文件的流程自动化了,
开发者只需要像写json一样写好java数据类,然后跑一次这个工程,就能得到支持proto协议的java类
实际使用时我推荐Square的方式,因为谷歌的方式生成了太多太多太多(真的多所以说三遍)不必要的变量和方法,
而Android开发对于方法数量非常敏感,一不留神就会触发65535上限。
此外,目前我写的这个工程
- 只支持基本的proto2
- 不支持二维数组,不支持Map,
- 不支持char和char数组
- 不支持import x.proto
- 不支持default(与proto3一致)
- 枚举值一定从0开始(与proto3一致)
乍一看很多功能缺失,实际上平时应用写绝大部分是普通数据类,完全够用了
(其实是因为这些特性开发起来有点麻烦,等到确实需要的时候再动吧哈哈哈)
最后
放出这个工程的GitHub地址
欢迎大家讨论和优化代码!