Статьи

Инъекция зависимостей без магии с JayWire

JayWire — это небольшая, простая в использовании библиотека Dependency Injection для Java 8, не требующая волшебства. Эта статья представляет собой краткое руководство о том, как начать использовать ее в проектах любого размера, от небольших одно- и до больших многомодульных.

Зачем нужна еще одна библиотека внедрения зависимостей?

Интересно, что ключевая особенность JayWire заключается не в том, что он может предоставлять объекты в качестве зависимостей другим объектам, а также в том, что объекты могут быть определены как имеющие область действия (например, одноэлементная область, область запроса и т. Д.). Все они поддерживаются любой другой структурой DI.

Отличительной особенностью этой библиотеки является то, что она не использует «магию» для достижения этих функций, и поэтому ее легче понять, она менее ограничена в отношении объектов, с которыми она может работать, и архитектур, которые она может поддерживать.

В частности, следующие технические решения все считаются «магическими», и их избегают в JayWire:

  • Classpath-сканирование
  • отражение
  • Аннотации
  • Улучшение байт-кода / ткачество, АОП
  • Прозрачные прокси-объекты
  • Генерация кода или специальные плагины компилятора
  • Скрытое статическое состояние

Таким образом, JayWire является на 100% Java-кодом, не навязывает свою модель программирования объектам, находящимся в управлении, статически безопасен, при необходимости может быть отлажен или зарегистрирован и может использоваться несколько раз в одной JVM.

Постановка проблемы: внедрение зависимости

Проблема на самом деле удивительно проста: некоторым объектам нужны другие объекты для работы. Эти «другие» объекты называются зависимостями, а акт предоставления этих зависимостей нуждающимся в них объектам называется инъекцией. Это выглядит так:

public interface Database {
}

public class PeopleRepository {
   public PeopleRepository(Database database) {
      ...
   }
}

В этом фрагменте PeopleRepository зависит от базы данных для работы, поэтому база данных является зависимостью PeopleRepository . Вот так выглядит инъекция:

new PeopleRepository(new SomeDatabaseImpl(...));

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

Запуск приложения

В какой-то момент во время запуска приложения все объекты, необходимые для «запуска» приложения, должны быть созданы, со всеми их зависимостями и рекурсивными зависимостями.

В средах на основе контейнеров, таких как CDI или Spring, этот процесс неявен и скрыт от разработчика. Однако относительно легко сделать эту важную часть приложения явной, а вместе с ней — поставить ее под контроль разработчика. Давайте попробуем написать «проводной» класс приложения, все еще без каких-либо платформ, для создания экземпляров всех объектов для запуска. Предполагая, что мы пытаемся запустить веб-сервер с двумя опубликованными службами, это будет выглядеть так:

public class MyApplication {
   public PeopleRepository getPeopleRepository() {
      ...
   }

   public GroupRepository getGroupRepository() {
      ...
   }

   public WebServer getWebServer() {
      return new WebServer(getPeopleRepository(), getGroupRepository());
   }
}

Обратите внимание, что WebServer имеет две зависимости, и обе «внедряются», используя простые вызовы методов. Здесь не нужно никакого волшебства. Затем MyApplication можно запустить с помощью следующего кода:

new MyApplication().getWebServer().start();

К сожалению, оба объекта хранилища по-прежнему зависят от базы данных , поэтому их необходимо добавить:

public class MyApplication {
   public Database getDatabase() {
      ...
   }

   public PeopleRepository getPeopleRepository() {
      return new PeopleRepository(getDatabase());
   }

   public GroupRepository getGroupRepository() {
      return new GroupRepository(getDatabase());
   }

   ...
}

«Синглтоны» с JayWire

Задача здесь состоит в том, чтобы реализовать метод getDatabase () , который будет создавать экземпляр базы данных только один раз и возвращать один и тот же экземпляр для всех последующих вызовов, поскольку обычно этот экземпляр объединяет соединения с базой данных, может синхронизировать вызовы и т. Д. Это все еще можно сделать без каких-либо рамок:

public class MyApplication {
   private Database database;

   public synchronized Database getDatabase() {
      if (database != null) {
         database = new SomeDatabaseImpl(...);
      }
      return database;
   }

   ...
}

Это, возможно, тот момент, когда шаблон становится неудобно большим и подверженным ошибкам. Вместо того, чтобы выбрасывать наш отличный код инициализации, JayWire может вмешаться и решить эту единственную проблему без каких-либо других изменений:

public class MyApplication extends StandaloneModule {
   public Database getDatabase() {
      return singleton( () -> new SomeDatabaseImpl(...) );
   }

   ...
}

Расширяя StandaloneModule , JayWire предлагает метод singleton () , который использует «фабрику», которая может создавать конкретный объект. Эта фабрика может быть легко реализована с использованием лямбда-выражений, как показано выше. JayWire будет использовать фабрику для создания экземпляра базы данных при первом вызове и использовать фабрику в качестве ключа для возврата того же экземпляра при всех последующих вызовах.

Обратите внимание, что информация о том, является ли база данных одноэлементной, не находится в реализации базы данных. Также обратите внимание, что реализация базы данных не изменилась, даже с аннотацией, и, следовательно, никак не зависит от JayWire.

Динамические объекты

Поддерживаются большинство традиционных «областей»:

  • одиночка
  • ThreadLocal
  • Запрос
  • сессия

Область «синглтон» не будет генерировать статический синглтон (один экземпляр на JVM), а синглетон относительно экземпляра приложения. Области запроса и сеанса доступны только в том случае, если они связаны с веб-контекстом (см. Интеграция JayWire ).

Иногда, когда границы области видимости пересекаются между объектом и его зависимостью, статически внедренного объекта недостаточно. Рассмотрим следующий код:

public class OrganizationService {
   public User currentUser;

   public OrganizationService(User currentUser) {
      this.currentUser = currentUser;
   }

   public List<Employee> getSubordinates() {
      return currentUser.getSubordinates();
   }
}

Проблема с этим фрагментом кода состоит в том, что OrganizationService является одноэлементным, а текущий пользователь является объектом области сеанса. Поэтому внедрение статического объекта User не работает. Другие структуры DI обычно решают эту проблему, фактически не внедряя объект User, а прозрачный прокси, который, опять же, скрывает важную информацию от разработчика.

Чтобы сделать это основное различие между службой и пользователем видимым, код может быть изменен на это:

import java.util.function.Supplier;

public class OrganizationService {
   public Supplier<User> currentUserSupplier;

   public OrganizationService(Supplier<User> currentUserSupplier) {
      this.currentUserSupplier = currentUserSupplier;
   }

   public List<People> getSubordinates() {
      return currentUserSupplier.get().getSubordinates();
   }
}

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

Все стандартные области в JayWire поддерживают этот вид явного косвенного обращения через поставщика следующим образом:

public class MyApplication extends SparkModule {
   public Supplier<User> getCurrentUserSupplier() {
      return sessionScope( () -> new User(...) );
   }

   public OrganizationService getOrganizationService() {
      return new singleton( () -> new OrganizationService(getCurrentUserSupplier()) );
   }
}

Собирается мультимодуль

To be able to scale up the object graph of the application, it needs to be possible to split the wiring code into different fragments or Modules. To do this, there needs to be a mechanism for defining module-level dependencies, objects which the module needs but does not manage.

Some frameworks, like Spring, have a solution to this (called “external beans” in this case), others, like CDI, just sidestep the issue with implicit “automatic” wiring based on interfaces.

JayWire, of course, does not only explicitly define inter-module dependencies, but makes them compile-time safe. If some module dependency is not fulfilled, the application will not compile!

The mechanism for this is quite simply abstract methods. Let’s assume a Module contains the previous two repository objects, but does not know what the database implementation should be or how it is instantiated. In this case the Module can be defined as:

public abstract class RepositoryModule extends StandaloneModule {
   public abstract Database getDatabase();

   public PeopleRepository getPeopleRepository() {
      return singleton( () -> new PeopleRepository(getDatabase()) );
   }

   public GroupRepository getGroupRepository() {
      return singleton( () -> new GroupRepository(getDatabase()) );
   }

   ...
}

Obviously this Module can not be instantiated as long as the Database dependency is not defined explicitly somewhere higher up the module dependency tree.

Dependency on Scopes

One problem with the above Module is that it extends the StandaloneModule directly to be able to access the standard JayWire scopes. This dependency is wrong most of the time, since a Module does not need to know whether the application it will be included in will be a standalone application, a JEE application, or a Spark application.

To avoid directly depending on a specific integration class, JayWire offers interfaces for all standard scopes, which can be used the following way:

public abstract class RepositoryModule implements SingletonScopeSupport {
   public abstract Database getDatabase();

   public PeopleRepository getPeopleRepository() {
      return singleton( () -> new PeopleRepository(getDatabase()) );
   }

   public GroupRepository getGroupRepository() {
      return singleton( () -> new GroupRepository(getDatabase()) );
   }

   ...
}

This Module still defines the same objects, but now has a “dependency” on the singleton scope (there is an abstract getSingletonScope() method through the SingletonScopeSupport interface), which somebody higher up has to provide, quite similarly to how a Database needs to be provided. So basically Scopes can be thought of as ordinary dependencies themselves.

Combining Modules

To start an application, all the relevant Modules need to be used, with all the missing dependencies filled/implemented at this point, including the Scope objects themselves.

Combining multiple Modules through inheritance is not possible, since that would be multiple inheritance. Combining multiple Modules through composition, while possible, would be quite complicated and redundant. Java 8, however, offers another (though still admittedly controversial) approach: Mixins.

Since the Modules themselves do not define any instance variables, only methods to create objects, it is possible to convert them to interfaces with default methods:

public interface RepositoryModule extends SingletonScopeSupport {
   Database getDatabase();

   default PeopleRepository getPeopleRepository() {
      return singleton( () -> new PeopleRepository(getDatabase()) );
   }

   default GroupRepository getGroupRepository() {
      return singleton( () -> new GroupRepository(getDatabase()) );
   }

   ...
}

This Module still contains the same knowledge, but now as a pure interface, and therefore can be combined freely.

At the top of the dependency graph, all the Modules can be combined in the following way:

public class MyApplication extends SparkModule implements
   RepositoryModule, DatabaseModule, WebPagesModule {

   @Override
   public Database getDatabase() {
      return getSomeSpecificDatabase();
   }
}

The Scopes are provided by extending a specific integration implementation of JayWire, and the application Modules are “mixed-in” through the interfaces implemented.

At this point, the decision can be made what database to use for the repository objects, and with that all dependencies are available to instantiate MyApplication. Please note, that if any dependencies are missing the compiler would indicate an error immediately.

Summary

This tutorial shows how JayWire can be used to explicitly and safely wire objects graphs of arbitrary size together. The resulting code is not only compile-time safe, but avoids magical tools that would make the process less transparent and less readable, and also does not impose any programming models or life-cycle restrictions on the objects used.

JayWire is available on GitHub: https://github.com/vanillasource/jaywire