Apache Thrift - это язык описания программных интерфейсов для построения сервисов под разные языки программирования. Работает это следующим образом. На специальном C-подобном языке вы описываете все интерфейсы и сущности вашего сервиса, после чего генерите код на нужном вам языке. Thrift поддерживает несколько десятков языков, в том числе и Java.
Сгенерированные классы вы копируете как на сервер, так и на клиент. На сервере делаете имплементацию интерфейсов, а на клиенте - их вызов. Таким образом, вызов процедур на сервере будет для вас выглядеть так же, как и вызов обычных методов локально. При этом код на клиенте и сервере может быть на разных языках. 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 создаст несколько файлов с исходниками на java, внутрь которых я заглядывать вам не советую - там сплошная простыня быдлокода, который вам пришлось бы писать вручную, если бы вы не захотели использовать 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):
Теперь можно запустить сервер и выполнить запрос с клиента для prsId = 2. В ответ вы получите профиль пользователя. Для любого другого значения prsId вы получите исключение.
Таким образом, Apache Thrift полностью избавляет нас от необходимости задумываться над проблемами сериализации, а также позволяет менять один протокол на другой буквально двумя строками кода.