Thrift — это такая штука для сериализации данных. Вы описываете схему данных в специальном формате. Из этого описания генерируются классы. Эти классы легко сериализуются и десериализуются. При этом схему можно изменять (например, добавлять-удалять поля в классах) так, что данные, сериализованные по старой схеме, будут успешно десериализованы по новой. Одну и ту же схему можно использовать в проектах на разных языках, и они будут успешно друг с другом взаимодействовать. Плюс к этому еще накручена возможность объявлять исключения и генерировать код для RPC. В этой заметке мы разберемся, как работать с Thrift на языке Scala.
Во-первых, нам понадобятся следующие зависимости:
«com.twitter» %% «scrooge-core» % «3.20.0» ,
Во-вторых, в project/plugins.sbt дописываем:
… а в build.sbt:
Таким образом мы подключили к проекту Scrooge -плагин и можем генерировать Scala-классы из thrift-файлов командой:
Более того, генерация будет происходить автоматически при сборке проекта, в том числе при выполнении команды sbt assembly
.
Далее создаем src/thrift/game.thrift следующего содержания:
namespace java me.eax.examples.thrift.game
// exception Ololo { … }
// service Ololo { … }
enum Weapon {
Sword = 1
Bow = 2
}
struct WarriorInfo {
1: optional Weapon weapon
2: required i64 arrowsNumber
}
enum Spell {
Fireball = 1
Thunderbolt = 2
}
struct MageInfo {
1: required set<Spell> spellbook
2: required i64 mana
}
union ClassSpecificInfo {
1: WarriorInfo warrior
2: MageInfo mage
}
struct Hero {
1: required string name
2: required i64 hp
3: required i64 xp
4: ClassSpecificInfo classSpecificInfo
}
Мне нужно было придумать пример, демонстрирующий использование required и optional полей, enum’ов, контейнеров типа set и map, а также, как при помощи Thrift получить алгебраические типы. В итоге придумались такие вот классы для RPG-игры. Мне кажется, тут все довольно очевидно, так что двигаемся дальше.
Примеры использования сгенерированных классов:
val mage = Hero (
name = «afiskon» , hp = 25L, xp = 1024L,
ClassSpecificInfo. Mage ( MageInfo ( spellbook, mana = 100L ) )
)
val warrior = Hero (
name = «eax» , hp = 50L, xp = 256L,
ClassSpecificInfo. Warrior ( WarriorInfo ( Some ( Weapon. Sword ) , 0L ) )
)
Для тестирования сериализации и десериализации мной были написаны тесты с использованием ScalaCheck . Вот один из них:
val bytes = {
val out = new ByteArrayOutputStream ( )
data1. write ( new TBinaryProtocol ( new TIOStreamTransport ( out ) ) )
out. toByteArray
}
val data2 = {
val stream = new ByteArrayInputStream ( bytes )
Hero. decode ( new TBinaryProtocol ( new TIOStreamTransport ( stream ) ) )
}
data1 shouldBe data2
}
Заметьте, что ScalaCheck откуда-то знает, как генерировать случайных героев. Это потому что для класса Hero мной специально был написан генератор. Генераторы в ScalaCheck пишутся очень просто. Выглядит это примерно так:
for {
name < — Arbitrary. arbitrary [ String ]
hp < — Gen. posNum [ Long ]
xp < — Gen. posNum [ Long ]
classSpecificInfo < — Arbitrary. arbitrary [ ClassSpecificInfo ]
} yield Hero ( name, hp, xp, classSpecificInfo )
)
Генераторы для ClassSpecificInfo.Mage, ClassSpecificInfo.Warrior и, так сказать, «корневого» ClassSpecificInfo выглядят аналогично.
Обратите внимание, что выше приводится тест для TBinaryProtocol. Всего Thrift поддерживает четыре так называемых протокола сериализации:
- TBinaryProtocol — обычный бинарный протокол;
- TCompactProtocol — компактный бинарный протокол;
- TJSONProtocol — очень сложно читаемый JSON;
- TSimpleJSONProtocol — легко читаемый JSON, но без десериализации;
Использование этих протоколов выглядит аналогично. Например:
val out = new ByteArrayOutputStream ( )
hero. write ( new TSimpleJSONProtocol ( new TIOStreamTransport ( out ) ) )
new String (
ByteBuffer. wrap ( out. toByteArray ) . array ( ) ,
StandardCharsets. UTF_8
)
}
Еще одна интересная фишка Thrift — возможность вручную закодировать список, map или set объектов:
val bytes = {
val out = new ByteArrayOutputStream ( )
val proto = new TBinaryProtocol ( new TIOStreamTransport ( out ) )
proto. writeListBegin ( new TList ( TType. STRUCT , data1. size ) )
data1. foreach ( _ . write ( proto ) )
proto. writeListEnd ( )
out. toByteArray
}
val data2 = {
val stream = new ByteArrayInputStream ( bytes )
val proto = new TBinaryProtocol ( new TIOStreamTransport ( stream ) )
val listInfo = proto. readListBegin ( )
val res = {
for ( _ < — 1 to listInfo. size ) yield Hero. decode ( proto )
} . toList
proto. readListEnd ( )
res
}
data1 shouldBe data2
}
Вот, по большому счету, все, что я хотел рассказать сегодня про Thrift. Более подробную информацию вы найдете по следующим ссылкам:
- Хорошая статья «Thrift: The Missing Guide» ;
- IRC-каналы и списки рассылок, посвященные Thrift ;
- Полная версия исходников к этому посту ;
А что вы используете для сериализации и десериализации?
Дополнение: Заметки Сериализация и десериализация в/из Protobuf на C++ и Сериализация в языке Go на примере библиотеки codec также могут быть вам интересны.