web-dev-qa-db-fra.com

Injection de dépendance avec classe abstraite et objet dans Play Framework 2.5

J'essaie de migrer de Play 2.4 à 2.5 en évitant les choses obsolètes.

J'ai eu un abstract class Microservice à partir duquel j'ai créé des objets. Certaines fonctions de la classe Microservice utilisaient play.api.libs.ws.WS pour effectuer des requêtes HTTP et également play.Play.application.configuration pour lire la configuration.

Auparavant, je n'avais besoin que d'importations telles que:

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

Mais maintenant, vous devriez utiliser l'injection de dépendance pour utiliser WS et aussi pour utiliser l'accès à l'application Play en cours .

J'ai quelque chose comme ça (raccourci):

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

Un objet ressemble à quelque chose comme ça (raccourci):

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

Malheureusement, je ne comprends pas comment obtenir tous les éléments (WS, configuration, ExecutionContect) dans la classe abstraite pour que cela fonctionne.

J'ai essayé de le changer pour:

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

Mais cela ne résout pas le problème, car maintenant je dois aussi changer d'objet et je ne vois pas comment.

J'ai essayé de transformer la object en @Singleton class, par exemple:

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

J'ai essayé toutes sortes de combinaisons, mais je ne vais nulle part et je sens que je ne suis pas vraiment sur la bonne voie ici.

Des idées sur la façon dont je peux utiliser des choses comme WS de manière appropriée (sans utiliser de méthodes obsolètes) sans rendre les choses si compliquées?

17
Nick

Ceci est davantage lié à la façon dont Guice gère l'héritage et vous devez faire exactement ce que vous feriez si vous n'utilisiez pas Guice, qui déclare les paramètres à la superclasse et appelle le super constructeur à vos classes enfants. Guice le suggère même à sa documentation :

Dans la mesure du possible, utilisez l'injection de constructeur pour créer des objets immuables. Les objets immuables sont simples, partageables et peuvent être composés.

L'injection de constructeur présente certaines limites:

  • Les sous-classes doivent appeler super () avec toutes les dépendances . Cela complique l'injection du constructeur, d'autant plus que la classe de base injectée change.

En Java pur, cela signifie faire quelque chose comme ceci:

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

L'injection de dépendance ne changera pas cela et il vous suffira d'ajouter les annotations pour indiquer comment injecter les dépendances. Dans ce cas, puisque Base class est abstract et qu'aucune instance de Base ne peut être créée, nous pouvons le ignorer et simplement annoter Child class:

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

En traduisant en Scala, nous aurons quelque chose comme ceci:

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

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

Maintenant, nous pouvons réécrire votre code pour utiliser ces connaissances et éviter les API obsolètes:

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

Le dernier point est la implicitExecutionContext et nous avons ici deux options:

  1. Utilisez le contexte d'exécution par défaut }, qui sera play.api.libs.concurrent.Execution.Implicits.defaultContext
  2. Utilisez autres pools de threads

Cela dépend de vous, mais vous pouvez facilement injecter une ActorSystem pour rechercher le répartiteur. Si vous décidez d'utiliser un pool de threads personnalisé, vous pouvez procéder comme suit:

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

Comment utiliser HelloWorldService?

Vous devez maintenant comprendre deux choses pour pouvoir injecter correctement une instance de HelloWorldService où vous en avez besoin.

D'où HelloWorldService tire-t-il ses dépendances?

Guice docs a une bonne explication à ce sujet:

Injection de dépendance

A l'instar de l'usine, l'injection de dépendance n'est qu'un modèle de conception. Le principe de base est de séparer le comportement de la résolution de dépendance.

Le modèle d'injection de dépendance conduit à un code modulaire et testable, et Guice facilite l'écriture. Pour utiliser Guice, nous devons d’abord lui dire comment mapper nos interfaces sur leurs implémentations. Cette configuration est effectuée dans un module Guice, qui est une classe Java qui implémente l'interface du module.

Ensuite, Playframework déclare les modules pour WSClient et pour Configuration . Les deux modules fournissent à Guice suffisamment d’informations sur la création de ces dépendances, et certains modules expliquent comment créer les dépendances nécessaires pour WSClient et Configuration. Encore une fois, Guice Docs a une bonne explication à ce sujet:

Avec l'injection de dépendance, les objets acceptent les dépendances dans leurs constructeurs. Pour construire un objet, vous devez d'abord créer ses dépendances. Mais pour construire chaque dépendance, vous avez besoin de ses dépendances, etc. Ainsi, lorsque vous construisez un objet, vous devez vraiment créer un graphe d'objet.

Dans notre cas, pour HelloWorldService, nous utilisons injection de constructeur pour permettre à Guice de définir/créer notre graphe d'objets.

Comment HelloWorldService est injecté?

Tout comme WSClient a un module décrivant comment une implémentation est liée à une interface/trait, nous pouvons faire la même chose pour HelloWorldService. Play docs a une explication claire sur la création et la configuration de modules, je ne vais donc pas le répéter ici.

Mais après avoir créé un module, pour injecter une HelloWorldService à votre contrôleur, il vous suffit de le déclarer en tant que dépendance:

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

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

En scala,

-> Si vous ne voulez pas explicitement transférer tous les paramètres injectés au constructeur de base, vous pouvez le faire comme ceci:

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

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