Apache Thrift - это язык описания программных интерфейсов для построения сервисов под разные языки программирования. Работает это следующим образом. На специальном C-подобном языке вы описываете все интерфейсы и сущности вашего сервиса, после чего генерите код на нужном вам языке. Thrift поддерживает несколько десятков языков, в том числе и Java.
Сгенерированные классы вы копируете как на сервер, так и на клиент. На сервере делаете имплементацию интерфейсов, а на клиенте - их вызов. Таким образом, вызов процедур на сервере будет для вас выглядеть так же, как и вызов обычных методов локально. При этом код на клиенте и сервере может быть на разных языках. Thrift берёт на себя все вопросы сериализации данных.
https://github.com/nordmine/examples
В корневом pom-файле определяются все зависимости и версии, которые будут доступны в модулях. Здесь нам потребуется только библиотека libthrift. Также в parent-pom определяются плагины для сборки.
Добавим в наш maven-проект новый модуль thrift-entities. В parent-pom у вас должна появится запись:
А в pom-файле нового модуля будет ссылка на parent-pom:
В модулях номер версии можно будет не указывать - он будет получен из parent-pom.
Теперь мы можем перейти к описанию нашего сервиса на языке Thrift. Поскольку в качестве примера мы выбрали сервис, возвращающий профиль пользотеля, пусть у каждого пользователя может быть некий статус. Статус имеет конечное множество значений, по сути, это enum. Предположим, профиль пользователя может быть не подтверждённым, подтверждённым (проверили почтовый ящик) и золотым (какие-то платные услуги подключены, например).
На языке Thrift перечисление будет выглядеть следующим образом (cначала указывается java-пакет). Для перечисления используется ключевое слово enum:
Поместим его в файл thrift-entities/src/main/resources/Enums.thrift.
Затем в thrift-entities/src/main/resources/ создадим файл Models.thrift и поместим в него описание сущности самого профиля пользователя.
К текущему файлу могут быть подключены другие thrift-файлы при помощи ключевого слова include. Классы-сущности описываются при помощи ключевого слова struct.
Обратите внимание, что каждое поле структуры снабжается обязательным индексом - такова особенности Thrift. Строка обозначается ключевым словом string, 32-битные целые обозначаются как i32, 64-битные целые - как i64. Если при описании нужно сослаться на сущность из другого thrift-файла, к имени этой сущности прибавляют имя файла с точкой (Enums.PersonState).
Наш профиль содержит id пользователя, его имя и фамилию, статус, а также дату рождения. В Thrift нет встроенных типов работы с датами. Можно было бы конечно определить отдельную структуру с полями год, месяц и день. Но что, если нужно знать дату до секунды? Уже потребуется шесть полей. А шесть полей уже не очень удобно инициализировать. Кроме того, кто будет контролировать, что в году 12 месяцев, а в месяце не более 31 дня? Поэтому я предлагаю любые даты отправлять в виде 64-битного целого числа, которое представляет собой количество миллисекунд, прошедших с 1 января 1970 года (Unix timestamp). А на клиенте мы всегда сможем создать новый объект даты на основе этого значения.
Теперь определим непосредственно интерфейс нашего сервиса с единственным методом getById, возвращающим сущность профиля. Обратите внимание, что метод выбрасывает исключение. Его определение будет дано ниже.
Интерфейсы сервисов определяются при помощи ключевого слова service. Параметры метода и все возможные исключения также снабжаются порядковыми номерами.
Хорошей практикой является создание отдельного, специального исключения, связанного с ошибками бизнес-логики нашего сервиса.
В данном случае это исключение (ключевое слово exception) с двумя пронумерованными полями: код ошибки и текстовое описание.
Мы разложили наши сущности по разным файлам, подобно тому, как это делается в java. Однако thrift не накладывает таких ограничений и мы можем хоть все сущности описать в одном файле и обойтись вовсе без include и лишних префиксов. Однако в больших проектах я так делать не рекомендую.
Теперь мы определили все сущности нашего сервиса и готовы сгенерить java-классы на основании наших файлов. Делается это при помощи утилиты thrift в командной строке:
thrift -gen java -r -out thrift-entities/src/main/java thrift-entities/src/main/resources/Services.thrift
В итоге у вас должна получиться такая структура в модуле thrift-entities:
Модуль с сущностями нам понадобится на сервере. Добавим новый maven-модуль thrift-server. Его pom-файл делаем аналогично предыдущему, только туда добавляется ещё одна зависимость:
Теперь создадим обработчик для нашего сервиса. Для этого достаточно реализовать его интерфейс:
В идеале здесь могла бы быть работа с базой, но мы лишь имитируем получение профиля. Если id персоны равно 2, то возвращаем некоторые данные. Иначе имитируем отсутствие данных. Обратите внимание, что тут мы используем два класса (Profile и ProfileException), которые изначально определяли на thrift.
Ну и ещё надо добавить класс с кодом инициализации самого сервера, где мы определяем протокольный и транспортный уровень. Здесь наш сервер слушает порт 9090 и взаимодействие происходит через сокеты.
Код клиента предельно прост. Указываем хост и порт сервера, а также задаём взаимодействие через сокеты (TSocket) по бинарному протоколу (TBinaryProtocol):
Сгенерированные классы вы копируете как на сервер, так и на клиент. На сервере делаете имплементацию интерфейсов, а на клиенте - их вызов. Таким образом, вызов процедур на сервере будет для вас выглядеть так же, как и вызов обычных методов локально. При этом код на клиенте и сервере может быть на разных языках. Thrift берёт на себя все вопросы сериализации данных.
План
Мы опишем интерфейс нашего сервиса на специальном языке Thrift, реализуем этот интерфейс на сервере и будем вызывать его с клиента. Для удобства реализуем всё это в виде maven-проекта с тремя модулями (клиент, сервер и сгенерированные общие классы). Для примера, наш сервис будет возвращать профиль пользователя по его id.Требования
Java 8, Apache Thrift 0.9, Maven 3.Исходники
https://github.com/nordmine/examples
Maven-проект с модулями
Поскольку классы, которые генерит Thrift нам нужны как на сервере, так и на клиенте, мы выделим их в отдельный maven-модуль и будем подтягивать его через dependency в модуле сервера и модуле клиента. Все эти три модуля связаны между собой, поэтому я поместил их в один maven-проект с модулями, хотя это и не обязательно. Вы можете сделать два или три отдельных проекта.<groupId>developer-remarks</groupId> <artifactId>examples</artifactId> <packaging>pom</packaging> <version>1.0</version> <modules> <module>thrift-entities</module> <module>thrift-server</module> <module>thrift-client</module> </modules> <dependencyManagement> <dependencies> <dependency> <groupId>org.apache.thrift</groupId> <artifactId>libthrift</artifactId> <version>0.9.3</version> </dependency> </dependencies> </dependencyManagement> <build> <pluginManagement> <plugins> <plugin> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> </plugins> </pluginManagement> </build>
В корневом pom-файле определяются все зависимости и версии, которые будут доступны в модулях. Здесь нам потребуется только библиотека libthrift. Также в parent-pom определяются плагины для сборки.
Модуль с классами Thrift
Добавим в наш maven-проект новый модуль thrift-entities. В parent-pom у вас должна появится запись:
<modules> <module>thrift-entities</module> </modules>
А в pom-файле нового модуля будет ссылка на parent-pom:
<parent> <artifactId>examples</artifactId> <groupId>developer-remarks</groupId> <version>1.0</version> </parent> <artifactId>thrift-entities</artifactId> <dependencies> <dependency> <groupId>org.apache.thrift</groupId> <artifactId>libthrift</artifactId> </dependency> </dependencies>
В модулях номер версии можно будет не указывать - он будет получен из parent-pom.
Теперь мы можем перейти к описанию нашего сервиса на языке Thrift. Поскольку в качестве примера мы выбрали сервис, возвращающий профиль пользотеля, пусть у каждого пользователя может быть некий статус. Статус имеет конечное множество значений, по сути, это enum. Предположим, профиль пользователя может быть не подтверждённым, подтверждённым (проверили почтовый ящик) и золотым (какие-то платные услуги подключены, например).
На языке Thrift перечисление будет выглядеть следующим образом (cначала указывается java-пакет). Для перечисления используется ключевое слово enum:
namespace java developer.remarks.thrift.v1.enums enum PersonState { UNVERIFIED VERIFIED GOLD }
Поместим его в файл thrift-entities/src/main/resources/Enums.thrift.
Затем в thrift-entities/src/main/resources/ создадим файл Models.thrift и поместим в него описание сущности самого профиля пользователя.
namespace java developer.remarks.thrift.v1.models include "Enums.thrift" struct Profile { 1: i64 prsId 2: string firstName 3: string lastName 4: i64 birthDate 5: Enums.PersonState state }
К текущему файлу могут быть подключены другие thrift-файлы при помощи ключевого слова include. Классы-сущности описываются при помощи ключевого слова struct.
Обратите внимание, что каждое поле структуры снабжается обязательным индексом - такова особенности Thrift. Строка обозначается ключевым словом string, 32-битные целые обозначаются как i32, 64-битные целые - как i64. Если при описании нужно сослаться на сущность из другого thrift-файла, к имени этой сущности прибавляют имя файла с точкой (Enums.PersonState).
Наш профиль содержит id пользователя, его имя и фамилию, статус, а также дату рождения. В Thrift нет встроенных типов работы с датами. Можно было бы конечно определить отдельную структуру с полями год, месяц и день. Но что, если нужно знать дату до секунды? Уже потребуется шесть полей. А шесть полей уже не очень удобно инициализировать. Кроме того, кто будет контролировать, что в году 12 месяцев, а в месяце не более 31 дня? Поэтому я предлагаю любые даты отправлять в виде 64-битного целого числа, которое представляет собой количество миллисекунд, прошедших с 1 января 1970 года (Unix timestamp). А на клиенте мы всегда сможем создать новый объект даты на основе этого значения.
Теперь определим непосредственно интерфейс нашего сервиса с единственным методом getById, возвращающим сущность профиля. Обратите внимание, что метод выбрасывает исключение. Его определение будет дано ниже.
namespace java developer.remarks.thrift.v1.services include "Exceptions.thrift" include "Models.thrift" service ProfileService { Models.Profile getById(1: i64 prsId) throws (1: Exceptions.ProfileException e) }
Интерфейсы сервисов определяются при помощи ключевого слова service. Параметры метода и все возможные исключения также снабжаются порядковыми номерами.
Хорошей практикой является создание отдельного, специального исключения, связанного с ошибками бизнес-логики нашего сервиса.
namespace java developer.remarks.thrift.v1.exceptions exception ProfileException { 1: i32 errorCode 2: string description }
В данном случае это исключение (ключевое слово exception) с двумя пронумерованными полями: код ошибки и текстовое описание.
Мы разложили наши сущности по разным файлам, подобно тому, как это делается в java. Однако thrift не накладывает таких ограничений и мы можем хоть все сущности описать в одном файле и обойтись вовсе без include и лишних префиксов. Однако в больших проектах я так делать не рекомендую.
Генерим java-классы на основе thrift-файлов
Теперь мы определили все сущности нашего сервиса и готовы сгенерить java-классы на основании наших файлов. Делается это при помощи утилиты thrift в командной строке:
thrift -gen java -r -out thrift-entities/src/main/java thrift-entities/src/main/resources/Services.thrift
- флаг -gen указывает, на каком языке следует сгенерить сущности
- флаг -r говорит, что нужно сгенерить не только указанную сущность, но и пройтись по всем include.
- флаг -out указывает директорию, куда следует сложить сгенерированные файлы.
- в конце следует путь до целевого thrift-файла, как правило, это файл с сервисом.
В итоге у вас должна получиться такая структура в модуле thrift-entities:
Запускаем сервер
Модуль с сущностями нам понадобится на сервере. Добавим новый maven-модуль thrift-server. Его pom-файл делаем аналогично предыдущему, только туда добавляется ещё одна зависимость:
<dependencies> <dependency> <groupId>org.apache.thrift</groupId> <artifactId>libthrift</artifactId> </dependency> <dependency> <groupId>developer-remarks</groupId> <artifactId>thrift-entities</artifactId> <version>1.0</version> </dependency> </dependencies>
Теперь создадим обработчик для нашего сервиса. Для этого достаточно реализовать его интерфейс:
public class ProfileHandler implements ProfileService.Iface { @Override public Profile getById(long prsId) throws ProfileException, TException { if (prsId == 2) { long birthTimestamp = LocalDateTime.of(1985, 8, 31, 0, 0, 0) .toEpochSecond(ZoneOffset.UTC); return new Profile(prsId, "Александр", "Кузнецов", birthTimestamp, PersonState.GOLD); } else { throw new ProfileException(-1, "Profile with id = " + prsId + " not found"); } } }
В идеале здесь могла бы быть работа с базой, но мы лишь имитируем получение профиля. Если id персоны равно 2, то возвращаем некоторые данные. Иначе имитируем отсутствие данных. Обратите внимание, что тут мы используем два класса (Profile и ProfileException), которые изначально определяли на thrift.
Ну и ещё надо добавить класс с кодом инициализации самого сервера, где мы определяем протокольный и транспортный уровень. Здесь наш сервер слушает порт 9090 и взаимодействие происходит через сокеты.
public class ProfileServer { private static final int PORT = 9090; public static ProfileHandler handler; public static ProfileService.Processor processor; public static void main(String[] args) { try { handler = new ProfileHandler(); processor = new ProfileService.Processor(handler); Runnable simple = () -> perform(processor); new Thread(simple).start(); } catch (Exception x) { x.printStackTrace(); } } public static void perform(ProfileService.Processor processor) { try { TServerTransport serverTransport = new TServerSocket(PORT); TServer server = new TSimpleServer( new TServer.Args(serverTransport).processor(processor) ); System.out.println("Starting the simple server on port " + PORT + "..."); server.serve(); } catch (Exception e) { e.printStackTrace(); } } }
Добавляем клиент
Создадим новый модуль thrift-client. Его pom-файл будет аналогичен серверному. В этом модуле мы также используем thrift-сущности.Код клиента предельно прост. Указываем хост и порт сервера, а также задаём взаимодействие через сокеты (TSocket) по бинарному протоколу (TBinaryProtocol):
public class ThriftClient { public static void main(String[] args) { try { TTransport transport = new TSocket("localhost", 9090); transport.open(); TProtocol protocol = new TBinaryProtocol(transport); ProfileService.Client client = new ProfileService.Client(protocol); perform(client); transport.close(); } catch (TException x) { x.printStackTrace(); } } private static void perform(ProfileService.Client client) throws TException { Profile profile = client.getById(2L); Instant instant = Instant.ofEpochSecond(profile.getBirthDate()); LocalDateTime birthDate = LocalDateTime.ofInstant(instant, ZoneId.systemDefault()); System.out.println( String.format("%s родился %s. Статус: %s.", profile.getFirstName(), DateTimeFormatter.ISO_DATE.format(birthDate), profile.getState()) ); } }
Теперь можно запустить сервер и выполнить запрос с клиента для prsId = 2. В ответ вы получите профиль пользователя. Для любого другого значения prsId вы получите исключение.
Заключение
Таким образом, Apache Thrift полностью избавляет нас от необходимости задумываться над проблемами сериализации, а также позволяет менять один протокол на другой буквально двумя строками кода.