«1. Обзор

В этом руководстве с помощью моделей Akka’s Actor \u0026 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. Центральный.

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/\u003cid\u003e, а метод 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.