Thrift中enum的一些探究


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,用於定義枚舉值到整數的映射


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM