Статьи

Инъекция доменных объектов вместо компонентов инфраструктуры

Dependency Injection — это широко используемый шаблон проектирования программного обеспечения в Java (и многих других языках программирования), который используется для достижения Inversion of Control . Это способствует повторному использованию, тестируемости, ремонтопригодности и помогает создавать слабосвязанные компоненты. В наше время Dependency Injection является стандартом де-факто для объединения объектов Java.

Различные Java-фреймворки, такие как Spring или Guice, могут помочь в внедрении Dependency Injection. Начиная с Java EE 6 также существует официальный API Java EE для внедрения зависимостей: контексты и внедрение зависимостей (CDI).

Мы используем Dependency Injection для внедрения сервисов, репозиториев, связанных с доменом компонентов, ресурсов или значений конфигурации. Однако, по моему опыту, часто упускается из виду, что Dependency Injection также можно использовать для внедрения объектов домена.

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

Код для этого может выглядеть примерно так:

01
02
03
04
05
06
07
08
09
10
public class SomeComponent {
 
  @Inject
  private AuthService authService;
   
  public void workWithUser() {
    User loggedInUser = authService.getLoggedInUser();
    // do something with loggedInUser
  }
}

Здесь экземпляр AuthService внедряется в SomeComponent. Методы SomeComponent теперь используют объект AuthService для получения экземпляра вошедшего в систему пользователя.

Однако вместо внедрения AuthService мы могли бы внедрить зарегистрированного пользователя непосредственно в SomeComponent.

Это может выглядеть так:

01
02
03
04
05
06
07
08
09
10
public class SomeComponent {
 
  @Inject
  @LoggedInUser
  private User loggedInUser;
   
  public void workWithUser() {
    // do something with loggedInUser
  }
}

Здесь объект User напрямую внедряется в SomeComponent, и экземпляр AuthService не требуется. Пользовательская аннотация @LoggedInUser используется, чтобы избежать конфликтов, если существует более одного (управляемого) компонента типа User.

Как Spring, так и CDI способны к этому типу инъекции (и конфигурация на самом деле очень похожа). В следующем разделе мы увидим, как объекты домена могут быть внедрены с помощью Spring. После этого я опишу, какие изменения необходимы, чтобы сделать то же самое с CDI.

Внедрение доменного объекта с помощью Spring

Чтобы добавить объекты домена, как показано в примере выше, нам нужно сделать только два небольших шага.

Сначала мы должны создать аннотацию @LoggedInUser:

1
2
3
4
5
6
7
8
9
import java.lang.annotation.*;
import org.springframework.beans.factory.annotation.Qualifier;
 
@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface LoggedInUser {
 
}

Обратите внимание на аннотацию @Qualifier, которая превращает @LoggedInUser в пользовательский квалификатор. Spring использует квалификаторы, чтобы избежать конфликтов, если доступно несколько бинов одного типа.

Далее мы должны добавить определение bean-компонента в нашу конфигурацию Spring. Здесь мы используем конфигурацию Java Spring, то же самое можно сделать с конфигурацией xml.

01
02
03
04
05
06
07
08
09
10
@Configuration
public class Application {
 
  @Bean
  @LoggedInUser
  @Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)
  public User getLoggedInUser() {
    // retrieve and return user object from server/database/session
  }
}

Внутри getLoggedInUser () мы должны извлечь и вернуть экземпляр текущего пользователя, вошедшего в систему (например, запросив AuthService из первого фрагмента). С @Scope мы можем контролировать объем возвращаемого объекта. Наилучшая область зависит от объектов домена и может отличаться для разных объектов домена. Для объекта User, представляющего зарегистрированного пользователя, область действия запроса или сеанса будет допустимым выбором. Аннотируя getLoggedInUser () с @LoggedInUser, мы говорим Spring использовать это определение bean-компонента всякий раз, когда должен быть введен bean-компонент с типом User, аннотированный @LoggedInUser.

Теперь мы можем внедрить зарегистрированного пользователя в другие компоненты:

1
2
3
4
5
6
7
8
9
@Component
public class SomeComponent {
 
  @Autowired
  @LoggedInUser
  private User loggedInUser;
   
  ...
}

В этом простом примере аннотация квалификатора на самом деле не нужна. Пока доступно только одно определение компонента типа User, Spring может внедрить зарегистрированного пользователя по типу. Однако при внедрении объектов домена может легко случиться так, что у вас будет несколько определений бинов одного типа. Таким образом, использование дополнительной аннотации квалификатора является хорошей идеей. С их описательным именем квалификаторы также могут выступать в качестве документации (если названы правильно).

Упростить определения bean-компонента Spring

При внедрении многих доменных объектов есть вероятность, что вы в конечном итоге будете повторять настройку области действия и прокси-сервера снова и снова в своей конфигурации бина. В такой ситуации удобно, когда аннотации Spring можно использовать для пользовательских аннотаций. Итак, мы можем просто создать нашу собственную аннотацию @SessionScopedBean, которую можно использовать вместо @Bean и @Scope:

1
2
3
4
5
6
7
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Bean
@Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)
public @interface SessionScopedBean {
 
}

Теперь мы можем упростить определение компонента до следующего:

1
2
3
4
5
6
7
8
9
@Configuration
public class Application {
 
  @LoggedInUser
  @SessionScopedBean
  public User getLoggedInUser() {
    ...
  }
}

Java EE и CDI

Конфигурация с CDI почти такая же. Единственное отличие состоит в том, что мы должны заменить аннотации Spring аннотациями javax.inject и CDI.

Таким образом, @LoggedInUser должен быть аннотирован javax.inject.Qualifier вместо org.springframework.beans.factory.annotation.Qualifier (см. Использование квалификаторов ).

Определение bean-компонента Spring можно заменить методом источника CDI . Вместо @Scope можно использовать соответствующую аннотацию области видимости CDI .

В точке ввода Spring @Autowired можно заменить на @Inject.

Обратите внимание, что Spring также поддерживает аннотации javax.inject. Если вы добавите зависимость javax.inject в свой проект Spring, вы также можете использовать @Inject и @ javax.inject.Qualifier. На самом деле это хорошая идея, потому что это уменьшает зависимости Spring в вашем Java-коде.

Вывод

Мы можем использовать пользовательские аннотации и объекты bean-объекта для добавления объектов домена в другие компоненты. Внедрение объектов домена может облегчить чтение вашего кода и привести к более чистым зависимостям. Если вы вводите только AuthService для получения вошедшего в систему пользователя, вы фактически зависите от вошедшего в систему пользователя, а не от AuthService.

С другой стороны, он сильнее связывает ваш код с платформой Dependency Injection, которая должна управлять областями бина для вас. Если вы хотите оставить возможность использовать ваши классы вне контейнера внедрения зависимостей, это может стать проблемой.

Какие типы доменных объектов подходят для внедрения, во многом зависит от приложения, над которым вы работаете. Хорошие кандидаты — это доменные объекты, которые вы часто используете и которые не зависят от какого-либо метода или параметров запроса. Зарегистрированный в данный момент пользователь — это объект, который часто может быть пригоден для инъекции.

  • Вы можете найти источник показанного примера на GitHub .