1. Обзор
В этом руководстве с помощью моделей Akka Actor & Stream мы узнаем, как настроить Akka для создания HTTP API, обеспечивающего базовые операции CRUD.
2. Зависимости Maven
Для начала давайте посмотрим на зависимости, необходимые для начала работы с Akka HTTP:
<dependency>
<groupId>com.typesafe.akka</groupId>
<artifactId>akka-http_2.12</artifactId>
<version>10.0.11</version>
</dependency>
<dependency>
<groupId>com.typesafe.akka</groupId>
<artifactId>akka-stream_2.12</artifactId>
<version>2.5.11</version>
</dependency>
<dependency>
<groupId>com.typesafe.akka</groupId>
<artifactId>akka-http-jackson_2.12</artifactId>
<version>10.0.11</version>
</dependency>
<dependency>
<groupId>com.typesafe.akka</groupId>
<artifactId>akka-http-testkit_2.12</artifactId>
<version>10.0.11</version>
<scope>test</scope>
</dependency>
Мы можем, конечно, найти последнюю версию этих библиотек Akka на Maven Central .
3. Создание актера
В качестве примера мы создадим HTTP API, который позволит нам управлять пользовательскими ресурсами. API будет поддерживать две операции:
- создание нового пользователя
- загрузка существующего пользователя
Прежде чем мы сможем предоставить HTTP API, нам нужно реализовать актор, который обеспечивает необходимые нам операции:
class UserActor extends AbstractActor {
private UserService userService = new UserService();
static Props props() {
return Props.create(UserActor.class);
}
@Override
public Receive createReceive() {
return receiveBuilder()
.match(CreateUserMessage.class, handleCreateUser())
.match(GetUserMessage.class, handleGetUser())
.build();
}
private FI.UnitApply<CreateUserMessage> handleCreateUser() {
return createUserMessage -> {
userService.createUser(createUserMessage.getUser());
sender()
.tell(new ActionPerformed(
String.format("User %s created.", createUserMessage.getUser().getName())), getSelf());
};
}
private FI.UnitApply<GetUserMessage> handleGetUser() {
return getUserMessage -> {
sender().tell(userService.getUser(getUserMessage.getUserId()), getSelf());
};
}
}
По сути, мы расширяем класс AbstractActor
и реализуем его метод createReceive() .
В createReceive()
мы сопоставляем типы входящих сообщений с методами, обрабатывающими сообщения соответствующего типа.
Типы сообщений представляют собой простые сериализуемые классы-контейнеры с некоторыми полями, описывающими определенную операцию . GetUserMessage
и имеет одно поле userId
для идентификации загружаемого пользователя. CreateUserMessage
содержит объект User
с пользовательскими данными, которые нам нужны для создания нового пользователя.
Позже мы увидим, как преобразовывать входящие HTTP-запросы в эти сообщения.
В конечном итоге мы делегируем все сообщения экземпляру UserService
, который обеспечивает бизнес-логику, необходимую для управления постоянными пользовательскими объектами.
Также обратите внимание на метод props()
. Хотя метод props()
не нужен для расширения AbstractActor ,
он пригодится позже при создании ActorSystem
.
Для более подробного обсуждения актеров взгляните на наше введение в Akka Actors .
4. Определение HTTP-маршрутов
Имея актор, который выполняет всю работу за нас, все, что нам осталось сделать, — это предоставить HTTP API, который делегирует входящие HTTP-запросы нашему актору.
Akka использует концепцию маршрутов для описания HTTP API. Для каждой операции нам нужен маршрут.
Чтобы создать HTTP-сервер, мы расширяем класс фреймворка HttpApp
и реализуем метод маршрутов
:
class UserServer extends HttpApp {
private final ActorRef userActor;
Timeout timeout = new Timeout(Duration.create(5, TimeUnit.SECONDS));
UserServer(ActorRef userActor) {
this.userActor = userActor;
}
@Override
public Route routes() {
return path("users", this::postUser)
.orElse(path(segment("users").slash(longSegment()), id -> route(getUser(id))));
}
private Route getUser(Long id) {
return get(() -> {
CompletionStage<Optional<User>> user =
PatternsCS.ask(userActor, new GetUserMessage(id), timeout)
.thenApply(obj -> (Optional<User>) obj);
return onSuccess(() -> user, performed -> {
if (performed.isPresent())
return complete(StatusCodes.OK, performed.get(), Jackson.marshaller());
else
return complete(StatusCodes.NOT_FOUND);
});
});
}
private Route postUser() {
return route(post(() -> entity(Jackson.unmarshaller(User.class), user -> {
CompletionStage<ActionPerformed> userCreated =
PatternsCS.ask(userActor, new CreateUserMessage(user), timeout)
.thenApply(obj -> (ActionPerformed) obj);
return onSuccess(() -> userCreated, performed -> {
return complete(StatusCodes.CREATED, performed, Jackson.marshaller());
});
})));
}
}
Теперь здесь довольно много шаблонов, но обратите внимание, что мы следуем тому же шаблону, что и раньше , для операций сопоставления, на этот раз для маршрутов. Давайте немного разберемся.
В getUser()
мы просто оборачиваем входящий идентификатор пользователя в сообщение типа GetUserMessage
и пересылаем это сообщение нашему userActor
.
Как только актор обработал сообщение, вызывается обработчик onSuccess
, в котором мы завершаем
HTTP-запрос, отправляя ответ с определенным статусом HTTP и определенным телом JSON. Мы используем маршаллер Джексона для сериализации ответа, данного актером, в строку JSON.
В postUser()
мы делаем вещи немного по-другому, так как мы ожидаем тело JSON в HTTP-запросе. Мы используем метод entity()
для сопоставления входящего тела JSON с объектом User
, прежде чем обернуть его в CreateUserMessage
и передать его нашему актеру. Опять же, мы используем Джексона для сопоставления между Java и JSON и наоборот.
Поскольку HttpApp
ожидает, что мы предоставим один объект Route , мы объединяем оба
маршрута
в один в методе route. Здесь мы используем директиву пути
, чтобы, наконец, указать URL-адрес, по которому должен быть доступен наш API.
Мы привязываем маршрут, предоставленный postUser()
, к пути /users
. Если входящий запрос не является POST-запросом, Akka автоматически перейдет в ветку orElse
и ожидает, что путь будет /users/<id>
, а метод HTTP — GET.
Если метод HTTP — GET, запрос будет переадресован на маршрут getUser()
. Если пользователь не существует, Akka вернет HTTP-статус 404 (не найдено). Если метод не является ни POST, ни GET, Akka вернет HTTP-статус 405 (метод не разрешен).
Для получения дополнительной информации о том, как определить HTTP-маршруты с помощью Akka, ознакомьтесь с документацией Akka .
5. Запуск сервера
После того, как мы создали реализацию HttpApp
, как показано выше, мы можем запустить наш HTTP-сервер с помощью пары строк кода:
public static void main(String[] args) throws Exception {
ActorSystem system = ActorSystem.create("userServer");
ActorRef userActor = system.actorOf(UserActor.props(), "userActor");
UserServer server = new UserServer(userActor);
server.startServer("localhost", 8080, system);
}
Мы просто создаем ActorSystem
с одним актором типа UserActor
и запускаем сервер на локальном
хосте .
6. Заключение
В этой статье мы узнали об основах Akka HTTP на примере, показывающем, как настроить HTTP-сервер и предоставить конечные точки для создания и загрузки ресурсов, аналогично REST API.
Как обычно, представленный здесь исходный код можно найти на GitHub .