«1. Введение

В этой статье мы сосредоточимся на интеграции Akka с Spring Framework, чтобы разрешить внедрение сервисов на основе Spring в актеры Akka.

Прежде чем читать эту статью, рекомендуется ознакомиться с основами Akka.

2. Внедрение зависимостей в Akka

Akka — это мощная среда приложений, основанная на модели параллелизма актеров. Фреймворк написан на Scala, что, конечно же, делает его полностью пригодным для использования в приложениях на основе Java. И поэтому очень часто нам нужно интегрировать Akka с существующим приложением на основе Spring или просто использовать Spring для подключения bean-компонентов к акторам.

Проблема с интеграцией Spring/Akka заключается в различии между управлением bean-компонентами в Spring и управлением акторами в Akka: у акторов есть определенный жизненный цикл, который отличается от типичного жизненного цикла bean-компонентов Spring.

Кроме того, акторы разделены на сам актор (который является деталью внутренней реализации и не может управляться Spring) и ссылку на актора, доступную для клиентского кода, а также сериализуемую и переносимую между различными средами выполнения Akka.

К счастью, Akka предоставляет механизм, а именно расширения Akka, которые делают использование внешних сред внедрения зависимостей довольно простой задачей.

3. Зависимости Maven

Чтобы продемонстрировать использование Akka в нашем проекте Spring, нам понадобится минимальная зависимость Spring — библиотека spring-context, а также библиотека akka-actor. Версии библиотеки можно извлечь в раздел \u003cproperties\u003e pom:

<properties>
    <spring.version>4.3.1.RELEASE</spring.version>
    <akka.version>2.4.8</akka.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>${spring.version}</version>
    </dependency>

    <dependency>
        <groupId>com.typesafe.akka</groupId>
        <artifactId>akka-actor_2.11</artifactId>
        <version>${akka.version}</version>
    </dependency>

</dependencies>

Обязательно проверьте Maven Central на наличие последних версий зависимостей spring-context и akka-actor.

Обратите внимание, что зависимость akka-actor имеет постфикс _2.11 в своем имени, что означает, что эта версия фреймворка Akka была создана для Scala версии 2.11. Соответствующая версия библиотеки Scala будет транзитивно включена в вашу сборку.

4. Внедрение Spring Beans в актеров Akka

Давайте создадим простое приложение Spring/Akka, состоящее из одного актера, который может отвечать на имя человека, отправляя приветствие этому человеку. Логика приветствия будет выделена в отдельный сервис. Мы хотим автоматически связать эту службу с экземпляром актора. Интеграция с Spring поможет нам в этой задаче.

4.1. Определение актора и службы

Чтобы продемонстрировать внедрение службы в актор, мы создадим простой класс GreetingActor, определенный как нетипизированный актор (расширение базового класса Akka UntypedActor). Основным методом каждого актора Akka является метод onReceive, который получает сообщение и обрабатывает его в соответствии с определенной логикой.

В нашем случае реализация GreetingActor проверяет, относится ли сообщение к предопределенному типу Greet, затем берет имя человека из экземпляра Greet, затем использует GreetingService для получения приветствия для этого человека и отвечает отправителю полученным сообщением. приветственная строка. Если сообщение имеет какой-то другой неизвестный тип, оно передается предварительно определенному необработанному методу актора.

Давайте посмотрим:

@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class GreetingActor extends UntypedActor {

    private GreetingService greetingService;

    // constructor

    @Override
    public void onReceive(Object message) throws Throwable {
        if (message instanceof Greet) {
            String name = ((Greet) message).getName();
            getSender().tell(greetingService.greet(name), getSelf());
        } else {
            unhandled(message);
        }
    }

    public static class Greet {

        private String name;

        // standard constructors/getters

    }
}

Обратите внимание, что тип сообщения Greet определен как статический внутренний класс внутри этого актора, что считается хорошей практикой. Принятые типы сообщений должны быть определены как можно ближе к актору, чтобы избежать путаницы в отношении того, какие типы сообщений может обрабатывать этот актор.

Также обратите внимание на аннотации Spring @Component и @Scope — они определяют класс как bean-компонент, управляемый Spring, с областью действия прототипа.

Область действия очень важна, потому что каждый запрос на получение компонента должен приводить к созданию вновь созданного экземпляра, поскольку такое поведение соответствует жизненному циклу актера Akka. Если вы реализуете этот бин с какой-то другой областью видимости, типичный случай перезапуска акторов в Akka, скорее всего, будет работать некорректно.

Наконец, обратите внимание, что нам не нужно было явно @Autowire экземпляр GreetingService — это возможно благодаря новой функции Spring 4.3 под названием Implicit Constructor Injection.

«Реализация GreeterService довольно проста, обратите внимание, что мы определили его как компонент, управляемый Spring, добавив к нему аннотацию @Component (с одноэлементной областью действия по умолчанию):

@Component
public class GreetingService {

    public String greet(String name) {
        return "Hello, " + name;
    }
}

4.2. Добавление поддержки Spring через расширение Akka

Самый простой способ интегрировать Spring с Akka — через расширение Akka.

Расширение — это отдельный экземпляр, созданный для каждой системы акторов. Он состоит из самого класса расширения, который реализует расширение интерфейса маркера, и класса идентификатора расширения, который обычно наследует AbstractExtensionId.

Поскольку эти два класса тесно связаны, имеет смысл реализовать класс Extension, вложенный в класс ExtensionId:

public class SpringExtension 
  extends AbstractExtensionId<SpringExtension.SpringExt> {

    public static final SpringExtension SPRING_EXTENSION_PROVIDER 
      = new SpringExtension();

    @Override
    public SpringExt createExtension(ExtendedActorSystem system) {
        return new SpringExt();
    }

    public static class SpringExt implements Extension {
        private volatile ApplicationContext applicationContext;

        public void initialize(ApplicationContext applicationContext) {
            this.applicationContext = applicationContext;
        }

        public Props props(String actorBeanName) {
            return Props.create(
              SpringActorProducer.class, applicationContext, actorBeanName);
        }
    }
}

Во-первых, SpringExtension реализует единственный метод createExtension из класса AbstractExtensionId, который учитывает для создания экземпляра расширения объект SpringExt.

Класс SpringExtension также имеет статическое поле SPRING_EXTENSION_PROVIDER, которое содержит ссылку на его единственный экземпляр. Часто имеет смысл добавить закрытый конструктор, чтобы явно указать, что SpringExtention должен быть одноэлементным классом, но мы его опустим для ясности.

Во-вторых, статический внутренний класс SpringExt является самим расширением. Поскольку расширение — это просто маркерный интерфейс, мы можем определить содержимое этого класса по своему усмотрению.

В нашем случае нам понадобится метод initialize для сохранения экземпляра Spring ApplicationContext — этот метод будет вызываться только один раз при инициализации расширения.

Также нам потребуется метод props для создания объекта Props. Экземпляр Props является чертежом актера, и в нашем случае метод Props.create получает класс SpringActorProducer и аргументы конструктора для этого класса. Это аргументы, с которыми будет вызываться конструктор этого класса.

Метод props будет выполняться каждый раз, когда нам понадобится ссылка на актор, управляемый Spring.

Третья и последняя часть головоломки — это класс SpringActorProducer. Он реализует интерфейс IndirectActorProducer от Akka, который позволяет переопределить процесс создания экземпляра актора путем реализации методов product и ActorClass.

Как вы, наверное, уже догадались, вместо прямого создания экземпляра всегда будет извлекаться экземпляр актора из Spring ApplicationContext. Поскольку мы сделали актор компонентом с областью действия прототипа, каждый вызов метода product будет возвращать новый экземпляр актора:

public class SpringActorProducer implements IndirectActorProducer {

    private ApplicationContext applicationContext;

    private String beanActorName;

    public SpringActorProducer(ApplicationContext applicationContext, 
      String beanActorName) {
        this.applicationContext = applicationContext;
        this.beanActorName = beanActorName;
    }

    @Override
    public Actor produce() {
        return (Actor) applicationContext.getBean(beanActorName);
    }

    @Override
    public Class<? extends Actor> actorClass() {
        return (Class<? extends Actor>) applicationContext
          .getType(beanActorName);
    }
}

4.3. Собираем все вместе

Единственное, что осталось сделать, это создать класс конфигурации Spring (помеченный аннотацией @Configuration), который сообщит Spring сканировать текущий пакет вместе со всеми вложенными пакетами (это обеспечивается аннотацию @ComponentScan) и создайте контейнер Spring.

Нам нужно только добавить один дополнительный компонент — экземпляр ActorSystem — и инициализировать расширение Spring в этой ActorSystem:

@Configuration
@ComponentScan
public class AppConfiguration {

    @Autowired
    private ApplicationContext applicationContext;

    @Bean
    public ActorSystem actorSystem() {
        ActorSystem system = ActorSystem.create("akka-spring-demo");
        SPRING_EXTENSION_PROVIDER.get(system)
          .initialize(applicationContext);
        return system;
    }
}

4.4. Извлечение акторов Spring-Wired

Чтобы проверить, что все работает правильно, мы можем внедрить экземпляр ActorSystem в наш код (либо код приложения, управляемого Spring, либо тест на основе Spring), создать объект Props для актора, используя наш расширения, получить ссылку на актора через объект Props и попытаться поприветствовать кого-нибудь:

ActorRef greeter = system.actorOf(SPRING_EXTENSION_PROVIDER.get(system)
  .props("greetingActor"), "greeter");

FiniteDuration duration = FiniteDuration.create(1, TimeUnit.SECONDS);
Timeout timeout = Timeout.durationToTimeout(duration);

Future<Object> result = ask(greeter, new Greet("John"), timeout);

Assert.assertEquals("Hello, John", Await.result(result, duration));

Здесь мы используем типичный шаблон akka.pattern.Patterns.ask, который возвращает экземпляр Future Scala. После завершения вычисления Future разрешается со значением, которое мы вернули в нашем методе GreetingActor.onMessasge.

Мы можем либо дождаться результата, применив метод Scala Await.result к Future, либо, что более предпочтительно, построить все приложение с асинхронными шаблонами.

5. Заключение

В этой статье мы показали, как интегрировать Spring Framework с Akka и bean-компонентами autowire в актеры.

Исходный код статьи доступен на GitHub.