web-dev-qa-db-de.com

Abhängigkeitsinjektion mit abstrakten Klassen und Objekten im Play Framework 2.5

Ich versuche, von Play 2.4 auf 2.5 zu migrieren, um veraltetes Material zu vermeiden.

Ich hatte einen abstract class Microservice, aus dem ich einige Objekte erstellt habe. Einige Funktionen der Klasse Microservice verwendeten play.api.libs.ws.WS für HTTP-Anforderungen und play.Play.application.configuration zum Lesen der Konfiguration.

Zuvor brauchte ich nur Importe wie:

import play.api.libs.ws._
import play.api.Play.current
import play.api.libs.concurrent.Execution.Implicits.defaultContext

Jetzt sollten Sie Abhängigkeitsinjektion verwenden, um WS und auch zu verwenden, um auf die aktuelle Play-Anwendung zuzugreifen.

Ich habe so etwas (verkürzt):

abstract class Microservice(serviceName: String) {
    // ...
    protected lazy val serviceURL: String = play.Play.application.configuration.getString(s"microservice.$serviceName.url")
    // ...and functions using WS.url()...
}

Ein Objekt sieht ungefähr so ​​aus (verkürzt):

object HelloWorldService extends Microservice("helloWorld") {
    // ...
}

Leider verstehe ich nicht, wie ich all das Zeug (WS, Konfiguration, ExecutionContect) in die abstrakte Klasse bekomme, damit es funktioniert.

Ich habe versucht es zu ändern:

abstract class Microservice @Inject() (serviceName: String, ws: WSClient, configuration: play.api.Configuration)(implicit context: scala.concurrent.ExecutionContext) {
    // ...
}

Aber das löst das Problem nicht, da ich jetzt auch das Objekt ändern muss und nicht herausfinden kann, wie.

Ich habe versucht, die object in einen @Singleton class umzuwandeln, wie:

@Singleton
class HelloWorldService @Inject() (implicit ec: scala.concurrent.ExecutionContext) extends Microservice ("helloWorld", ws: WSClient, configuration: play.api.Configuration) { /* ... */ }

Ich habe alle möglichen Kombinationen ausprobiert, komme aber nicht weiter und fühle mich hier nicht wirklich auf dem richtigen Weg.

Irgendwelche Ideen, wie ich Dinge wie WS richtig verwenden kann (ohne veraltete Methoden zu verwenden), ohne die Dinge so kompliziert zu machen?

17
Nick

Dies bezieht sich mehr darauf, wie Guice mit der Vererbung umgeht, und Sie müssen genau das tun, was Sie tun würden, wenn Sie Guice nicht verwenden würden. Dies bedeutet, dass die Parameter der Superklasse deklariert und der Superkonstruktor in Ihren Kindklassen aufgerufen wird. Guice schlägt es sogar an seine Dokumente :

Verwenden Sie nach Möglichkeit die Konstruktorinjektion, um unveränderliche Objekte zu erstellen. Unveränderliche Objekte sind einfach und können gemeinsam genutzt werden.

Konstruktorinjektion hat einige Einschränkungen:

  • Unterklassen müssen super () mit allen Abhängigkeiten aufrufen . Dies macht die Konstruktoreinspritzung umständlich, insbesondere wenn sich die eingespritzte Basisklasse ändert.

In reinem Java bedeutet dies Folgendes:

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;
  }
}

Die Abhängigkeitseingabe ändert daran nichts, und Sie müssen lediglich die Anmerkungen hinzufügen, um anzugeben, wie die Abhängigkeiten eingefügt werden sollen. Da in diesem Fall Base-Klasse abstract ist und keine Instanzen von Base erstellt werden können, können wir sie überspringen und lediglich Child-Klasse kommentieren:

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;
  }
}

Wenn wir nach Scala übersetzen, haben wir so etwas:

abstract class Base(dep: Dependency) {
  // something else
}

class Child @Inject() (anotherDep: AnotherDependency, dep: Dependency) extends Base(dep) {
  // something else
}

Jetzt können wir Ihren Code umschreiben, um dieses Wissen zu nutzen und veraltete APIs zu vermeiden:

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) {
    // ...
}

Der letzte Punkt ist die implicitExecutionContext und hier haben wir zwei Möglichkeiten:

  1. Verwenden Sie den Standardausführungskontext , der play.api.libs.concurrent.Execution.Implicits.defaultContext lautet.
  2. Verwenden Sie andere Thread-Pools

Dies hängt von Ihnen ab, Sie können jedoch leicht eine ActorSystem einfügen, um den Dispatcher nachzuschlagen. Wenn Sie sich für einen benutzerdefinierten Thread-Pool entscheiden, können Sie Folgendes tun:

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) {
    // ...
}

Wie verwende ich HelloWorldService?

Nun, es gibt zwei Dinge, die Sie verstehen müssen, um eine Instanz von HelloWorldService dort zu platzieren, wo Sie sie benötigen.

Woher bekommt HelloWorldService ihre Abhängigkeiten?

Guice docs hat eine gute Erklärung dazu:

Abhängigkeitsspritze

Wie in der Fabrik ist die Abhängigkeitseinspritzung nur ein Entwurfsmuster. Das Kernprinzip besteht darin, das Verhalten von der Abhängigkeitsauflösung zu trennen.

Das Abhängigkeitsinjektionsmuster führt zu Code, der modular und testbar ist, und Guice vereinfacht das Schreiben. Um Guice verwenden zu können, müssen wir ihm zunächst erklären, wie unsere Schnittstellen ihren Implementierungen zugeordnet werden können. Diese Konfiguration erfolgt in einem Guice-Modul, einer beliebigen Java-Klasse, die die Modulschnittstelle implementiert.

Playframework deklariert Module für WSClient und für Configuration . In beiden Modulen erhält Guice ausreichend Informationen zum Erstellen dieser Abhängigkeiten, und es gibt Module, die beschreiben, wie die für WSClient und Configuration erforderlichen Abhängigkeiten erstellt werden. Guice docs hat eine gute Erklärung dazu:

Bei der Abhängigkeitsinjektion akzeptieren Objekte Abhängigkeiten in ihren Konstruktoren. Um ein Objekt zu erstellen, müssen Sie zunächst seine Abhängigkeiten erstellen. Um jedoch jede Abhängigkeit aufzubauen, benötigen Sie ihre Abhängigkeiten und so weiter. Wenn Sie also ein Objekt erstellen, müssen Sie wirklich ein Objektdiagramm erstellen.

In unserem Fall für HelloWorldService verwenden wir Konstruktor Injektion , um Guice das Setzen/Erstellen unseres Objektgraphen zu ermöglichen.

Wie wird HelloWorldService injiziert?

So wie WSClient ein Modul enthält, das beschreibt, wie eine Implementierung an ein Interface/eine Eigenschaft gebunden ist, können wir dasselbe für HelloWorldService tun. Play docs hat eine klare Erklärung zum Erstellen und Konfigurieren von Modulen, daher werde ich es hier nicht wiederholen.

Aber nachdem Sie ein Modul erstellt haben, um eine HelloWorldService in Ihren Controller einzuspeisen, deklarieren Sie es einfach als Abhängigkeit:

class MyController @Inject() (service: Microservice) extends Controller {

    def index = Action {
        // access "service" here and do whatever you want 
    }
}
14
marcospereira

In Scala,

-> Wenn Sie nicht alle injizierten Parameter explizit an den Basiskonstruktor weiterleiten möchten, können Sie dies folgendermaßen tun:

abstract class Base {
  val depOne: DependencyOne
  val depTwo: DependencyTwo
  // ...
}

case class Child @Inject() (param1: Int,
                            depOne: DependencyOne,
                            depTwo: DependencyTwo) extends Base {
  // ...
}
1
Axel Borja