Внедрение зависимостей с абстрактным классом и объектом в Play Framework 2.5
Я пытаюсь перейти с Play 2.4 на 2.5, избегая устаревших вещей.
У меня был abstract class Microservice
из которого я создал несколько объектов. Некоторые функции Microservice
класс используется play.api.libs.ws.WS
делать HTTP-запросы, а также play.Play.application.configuration
читать конфигурацию.
Раньше все, что мне было нужно, это импорт:
import play.api.libs.ws._
import play.api.Play.current
import play.api.libs.concurrent.Execution.Implicits.defaultContext
Но теперь вы должны использовать внедрение зависимостей, чтобы использоватьWS
а также использовать доступ к текущему приложению Play.
У меня есть что-то вроде этого (сокращенно):
abstract class Microservice(serviceName: String) {
// ...
protected lazy val serviceURL: String = play.Play.application.configuration.getString(s"microservice.$serviceName.url")
// ...and functions using WS.url()...
}
Объект выглядит примерно так (сокращенно):
object HelloWorldService extends Microservice("helloWorld") {
// ...
}
К сожалению, я не понимаю, как я помещаю все вещи (WS, Configuration, ExecutionContect) в абстрактный класс, чтобы заставить его работать.
Я попытался изменить это на:
abstract class Microservice @Inject() (serviceName: String, ws: WSClient, configuration: play.api.Configuration)(implicit context: scala.concurrent.ExecutionContext) {
// ...
}
Но это не решает проблему, потому что теперь мне тоже нужно поменять объект, и я не могу понять, как.
Я пытался повернуть object
в @Singleton class
, лайк:
@Singleton
class HelloWorldService @Inject() (implicit ec: scala.concurrent.ExecutionContext) extends Microservice ("helloWorld", ws: WSClient, configuration: play.api.Configuration) { /* ... */ }
Я перепробовал все виды комбинаций, но я никуда не попал и чувствую, что здесь я не совсем на правильном пути.
Любые идеи, как я могу использовать такие вещи, как WS правильно (не используя устаревшие методы), не усложняя ситуацию?
2 ответа
Это больше связано с тем, как Guice обрабатывает наследование, и вы должны делать именно то, что сделали бы, если бы вы не использовали Guice, который объявляет параметры суперклассу и вызывает супер-конструктор в ваших дочерних классах. Guice даже предлагает это в своих документах:
Везде, где возможно, используйте конструктор для создания неизменяемых объектов. Неизменяемые объекты просты, доступны для совместного использования и могут быть составлены.
Внедрение в конструктор имеет некоторые ограничения:
- Подклассы должны вызывать super() со всеми зависимостями. Это делает внедрение конструктора громоздким, особенно когда изменяется введенный базовый класс.
В чистой Java это будет означать что-то вроде этого:
public abstract class Base {
private final Dependency dep;
public Base(Dependency dep) {
this.dep = dep;
}
}
public class Child extends Base {
private final AnotherDependency anotherDep;
public Child(Dependency dep, AnotherDependency anotherDep) {
super(dep); // guaranteeing that fields at superclass will be properly configured
this.anotherDep = anotherDep;
}
}
Внедрение зависимостей не изменит этого, и вам просто нужно добавить аннотации, чтобы указать, как внедрить зависимости. В этом случае, так как Base
класс abstract
, а затем никаких случаев Base
может быть создан, мы можем пропустить это и просто аннотировать Child
учебный класс:
public abstract class Base {
private final Dependency dep;
public Base(Dependency dep) {
this.dep = dep;
}
}
public class Child extends Base {
private final AnotherDependency anotherDep;
@Inject
public Child(Dependency dep, AnotherDependency anotherDep) {
super(dep); // guaranteeing that fields at superclass will be properly configured
this.anotherDep = anotherDep;
}
}
В переводе на Scala у нас будет что-то вроде этого:
abstract class Base(dep: Dependency) {
// something else
}
class Child @Inject() (anotherDep: AnotherDependency, dep: Dependency) extends Base(dep) {
// something else
}
Теперь мы можем переписать ваш код, чтобы использовать эти знания и избежать устаревших API:
abstract class Microservice(serviceName: String, configuration: Configuration, ws: WSClient) {
protected lazy val serviceURL: String = configuration.getString(s"microservice.$serviceName.url")
// ...and functions using the injected WSClient...
}
// a class instead of an object
// annotated as a Singleton
@Singleton
class HelloWorldService(configuration: Configuration, ws: WSClient)
extends Microservice("helloWorld", configuration, ws) {
// ...
}
Последний пункт является implicit
ExecutionContext
и здесь у нас есть два варианта:
- Используйте контекст выполнения по умолчанию, который будет
play.api.libs.concurrent.Execution.Implicits.defaultContext
- Используйте другие пулы потоков
Это зависит от вас, но вы можете легко ввести ActorSystem
искать диспетчер. Если вы решили использовать пользовательский пул потоков, вы можете сделать что-то вроде этого:
abstract class Microservice(serviceName: String, configuration: Configuration, ws: WSClient, actorSystem: ActorSystem) {
// this will be available here and at the subclass too
implicit val executionContext = actorSystem.dispatchers.lookup("my-context")
protected lazy val serviceURL: String = configuration.getString(s"microservice.$serviceName.url")
// ...and functions using the injected WSClient...
}
// a class instead of an object
// annotated as a Singleton
@Singleton
class HelloWorldService(configuration: Configuration, ws: WSClient, actorSystem: ActorSystem)
extends Microservice("helloWorld", configuration, ws, actorSystem) {
// ...
}
Как пользоваться HelloWorldService
?
Теперь нужно понять две вещи, чтобы правильно внедрить экземпляр HelloWorldService
где вам это нужно.
Отсюда HelloWorldService
получает свои зависимости?
У Guice Docs есть хорошее объяснение:
Внедрение зависимостиКак и фабрика, внедрение зависимостей - это просто шаблон проектирования. Основной принцип - отделить поведение от разрешения зависимостей.
Шаблон внедрения зависимостей приводит к модульному и тестируемому коду, а Guice облегчает его написание. Чтобы использовать Guice, мы сначала должны сказать ему, как сопоставить наши интерфейсы с их реализациями. Эта конфигурация выполняется в модуле Guice, который является любым классом Java, который реализует интерфейс модуля.
А затем Playframework объявляет модули для WSClient и для конфигурации. Оба модуля дают Guice достаточно информации о том, как построить эти зависимости, и есть модули для описания того, как построить зависимости, необходимые для WSClient
а также Configuration
, Опять же, у Guice docs есть хорошее объяснение:
При внедрении зависимостей объекты принимают зависимости в своих конструкторах. Чтобы построить объект, вы сначала строите его зависимости. Но для построения каждой зависимости вам нужны ее зависимости и так далее. Поэтому, когда вы строите объект, вам действительно нужно построить граф объектов.
В нашем случае для HelloWorldService
, мы используем инжектор конструктора, чтобы позволить Guice установить / создать наш граф объектов.
Как HelloWorldService
вводится?
Как WSClient
имеет модуль, чтобы описать, как реализация привязана к интерфейсу / признаку, мы можем сделать то же самое для HelloWorldService
, В Play docs есть четкое объяснение того, как создавать и настраивать модули, поэтому я не буду повторять это здесь.
Но после создания модуля, чтобы ввести HelloWorldService
для вашего контроллера, вы просто объявляете его как зависимость:
class MyController @Inject() (service: Microservice) extends Controller {
def index = Action {
// access "service" here and do whatever you want
}
}
В скале,
-> Если вы не хотите явно перенаправлять все введенные параметры в базовый конструктор, вы можете сделать это следующим образом:
abstract class Base {
val depOne: DependencyOne
val depTwo: DependencyTwo
// ...
}
case class Child @Inject() (param1: Int,
depOne: DependencyOne,
depTwo: DependencyTwo) extends Base {
// ...
}