Перейти к основному содержимому

Введение в Akka HTTP

· 5 мин. чтения

Задача: Сумма двух чисел

Напишите функцию twoSum. Которая получает массив целых чисел nums и целую сумму target, а возвращает индексы двух чисел, сумма которых равна target. Любой набор входных данных имеет ровно одно решение, и вы не можете использовать один и тот же элемент дважды. Ответ можно возвращать в любом порядке...

ANDROMEDA

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 .