http://anruence.com/2018/06/27/enum-thrift/
问题
在用注解定义的Thrift enum 中,如果客户端端和服务端的enum定义不同,比如调换了enum中的枚举值的顺序,就会发生调用端发送的枚举参数与服务端解析得到的枚举参数不一致的问题。
猜想
java 中的enum类的每一个具体的枚举值都有一个ordinal,代表其声明顺序,从零开始。thrift在序列化和反序列化时会将枚举值转换为一个整数传递,所以枚举值的具体含义与调用端和服务端各自的enum代码中声明顺序有关。
Thrift 注解的实现中对于thrift中每个关键字都有对应的编解码器,enum对应的为EnumThriftCodec<T extends Enum,这个类继承了接口ThriftCodec 。具体代码如下:
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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93
|
package com.facebook.swift.codec.internal;
import com.facebook.swift.codec.ThriftCodec; import com.facebook.swift.codec.metadata.ThriftEnumMetadata; import com.facebook.swift.codec.metadata.ThriftType; import com.google.common.base.Preconditions; import org.apache.thrift.protocol.TProtocol;
import javax.annotation.concurrent.Immutable; // 在类开始部分的注释就说明了EnumThriftCodec将enum编码成thrift里的i32,也就是int。枚举值会被编码成一个整数。
/** * EnumThriftCodec is a codec for Java enum types. An enum is encoded as an I32 in Thrift, and this * class handles converting this vale to a Java enum constant. */ @Immutable public class EnumThriftCodec<T extends Enum<T>> implements ThriftCodec<T> { private final ThriftType type; private final ThriftEnumMetadata<T> enumMetadata;
public EnumThriftCodec(ThriftType type) { this.type = type; enumMetadata = (ThriftEnumMetadata<T>) type.getEnumMetadata(); }
@Override public ThriftType getType() { return type; }
@Override public T read(TProtocol protocol) throws Exception { int enumValue = protocol.readI32(); if (enumValue >= 0) { // 检查当前解码的枚举类是否有显式声明的对应整数值,如果有,则直接根据声明的对应关系进行解码,如果没有显式声明对应关系,则直接获取当前枚举类下声明的所有枚举值,按照enumValue进行索引 if (enumMetadata.hasExplicitThriftValue()) { T enumConstant = enumMetadata.getByEnumValue().get(enumValue); if (enumConstant != null) { return enumConstant; } } else { T[] enumConstants = enumMetadata.getEnumClass().getEnumConstants(); if (enumValue < enumConstants.length) { return enumConstants[enumValue]; } } } // unknown, throw unknown value exception throw new UnknownEnumValueException( String.format( "Enum %s does not have a value for %s", enumMetadata.getEnumClass(), enumValue ) ); }
@Override public void write(T enumConstant, TProtocol protocol) throws Exception { Preconditions.checkNotNull(enumConstant, "enumConstant is null");
int enumValue; // 编码过程与解码过程基本一致,首先判断枚举类有没有显式声明的对应整数值,如果有则根据声明的对应关系进行编码,否则就直接按照其ordinal编码。ordinal()方法的具体实现就是返回枚举值的声明顺序的索引(从0开始)
if (enumMetadata.hasExplicitThriftValue()) { enumValue = enumMetadata.getByEnumConstant().get(enumConstant); } else { enumValue = enumConstant.ordinal(); } protocol.writeI32(enumValue); } }
/** * Returns the ordinal of this enumeration constant (its position * in its enum declaration, where the initial constant is assigned * an ordinal of zero). * * Most programmers will have no use for this method. It is * designed for use by sophisticated enum-based data structures, such * as {@link java.util.EnumSet} and {@link java.util.EnumMap}. * * @return the ordinal of this enumeration constant */
|
初步结论
现在总结一下,Thrift 注解方式会将枚举值编码成一个int进行网络传输,而在处理具体的枚举值与整数之间的对应关系的时候有两种策略:
如果枚举类显式声明了枚举值与整数之间的对应关系,则根据声明的规则进行编解码
如果枚举类中没有显式声明对应关系,则根据声明顺序的索引进行编解码。
具备了以上知识,就能解答文章最开始的问题了:如果客户端和服务端的枚举类里没有显式声明枚举值和整数值的对应关系,那么在编解码的时候的对应关系就是枚举值的声明顺序,如果两端的枚举类中枚举值的顺序不一致,就会导致两端编解码的的枚举值不一致。
深入探究
接下来再进一步探索,EnumThriftCodec如何判断一个枚举类是否声明了枚举值到整数的对应关系?为了回答这个问题,需要首先弄清楚EnumThriftCodec中的成员变量EnumMetadata的具体内容:
ThriftEnumMetadata.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 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
|
/* * Copyright (C) 2012 Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ package com.facebook.swift.codec.metadata; import com.facebook.swift.codec.ThriftEnumValue; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.Map; import javax.annotation.concurrent.Immutable; import static java.lang.String.format; @Immutable public class ThriftEnumMetadata<T extends Enum<T>> { private final Class<T> enumClass; private final Map<Integer, T> byEnumValue; private final Map<T, Integer> byEnumConstant; private final String enumName; private final ImmutableList<String> documentation; private final ImmutableMap<T, ImmutableList<String>> elementDocs; public ThriftEnumMetadata( String enumName, Class<T> enumClass) throws RuntimeException { //构造函数 } public String getEnumName() { return enumName; } public Class<T> getEnumClass() { return enumClass; } public boolean hasExplicitThriftValue() { return byEnumValue != null; } public Map<Integer, T> getByEnumValue() { return byEnumValue; } public Map<T, Integer> getByEnumConstant() { return byEnumConstant; } public ImmutableList<String> getDocumentation() { return documentation; } public Map<T, ImmutableList<String>> getElementsDocumentation() { return elementDocs; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } final ThriftEnumMetadata<?> that = (ThriftEnumMetadata<?>) o; if (!enumClass.equals(that.enumClass)) { return false; } return true; } @Override public int hashCode() { return enumClass.hashCode(); } @Override public String toString() { final StringBuilder sb = new StringBuilder(); sb.append("ThriftEnumMetadata"); sb.append("{enumClass=").append(enumClass); sb.append(", byThriftValue=").append(byEnumValue); sb.append('}'); return sb.toString(); } }
|
顾名思义,ThriftEnumMetadata表示一个thrift enum类的元数据,它的主要逻辑都集中在构造函数ThriftEnumMetadata( String enumName, Class enumClass)中,其实现比较长,我们分两个部分来看:
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
|
ThriftEnumMetadata(String enumName, Class<T> enumClass) throws RuntimeException part 1
Method enumValueMethod = null; for (Method method : enumClass.getMethods()) { if (method.isAnnotationPresent(ThriftEnumValue.class)) { Preconditions.checkArgument( Modifier.isPublic(method.getModifiers()), "Enum class %s @ThriftEnumValue method is not public: %s", enumClass.getName(), method); Preconditions.checkArgument( !Modifier.isStatic(method.getModifiers()), "Enum class %s @ThriftEnumValue method is static: %s", enumClass.getName(), method); Preconditions.checkArgument( method.getTypeParameters().length == 0, "Enum class %s @ThriftEnumValue method has parameters: %s", enumClass.getName(), method); Class<?> returnType = method.getReturnType(); Preconditions.checkArgument( returnType == int.class || returnType == Integer.class, "Enum class %s @ThriftEnumValue method does not return int or Integer: %s", enumClass.getName(), method); enumValueMethod = method; } }
|
第一部分的实现比较简单,首先给成员变量enumName和enumClass赋值,然后声明了一个Method类型的变量enumValueMethod,从代码逻辑我们可以看到这个enumValueMethod的几个特征:
包含注解@ThriftEnumValue
public方法
非static方法
参数列表为空,即不要求传入参数
返回值为int或Integer
接下来看构造函数的第二个部分:
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
|
ThriftEnumMetadata(String enumName, Class<T> enumClass) throws RuntimeException part 2
ImmutableMap.Builder<T, ImmutableList<String>> elementDocs = ImmutableMap.builder(); if (enumValueMethod != null) { ImmutableMap.Builder<Integer, T> byEnumValue = ImmutableMap.builder(); ImmutableMap.Builder<T, Integer> byEnumConstant = ImmutableMap.builder(); for (T enumConstant : enumClass.getEnumConstants()) { Integer value; try { value = (Integer) enumValueMethod.invoke(enumConstant); } catch (Exception e) { throw new RuntimeException(format("Enum class %s element %s get value method threw an exception", enumClass.getName(), enumConstant), e); } Preconditions.checkArgument( value != null, "Enum class %s element %s returned null for enum value: %s", enumClass.getName(), enumConstant ); byEnumValue.put(value, enumConstant); byEnumConstant.put(enumConstant, value); elementDocs.put(enumConstant, ThriftCatalog.getThriftDocumentation(enumConstant)); } this.byEnumValue = byEnumValue.build(); this.byEnumConstant = byEnumConstant.build(); } else { byEnumValue = null; byEnumConstant = null; for (T enumConstant : enumClass.getEnumConstants()) { elementDocs.put(enumConstant, ThriftCatalog.getThriftDocumentation(enumConstant)); } } this.elementDocs = elementDocs.build(); this.documentation = ThriftCatalog.getThriftDocumentation(enumClass);
|
第二部分的主要逻辑有两个:
1)如果enumValueMethod不为空,则对当前枚举类下声明的每个枚举值调用enumValueMethod方法,并用返回结果填充两个map:byEnumValue和byEnumConstant,分别表示整数到枚举值和枚举值到整数的映射关系;
2)获取并保存枚举类声明的documention。
最后再看EnumThriftCodec是如何判断枚举类是否显式声明了枚举值与整数之间的对应关系的,即hasExplicitThriftValue方法的实现:
1 2 3 4 5 6
|
hasExplicitThriftValue
public boolean hasExplicitThriftValue() { return byEnumValue != null; }
|
很简单,就是判断保存整数到枚举值的对应关系的byEnumValue是否为空。
最佳实践
Thrift 中注解开发时 enum 相关的几点建议
尽量保持调用端和服务端的 thrift 定义一致
在枚举类中定义@ThriftEnumValue方法来显式声明枚举值与整数的对应关系,避免使用默认的编解码规则
如果声明了带有@ThriftEnumValue的返回整数类型的无参public函数,请确保每个枚举值调用该方法的返回值都不一样(参考Object的hashcode方法)
举例
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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110
|
欠佳的例子:
欠佳的例子 /** * 没有提供枚举值到整数的对应关系,在编解码时会按照声明顺序进行索引 */ @ThriftEnum public enum ThriftAnnotatedEnum { FIRST_VALUE("first"), SECOND_VALUE("second"); private String description; ThriftAnnotatedEnum(String description) { this.description = description; } } 建议的实现:
建议的实现 1
@ThriftEnum public enum ThriftAnnotatedEnum { FIRST_VALUE("fist"), SECOND_VALUE("second"); private String description; ThriftAnnotatedEnum(String description) { this.description = description; } //提供了返回int类型的无参public函数,建立从枚举值到整数的映射 @ThriftEnumValue public int getIntValue() { return this.description.hashCode(); } } 建议的实现 2
@ThriftEnum public enum ThriftAnnotatedEnum { FIRST_VALUE("fist", 0), SECOND_VALUE("second", 1); private String description; private int intValue;//直接在枚举类定义整数类型的成员变量用于标识 ThriftAnnotatedEnum(String description, int intValue) { this.description = description; this.intValue = intValue; } @ThriftEnumValue public int getIntValue() { return intValue; } } 补充 IDL方式中的enum的实现细节
TweetType.thrift
enum TweetType { TWEET, RETWEET = 2, DM = 0xa, REPLY } TweetType.java
public enum TweetType implements TEnum { TWEET(0), RETWEET(2), DM(10), REPLY(11); private final int value; private TweetType(int value) { this.value = value; } /** * Get the integer value of this enum value, as defined in the Thrift IDL. */ public int getValue() { return value; } /** * Find a the enum type by its integer value, as defined in the Thrift IDL. * @return null if the value is not found. */ public static TweetType findByValue(int value) { switch (value) { case 0: return TWEET; case 2: return RETWEET; case 10: return DM; case 11: return REPLY; default: return null; } } }
|
使用IDL文件编译生成的枚举类下有两个方法getValue和findByValue,用于定义枚举值到整数的映射
- MYSQL中 ENUM 类型
MYSQL中 ENUM 类型的详细解释 ENUM类型 ENUM 是一个字符串对象,其值通常选自一个允许值列表中,该列表在表创建时的列规格说明中被明确地列举. 在下列某些情况下,值也可以是空串(&quo ...
- mysql中enum的用法
字段 类型 长度/值*1 整理 属性 Null 默认2 额外 注释 enum 说明:enum类型的字段,若长度值写长度1/2,报错 (1) 数据长度为1,则为0,1,2… (2) ...
- Android菜鸟的成长笔记(14)—— Android中的状态保存探究(上)
原文:[置顶] Android菜鸟的成长笔记(14)—— Android中的状态保存探究(上) 我们在用手机的时候可能会发现,即使应用被放到后台再返回到前台数据依然保留(比如说我们正在玩游戏,突然电话 ...
- JAVA中enum的常见用法
JAVA中enum的常见用法包括:定义并添加方法.switch.遍历.EnumSet.EnumMap 1.定义enum并添加或覆盖方法 public Interface Behaviour{ void ...
- Android菜鸟的成长笔记(15)—— Android中的状态保存探究(下)
原文:Android菜鸟的成长笔记(15)-- Android中的状态保存探究(下) 在上一篇中我们简单了解关于Android中状态保存的过程和原理,这一篇中我们来看一下在系统配置改变的情况下保存数据 ...
- Java中enum的学习总结
一.通常的定义常量的方法 public class Sex{ public final static int MALE = 1; public final static int FEMALE=2; } ...
- MVC图片上传详解 IIS (安装SSL证书后) 实现 HTTP 自动跳转到 HTTPS C#中Enum用法小结 表达式目录树 “村长”教你测试用例 引用provinces.js的三级联动
MVC图片上传详解 MVC图片上传--控制器方法 新建一个控制器命名为File,定义一个Img方法 [HttpPost]public ActionResult Img(HttpPostedFile ...
- thrift 的required、optional探究
原因 经常使用thrift来编写rpc通信,但是对下面两个问题还是有些疑惑 thrift 的required.optional和不写有什么区别 optional不设置isset的话被传输后值? 实验 ...
- 关于python中Enum的个人总结
关于python中Enum的个人总结 初识 可以通过enum模块导入 语法 初始化: 可以通过enum_ = Enum('class_name', names,start = 1)来创建,其中name ...
- ThreadPoolExecutor源码学习(2)-- 在thrift中的应用
thrift作为一个从底到上除去业务逻辑代码,可以生成多种语言客户端以及服务器代码,涵盖了网络,IO,进程,线程管理的框架,着实庞大,不过它层次清晰,4层每层解决不同的问题,可以按需取用,相当方便. ...
随机推荐
- web端ant-design-vue Modal.info组件自定义icon和title使用小节
web端ant-design-vue Modal.info组件自定义icon和title整理小节,最近在项目中用到了自定义icon和title的功能,经过测试发现,如果自定义icon title会自动 ...
- threejs 浏览器窗口resize变化 自适应 html 全屏
全屏:画布全屏和body页面全屏: // 导入 threejs import * as THREE from "three"; import { OrbitControls } f ...
- 如何使用echarts
官网:https://echarts.apache.org/handbook/zh/get-started/ a 下载js文件并引入 b 初始化实例对象 echarts.init(获取盒子对象) 关 ...
- Nuxt3+PM2集群模式启动及勘误
起因 之前写过一篇 Nuxt3 的文章,Nuxt3 环境变量配置,用到了 PM2,但是里面的一些配置存在问题,最近有空又验证了一下,这里做一个勘误. 问题 PM2 的启动配置中有一项是exec_mod ...
- Bitmap 和 布隆过滤器傻傻分不清?你这不应该啊
大家好,我是小富- 有个兄弟私下跟我说,他在面试狗东时,有一道面试题没回答上来:Redis 的Bitmap和布隆过滤器啥区别与关系? 其实就是考小老弟对这两种工具的底层数据结构是否了解,不算太难的题. ...
- AI For Everyone_Week_1 By Andrew NG 课程英文
AI For Everyone__Week__1 By Andrew NG 1 Introduction Welcome to AI for everyone. AI is changing the ...
- Cartographer学习——地图概率更新过程
前言:最近一直在研究建图,对google的开源SLAM框架 Cartographer 进行了源码梳理,发现很多巧妙的算法设计,结合原论文 <Real-time Loop Closure in 2 ...
- 全面解释人工智能LLM模型的真实工作原理(三)
前一篇:<全面解释人工智能LLM模型的真实工作原理(二)> 序言:前面两节中,我们介绍了大语言模型的设计图和实现了一个能够生成自然语言的神经网络.这正是现代先进人工智能语言模型的雏形.不过 ...
- DRF-Serializers序列化器组件源码分析及改编
1. 源码分析 注意:以下代码片段为方便理解已进行简化,只保留了与序列化功能相关的代码 序列化的源码中涉及到了元类的概念,我在这里简单说明一下:元类(metaclass)是一个高级概念,用于定义类的创 ...
- P4253 SCOI2015 小凸玩密室
P4253 SCOI2015 小凸玩密室 一道紫色的 dp. 思路 首先读题: 要保证任意时刻所有被点亮的灯泡必须连通 在点亮一个灯泡后必须先点亮其子树所有灯泡才能点亮其他灯泡 考虑设 \(g[u][ ...