前言
在之前的博客中,我们已经尝试过了在C#环境下,使用Protobuf来序列化Excel表格,然后再代码中反序列化出表格数据【Unity3D —— protobuf 导excel表格数据】,如今我们在服务器假如要共用一套配置表格,当需要一个类似的导表和读表过程,接下来我就在原来博客的基础上稍加修改,完成这个需求。
准备工作:
为了方便起见,我直接复制了之前的一些工具,假如不了解的也可以直接在此处下载资源包:Protobuf导Excel表C#版,然后跟着我的步骤来进行修改。
修改导表过程:
在之前我们的导表脚本中,例如:
::---------------------------------------------------
::第一步,将xls经过xls_deploy_tool转成data和proto
::---------------------------------------------------
call python xls_deploy_tool.py PERSON xls/person.xls
::---------------------------------------------------
::第二步:把proto翻译成cs
::---------------------------------------------------
call protoc tnt_deploy_person.proto --descriptor_set_out=person.protodesc
call ProtoGen\protogen -i:person.protodesc -o:person.cs
pause
上面是之前C#版本导表的过程,在将.proto
转成.cs
之前,先转为.protodesc
格式的中间状态,然后才能得到最终的.cs
脚本。而在java版本中,我们可以跳过中间状态的,直接将.proto
转化为.java
的JavaBean
脚本,修改之后如下:
::---------------------------------------------------
::第一步,将xls经过xls_deploy_tool转成data和proto
::---------------------------------------------------
call python xls_deploy_tool.py PERSON xls/person.xls
::---------------------------------------------------
::第二步:把proto翻译成java
::---------------------------------------------------
call protoc --java_out=java/ tnt_deploy_person.proto
::---------------------------------------------------
::第三步:清除中间文件
::---------------------------------------------------
@echo off
echo TRY TO DELETE TEMP FILES:
del *_pb2.py
del *_pb2.pyc
del *.proto
del *.data
del *.log
del *.txt
::---------------------------------------------------
::第四步,关闭窗口
::---------------------------------------------------
@echo on
因为省去了一个步骤,文件夹中ProtoGen文件目录下的工具我们也用不到了,可以直接删掉,并且这里我们也加入了中间文件的清理指令,我们还可以在批处理脚本中将文件直接拷贝到我们项目指定目录下。
改造表格思路:
为了方便策划进行表格的管理和修改,通常在开发过程中,客户端和服务器使用的是同一套表格,但是这就不免会有冗余的数据,因为有些数据列是客户端使用而对于服务端无用的。
所以,为了减少这些冗余数量,我们可以在表格的第一行加入一个标签,标志三种状态:C
->客户端用,S
->服务端用,CS
->客户端和服务端都用到,如此,前端和后端都只导出需要用到的数据属性列,而跳过不用的列,表格修改如下:
为此对xls_deploy_tool.py
这个导表的脚本也需要进行相应的修改,而且对于本在.xls表格中,数据页的命名只支持大写字母这一点,也需要进行优化改造,由于修改量还是比较大,这里不做细说,只是提供此优化思路。
优化脚本:
为了简化脚本内容,我们可以为每个表格创建一个.bat批处理脚本,但里面只是用来指定当前表格的名称
,要导出数据的数据Sheet名称
这两项信息,而真正的导表操作交给一个通用的脚本xlss_java.bat
去处理,这里通用脚本修改后如下:
@echo off
::表格名称
set XLS_NAME=%1
::数据页名称
set SHEET_NAME=%2
::目标文件夹名称
set DATA_DEST=%3
::表格所在目录
set STEP1_XLS2PROTO_PATH = xls
::当前正在导的表格名称
echo.
echo =========Compilation of %XLS_NAME%.xls=========
@echo on
cd %STEP1_XLS2PROTO_PATH%
::---------------------------------------------------
::第一步,将xls经过xls_deploy_tool转成data和proto
::---------------------------------------------------
call python xls_deploy_tool.py %SHEET_NAME% xls\%XLS_NAME%.xls s
::---------------------------------------------------
::第二步:把proto翻译成java
::---------------------------------------------------
::生成表格列表
dir .\*.proto /b > protolist.txt
@echo on
for /f "delims=." %%i in (protolist.txt) do protoc --java_out=. dataconfig_%SHEET_NAME%.proto
pause
::---------------------------------------------------
::第三步:把data和java复制到指定目录
::---------------------------------------------------
@echo off
set OUT_PATH=..\res\
set DATA_DEST=com\tw\login\protobufdata
set JAVA_DEST=..\src\
copy .\dataconfig_%SHEET_NAME%.data %OUT_PATH%\%DATA_DEST%\dataconfig_%SHEET_NAME%.bytes
pause
::---------------------------------------------------
::第三步:清除中间文件
::---------------------------------------------------
@echo off
echo TRY TO DELETE TEMP FILES:
del *_pb2.py
del *_pb2.pyc
del *.proto
del *.data
del *.log
del *.txt
::---------------------------------------------------
::第四步,关闭窗口
::---------------------------------------------------
@echo on
而指定表格的脚本,例如person.xls表格的Person也需要进行导表:
set ExcelFileName=person
set ExcelSheetName_1=Person
call xlss_java.bat %ExcelFileName% %ExcelSheetName_1%
pause
如此每次添加一张新表,不必复制大量的命令行内容,只需简短的几行声明即可。
项目实战:
通过上述的步骤,我们得到了表格数据序列化之后的.bytes
二进制数据文件,还有对应每张表结构的JavaBean
解析类,那么接下来我们要做的步骤大概如下:
- 在java中读取二进制文件,获得byte[]字节数组;
- 创建一个序列化和反序列化的工具类,用来实现将
byte[]
反序列化为对应的结构化数据还有将protobuf数据序列化为byte[]字节流。
读取二进制文件:
首先,关于二进制文件的读取操作,网上检索一下发现有一大堆方法,首先,我们在当前项目的根目录下创建一个res
文件夹,把之前导表得到的所有.bytes
二进制文件都拷贝到此文件目录下:
创建一个读取二进制文件,并转化为二进制数组返回给调用函数:
package com.tw.login.tools;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
/**
* 此工具类用于二进制文件的读写
*
* @author linsh
*/
public class BinaryFile {
//读取res目录下的二进制文件
public static byte[] readResFile(String filePath){
return read("res/"+filePath+".bytes");
}
// 把二进制文件读入字节数组,如果没有内容,字节数组为null
public static byte[] read(String filePath) {
byte[] data = null;
try {
BufferedInputStream in = new BufferedInputStream(
new FileInputStream(filePath));
try {
data = new byte[in.available()];
in.read(data);
} finally {
in.close();
}
} catch (IOException e) {
e.printStackTrace();
}
return data;
}
// 把字节数组为写入二进制文件,数组为null时直接返回
public static void write(String filePath, byte[] data) {
if (data == null)
return;
try {
BufferedOutputStream out = new BufferedOutputStream(
new FileOutputStream(filePath));
try {
out.write(data);
} finally {
out.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
调用的方式很简单,例如这里我们要读取刚刚导入到res
目录下的dataconfig_Person.bytes
二进制文件:
byte[] bs = BinaryFile.readResFile("dataconfig_Person");
定义解析类:
关于解析类,其实就是将序列化后的表格数据,以二进制流读取,存在byte[]字节数组中,然后再讲数组中的数据转化为对应的protobuf数据结构类型。
protostuff插件:
看到网上很多基于protostuff
(这是一个基于protobuf开发的工具)定义一个自己的序列化和反序列化的工具类ProtoStuffSerializerUtil
,由于反序列化需要使用到protostuff的一下API,需要在pom.xml
中添加两个依赖,添加依赖时,假如不清楚如何定义版本和目录,可以在Maven资源网进行搜索:<dependency> <groupId>io.protostuff</groupId> <artifactId>protostuff-runtime-registry</artifactId> <version>1.5.3</version> </dependency> <dependency> <groupId>io.protostuff</groupId> <artifactId>protostuff-core</artifactId> <version>1.5.3</version> </dependency>
然而,在经过我的测试,返现这个类只对通过protobuf数据类在代码中创建的protobuf数据有效,但是对于通过表格定义然后导表所得到的表格数据,显然还存在着缺陷,特别是当前表格定义的是一行一条对应的结构数据,导出来的二进制文件解析出来是对应数据结构的列表,例如这里每行数据是一个
Person
数据,那么整个表导出来就是一个PersonArray
,但是使用ProtoStuffSerializerUtil
解析这种数据结构会出现解析报错。protobuf反序列化:
由于我们一般对于表格数据都是只读不写,所以这里我还是使用protobuf原生的反序列化方式来定义一个解析表格数据的函数,为了使其更为通用,我使用泛型来定义此函数,MessageLite
是所有protobuf数据对象的父类,弱化了传入数据的类型限制,达到通用的效果:package com.tw.login.protobufdata; import java.io.Serializable; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.MessageLite; /** * 表格二进制数据反序列化接口 * @author linsh * */ @SuppressWarnings("serial") public class ProtobufSerializerUtil implements Serializable { @SuppressWarnings({ "unchecked"}) public static <T> T deserialize(byte[] bs, MessageLite prototype) { MessageLite msg = null; try { msg = prototype.getDefaultInstanceForType().getParserForType().parseFrom(bs); } catch (InvalidProtocolBufferException e) { e.printStackTrace(); } return (T) msg; } }
结合上述读取二进制文件的接口,完整解析表格数据的调用方式如下:
PersonArray persons = ProtobufSerializerUtil.deserialize(BinaryFile.readResFile("dataconfig_Person"), PersonArray.getDefaultInstance());
总结优化:
假如有多张表格,我们可以在代码中构建一个管理表格配置数据的数据中心,在服务器启动时将表格都读取到内存中,表格数据反序列之后存在易用的数据结构中,这里我简单写了一个类似的工具类:
package com.tw.login.protobufdata;
import java.util.HashMap;
import java.util.Map;
import com.google.protobuf.MessageLite;
import com.tw.login.protobufdata.DataconfigPerson.Person;
import com.tw.login.protobufdata.DataconfigPerson.PersonArray;
import com.tw.login.tools.BinaryFile;
/**
* 表格读取到内存,表格配置数据中心
* @author linsh
*
*/
public class ProtobufDataConfigCenter {
private static ProtobufDataConfigCenter _Instance = null;
//数据.bytes文件与对应的解析类绑定
private static Map<String, MessageLite> file_map;
//数据与结构体绑定
private static Map<String, MessageLite> data_map;
//指定表的数据结构
private PersonArray persons;
/**
* 单例模式
* @return
*/
public static ProtobufDataConfigCenter Instance(){
if(_Instance == null){
_Instance = new ProtobufDataConfigCenter();
Initialize();
}
return _Instance;
}
/**
* 初始化
*/
private static void Initialize() {
data_map = new HashMap<String, MessageLite>();
file_map = new HashMap<String, MessageLite>();
//人物信息表
file_map.put("dataconfig_Person", PersonArray.getDefaultInstance());
LoadDataConfigs();
}
private static void LoadDataConfigs(){
for(String _key:file_map.keySet()){
MessageLite data = ProtobufSerializerUtil.deserialize(BinaryFile.readResFile(_key), _Instance.GetInstanceByFileName(_key));
data_map.put(_key, data);
}
}
/**
* 通过数据文件名称获取对应的解析类
* @param fileName
* @return
*/
public MessageLite GetInstanceByFileName(String fileName) {
return file_map.get(fileName);
}
/**
* 通过id获取玩家信息表的数据
* @param _id
* @return
*/
public Person GetPersonDataById(int _id) {
Person result = null;
if(data_map.containsKey("dataconfig_Person")){
persons = (PersonArray) data_map.get("dataconfig_Person");
for(int i=0;i<persons.getItemsCount();i++){
if(persons.getItems(i).getId() == _id){
return persons.getItems(i);
}
}
}
return result;
}
}
这里public Person GetPersonDataById(int _id)
是特定表格的外部调用接口,具体的导表和反序列化过程调用方无需了解,只需调用此接口即可得到想要的数据,测试一下:
Person person = ProtobufDataConfigCenter.Instance().GetPersonDataById(1);
logger.info("获取表格数据:----------------"+person.getUsername());
运行结果: