Thrift — это такая штука для сериализации данных. Вы описываете схему данных в специальном формате. Из этого описания генерируются классы. Эти классы легко сериализуются и десериализуются. При этом схему можно изменять (например, добавлять-удалять поля в классах) так, что данные, сериализованные по старой схеме, будут успешно десериализованы по новой. Одну и ту же схему можно использовать в проектах на разных языках, и они будут успешно друг с другом взаимодействовать. Плюс к этому еще накручена возможность объявлять исключения и генерировать код для RPC. В этой заметке мы разберемся, как работать с Thrift на языке Scala.

Во-первых, нам понадобятся следующие зависимости:

«org.apache.thrift» % «libthrift» % «0.9.2» ,
«com.twitter» %% «scrooge-core» % «3.20.0» ,

Во-вторых, в project/plugins.sbt дописываем:

addSbtPlugin ( «com.twitter» %% «scrooge-sbt-plugin» % «3.16.3» )

… а в build.sbt:

com. twitter . scrooge . ScroogeSBT . newSettings

Таким образом мы подключили к проекту Scrooge -плагин и можем генерировать Scala-классы из thrift-файлов командой:

sbt scrooge-gen

Более того, генерация будет происходить автоматически при сборке проекта, в том числе при выполнении команды sbt assembly .

Далее создаем src/thrift/game.thrift следующего содержания:

// include «ololo.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 spellbook = Set ( Spell. Thunderbolt , Spell. Fireball )
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 . Вот один из них:

forAll { ( data1 : Hero ) =>
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 пишутся очень просто. Выглядит это примерно так:

implicit lazy val arbHero : Arbitrary [ Hero ] = Arbitrary (
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 поддерживает четыре так называемых протокола сериализации:

  1. TBinaryProtocol — обычный бинарный протокол;
  2. TCompactProtocol — компактный бинарный протокол;
  3. TJSONProtocol — очень сложно читаемый JSON;
  4. TSimpleJSONProtocol — легко читаемый JSON, но без десериализации;

Использование этих протоколов выглядит аналогично. Например:

def heroToJson ( hero : Hero ) : String = {
val out = new ByteArrayOutputStream ( )
hero. write ( new TSimpleJSONProtocol ( new TIOStreamTransport ( out ) ) )
new String (
ByteBuffer. wrap ( out. toByteArray ) . array ( ) ,
StandardCharsets. UTF_8
)
}

Еще одна интересная фишка Thrift — возможность вручную закодировать список, map или set объектов:

forAll { ( data1 : List [ Hero ] ) =>
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. Более подробную информацию вы найдете по следующим ссылкам:

А что вы используете для сериализации и десериализации?

Дополнение: Заметки Сериализация и десериализация в/из Protobuf на C++ и Сериализация в языке Go на примере библиотеки codec также могут быть вам интересны.

EnglishRussianUkrainian