背景
proto來定義和后臺通信的數(shù)據(jù)模型,并且很多地方使用到了proto的枚舉(enum),但是這個枚舉的向前兼容性不太好。例如
消息類型定義為:
message CCCBean{
BEnum b = 1;
}
低版本里面的枚舉只有
enum BEnum {
a = 0; b = 1;
}
但是隨著業(yè)務發(fā)展高版本新增了c=2 和 d=3 的類型
enum BEnum {
a = 0; b = 1; c = 2;d = 3;
}
這個時候后臺下發(fā)的數(shù)據(jù)里面帶有BEnum = 3的情況的時候,就拿不到正確的枚舉類型蜈首,切使用以下方式會拋出異常
例如我們模擬后臺返回,枚舉為3
val json = "{\"b\":3}"
val newJsonBuilder = TestPbMutile.CCCBean.newBuilder()
JsonFormat.parser().merge(json, newJsonBuilder)
val jsonPb = newJsonBuilder.build()
val value = jsonPb.bValue // 這種方式不會報錯
val b = jsonPb.b
val enumB = jsonPb.b.number // 這一行會報錯
總結以上分別兩種取值方式
1、
jsonPb.bValue
2、
jsonPb.b.number
第一種不會報錯,第二種會拋出異常
由此可以思考出以下解決方案判耕,不調(diào)用 proto生成的java類的getEnum().number 方法
其中有思考方案如下
1.添加注解,標注廢棄翘骂,在項目組內(nèi)同步宣講壁熄,避免大家踩坑
2.移除getEnum()方法
2.將getEnum()方法返回值從Enum類型改為int類型
經(jīng)過思考得出
方案1在多人協(xié)同開發(fā)中沒辦法完全避免,總會有其他同學踩坑
方案2會讓pb.toString()的時候會拋出異常 碳竟,如下
最終實現(xiàn)方案3请毛,將getEnum返回類型改為int
接下來方案3是實現(xiàn)流程
這里是jar腳本大致邏輯
介紹一下jar腳本中使用的框架
1、JavaParser 可以解析java文件瞭亮,可以獲取其中的類,方法固棚,成員變量等等统翩,也可以方便的添加刪除修改代碼,并且方便的覆蓋掉原文件
依賴
implementation 'com.github.javaparser:javaparser-core:3.23.1'
所用主要功能
a此洲、解析java文件內(nèi)所有class,并輸出名字
// ...
Path path = Paths.get(pbFileName);
CompilationUnit outCu = StaticJavaParser.parse(path);
List<String> allClassname = ClassNameExtractor.extractFullClassNames(outCu);
// ...
public static List<String> extractFullClassNames(CompilationUnit cu) throws IOException, ParseException {
List<String> fullClassNames = new ArrayList<>();
VoidVisitorAdapter<List<String>> classNameCollector = new VoidVisitorAdapter<List<String>>() {
@Override
public void visit(ClassOrInterfaceDeclaration n, List<String> arg) {
super.visit(n, arg);
arg.add(getFullClassName(n));
}
@Override
public void visit(EnumDeclaration n, List<String> arg) {
super.visit(n, arg);
arg.add(getFullClassName(n));
}
private String getFullClassName(TypeDeclaration<?> n) {
String packageName = cu.getPackageDeclaration()
.map(PackageDeclaration::getNameAsString)
.orElse("");
String oriClassName = n.getFullyQualifiedName()
.orElse(n.getNameAsString());
oriClassName = oriClassName.replace(".", "$");
if (!packageName.isEmpty()) {
oriClassName = packageName + "." + oriClassName.substring(packageName.length() + 1);
}
return oriClassName;
}
};
classNameCollector.visit(cu, fullClassNames);
return fullClassNames;
}
b厂汗、遍歷類里面的method,找出返回值為枚舉的getXXXEnum方法呜师。將其返回值設置為int娶桦,注意這里要注意oneOf關鍵字也會生成Enum需要被過濾掉,看源碼發(fā)現(xiàn)oneOf生成的枚舉實現(xiàn)于com.google.protobuf.Internal.EnumLite汁汗,而 filed為枚舉生成的枚舉實現(xiàn)于com.google.protobuf.ProtocolMessageEnum衷畦,可以用于區(qū)別
for (Method method : clazz.getMethods()) {
// 將返回值是枚舉類型的方法并且是getXXXEnum方法 改變返回值為int
if (method.getReturnType().isEnum()&&method.getName().startsWith("get")) {
// ...
// 注意,這里過濾oneOf的情況
boolean needJump = false;
for (Class<?> anInterface : method.getReturnType().getInterfaces()) {
if(enumLiteClass.getName().equals(anInterface.getName())){
needJump = true;
break;
}
}
if (needJump){
continue;
}
// 標記這個方法知牌,需要被修改 F碚!=谴纭菩混!
// ...
}
}
c、對method修改扁藕,并且覆蓋寫入原文件
for (MethodDeclaration method : classOrInterface.getMethods()) {
// ...
// 匹配到需要被修改的方法
// 將返回枚舉的方法沮峡。變成返回int
method.setType(int.class);
// 修改其方法體內(nèi)容
method.removeBody();
Statement statement = StaticJavaParser.parseStatement("return " + method.getNameAsString() + "Value();");
BlockStmt blockStmt = new BlockStmt();
blockStmt.addStatement(statement);
method.setBody(blockStmt);
// 處理注釋 和 添加注釋
String oldCommentStr = "";
Comment oldComment = method.getComment().orElse(null);
if (oldComment != null) {
oldCommentStr = oldComment.toString()
.replaceAll("/\\*\\*", "")
.replaceAll("/\\*", "")
.replaceAll("\\*", "")
.replaceAll("/", "")
.replaceAll("\\n", "")
.replaceAll("\\*/", "");
}
method.setBlockComment("* \n\t\t* " + oldCommentStr + " \n\t\t* old return type is " + oldReturnType + " now change return type to int \n\t\t");
// ...
}
// 覆蓋寫入文件
Files.write(path, outCu.toString().getBytes());
2、Compiler可以將java代碼編譯成class文件亿柑⌒细恚可以讓其被classLoader所解析。然后就可以通過class相關api獲取類描述信息
依賴
implementation 'org.codehaus.groovy:groovy-eclipse-compiler:3.6.0-03'
主要功能
a、將java文件編譯為class文件秘症,并輸出到對應目錄
// ...
DynamicCompiler.compile(pbFileName, compilerPath);
// ...
public static void compile(String sourceFilePath, String outputDirectoryPath) {
File sourceFile = new File(sourceFilePath);
File outputDirectory = new File(outputDirectoryPath);
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
try {
fileManager.setLocation(StandardLocation.CLASS_OUTPUT, Arrays.asList(outputDirectory));
} catch (IOException e) {
e.printStackTrace();
}
Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjectsFromFiles(Arrays.asList(sourceFile));
JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, null, null, null, compilationUnits);
task.call();
try {
fileManager.close();
} catch (IOException e) {
e.printStackTrace();
}
}
接下來對比執(zhí)行了jar腳本之后pb生成的java類,生成類代碼太多了照卦,此處就不貼代碼了,只羅列出修改點
注意乡摹,一個pb的message生成類會生成三個java類役耕,接下來我舉例說明
pb文件名為testPb.proto,內(nèi)容為
message TestBean{
int64 id = 1;
Sex sex = 2;
}
enum Sex {
man = 0;
female = 1;
}
以下為Java偽代碼,主要是解釋生成類的關系
public final class TestPb {
public interface TestBeanOrBuilder extends
com.google.protobuf.MessageOrBuilder {
// <code>int64 id = 1;</code>
long getId();
// <code>.com.pb.test.Sex sex = 4;</code>
int getSexValue();
// <code>.com.pb.test.Sex sex = 4;</code>
Sex getSex();
}
public static final class TestBean extends
com.google.protobuf.GeneratedMessageV3 implements
TestBeanOrBuilder {
// ...
public static final class Builder extends
com.google.protobuf.GeneratedMessageV3.Builder<Builder> implements
com.pb.test.TestPb.TestBeanOrBuilder {
// ...
// ...
}
// ...
}
}
最外層類名為pb文件名聪廉,其中一個message就對應一個TestBeanOrBuilder接口 和一個 Bean類瞬痘,且這個Bean類里面有一個內(nèi)部類Builder類
以下三個類分別簡稱為 數(shù)據(jù)接口、Bean類板熊,BeanBuilder類
1框全、原類會返回帶枚舉的值,新類返回值會變成int (包括 數(shù)據(jù)接口干签、Bean類津辩,BeanBuilder類)
原類
/**
* <code>.com.pb.test.Sex sex = 4;</code>
*/
public com.pb.test.TestPb.Sex getSex() {
com.pb.test.TestPb.Sex result = com.pb.test.TestPb.Sex.valueOf(sex_);
return result == null ? com.pb.test.TestPb.Sex.UNRECOGNIZED : result;
}
修改后的
// <code>.com.pb.test.Sex sex = 4;<code>
// old return type is com.pb.test.TestPb.Sex now change return type to int
public int getSex() {
return sex_;
}
2、BeanBuilder類添加 setEnum方法
原類
public static final class TestBean extends
com.google.protobuf.GeneratedMessageV3 implements
TestBeanOrBuilder {
// <code>.com.pb.test.Sex sex = 4;</code>
public Builder setSexValue(int value) {
sex_ = value;
onChanged();
return this;
}
// <code>.com.pb.test.Sex sex = 4;</code>
public Builder setSex(com.pb.test.TestPb.Sex value) {
if (value == null) {
throw new NullPointerException();
}
sex_ = value.getNumber();
onChanged();
return this;
}
}
修改類
public static final class TestBean extends
com.google.protobuf.GeneratedMessageV3 implements
TestBeanOrBuilder {
// <code>.com.pb.test.Sex sex = 4;</code>
public Builder setSexValue(int value) {
sex_ = value;
onChanged();
return this;
}
// <code>.com.pb.test.Sex sex = 4;</code>
public Builder setSex(com.pb.test.TestPb.Sex value) {
if (value == null) {
throw new NullPointerException();
}
sex_ = value.getNumber();
onChanged();
return this;
}
// 新增方法
/* 為了兼容 將getEnum方法返回的enum的類型 改成了int容劳,所以需要在build方法中添加對應的set方法*/
public Builder setSex(int value) {
sex_ = value;
onChanged();
return this;
}
}
3喘沿、在靜態(tài)代碼塊中 遍歷所有filde找到類型為Enum的所有filde,他他們的類型改為int竭贩,并且設置defalutValue為0
static{
//...
//這一句就是初始化 getDescriptor()
Descriptors.FileDescriptor.internalBuildGeneratedFileFrom(descriptorData, new Descriptors.FileDescriptor[] {}, assigner);
//...
}
修改后的類
static{
//...
//這一句就是初始化 getDescriptor()
Descriptors.FileDescriptor.internalBuildGeneratedFileFrom(descriptorData, new Descriptors.FileDescriptor[] {}, assigner);
for (Descriptors.Descriptor messageType : getDescriptor().getMessageTypes()) {
for (Descriptors.FieldDescriptor field : messageType.getFields()) {
if (field.getType() == Descriptors.FieldDescriptor.Type.ENUM) {
Class fieldClass = field.getClass();
try {
Field typeField = fieldClass.getDeclaredField("type");
typeField.setAccessible(true);
typeField.set(field, Descriptors.FieldDescriptor.Type.INT32);
Field defaultValueField = fieldClass.getDeclaredField("defaultValue");
defaultValueField.setAccessible(true);
defaultValueField.set(field, 0);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
//...
}
被修改的生成pb類蚜印,在運行過程中的表現(xiàn)
測試Pb類
message PbA{
int64 id = 1;
string name = 2;
repeated string job = 3;
Sex sex = 4;
Sex sex_b_type = 5;
Sex sex_c_type = 6;
TypeA type = 7;
enum TypeA {
type1 = 0;
type2 = 1;
type3 = 2;
}
}
enum Sex {
man = 0;
female = 1;
}
1、pb使用builder初始化和toString()方法留量,以及取值
2窄赋、pb和bytes相互轉(zhuǎn)換
3、pb和json相互轉(zhuǎn)換
可以看出來楼熄,修改之后的pb生成類忆绰,也可以滿足日常開發(fā)中的業(yè)務功能,但是由于PB庫很多使用了反射機制來訪問可岂。所以不排除有一些極少出情況的坑點较木。如果碰到了歡迎大家留言。
另外如果大家想要看源碼可以去github獲取源碼我是傳送門
主要邏輯都在proto和protoCusEnumCompiler兩個模塊里面
protoCusEnumCompiler負責生成修改pb生成類的jar邏輯
proto 則是存放pb文件和生成pb文件