關于ProtoBuf枚舉向前兼容問題解決方案

背景

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 // 這一行會報錯
image.png

總結以上分別兩種取值方式
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()的時候會拋出異常 碳竟,如下

image.png

最終實現(xiàn)方案3请毛,將getEnum返回類型改為int

接下來方案3是實現(xiàn)流程

image.png

這里是jar腳本大致邏輯

image.png

介紹一下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()方法留量,以及取值

image.png

2窄赋、pb和bytes相互轉(zhuǎn)換

image.png

3、pb和json相互轉(zhuǎn)換

image.png

可以看出來楼熄,修改之后的pb生成類忆绰,也可以滿足日常開發(fā)中的業(yè)務功能,但是由于PB庫很多使用了反射機制來訪問可岂。所以不排除有一些極少出情況的坑點较木。如果碰到了歡迎大家留言。

另外如果大家想要看源碼可以去github獲取源碼我是傳送門
主要邏輯都在proto和protoCusEnumCompiler兩個模塊里面
protoCusEnumCompiler負責生成修改pb生成類的jar邏輯
proto 則是存放pb文件和生成pb文件

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末青柄,一起剝皮案震驚了整個濱河市伐债,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌致开,老刑警劉巖峰锁,帶你破解...
    沈念sama閱讀 216,544評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異双戳,居然都是意外死亡虹蒋,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來魄衅,“玉大人峭竣,你說我怎么就攤上這事』纬妫” “怎么了皆撩?”我有些...
    開封第一講書人閱讀 162,764評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長哲银。 經(jīng)常有香客問我扛吞,道長,這世上最難降的妖魔是什么荆责? 我笑而不...
    開封第一講書人閱讀 58,193評論 1 292
  • 正文 為了忘掉前任滥比,我火速辦了婚禮,結果婚禮上做院,老公的妹妹穿的比我還像新娘盲泛。我一直安慰自己,他們只是感情好键耕,可當我...
    茶點故事閱讀 67,216評論 6 388
  • 文/花漫 我一把揭開白布查乒。 她就那樣靜靜地躺著,像睡著了一般郁竟。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上由境,一...
    開封第一講書人閱讀 51,182評論 1 299
  • 那天棚亩,我揣著相機與錄音,去河邊找鬼虏杰。 笑死讥蟆,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的纺阔。 我是一名探鬼主播瘸彤,決...
    沈念sama閱讀 40,063評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼笛钝!你這毒婦竟也來了质况?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 38,917評論 0 274
  • 序言:老撾萬榮一對情侶失蹤玻靡,失蹤者是張志新(化名)和其女友劉穎结榄,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體囤捻,經(jīng)...
    沈念sama閱讀 45,329評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡臼朗,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,543評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片视哑。...
    茶點故事閱讀 39,722評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡绣否,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出挡毅,到底是詐尸還是另有隱情蒜撮,我是刑警寧澤,帶...
    沈念sama閱讀 35,425評論 5 343
  • 正文 年R本政府宣布慷嗜,位于F島的核電站淀弹,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏庆械。R本人自食惡果不足惜薇溃,卻給世界環(huán)境...
    茶點故事閱讀 41,019評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望缭乘。 院中可真熱鬧沐序,春花似錦、人聲如沸堕绩。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,671評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽奴紧。三九已至特姐,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間黍氮,已是汗流浹背唐含。 一陣腳步聲響...
    開封第一講書人閱讀 32,825評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留沫浆,地道東北人捷枯。 一個月前我還...
    沈念sama閱讀 47,729評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像专执,于是被迫代替她去往敵國和親淮捆。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,614評論 2 353

推薦閱讀更多精彩內(nèi)容