Play Scala Dependency injection: How to use it
Asked Answered
M

2

9

I am trying to use Play 2.5 dependency injection. I have following class which makes a call to REST api and parses the response

class Client @Inject()(ws:WSClient, baseUrl: string) {

  def this(ws:WSClient) = this(ws, "<url string>")
  def getResponse() = {....}
....

}

The caller of the code looks like below

var client = new Client(WS.client)
client.getResponse()

I am getting following warning.

object WS in package ws is deprecated: Inject WSClient into your component

I understand that i need to inject WS.Client instead of passing it explicitly to the Client constructor. But how do i do that?

=== Update ===

I don't want to inject Client or WSClient from the Controller. My controller creates objects and classes at run time and i want those objects to create Client Object. When i explicitly pass WS.client object to the Client object i get the above stated warning.

=== Update 2 ===

I have a plugin architecture in my application. When a a controller starts an action. It does not know what set of plugins it is going to execute. Some plugins would not need a WSClient and some of them would. So i dont want to couple the injection of WSClient into my controller. Each plugin independently decides if it wants to call a remote service. When a plugin decides to call the remote service, it should be able to inject WSClient in what ever client it wants to invoke.

Controller Action --> Determine Plugins to Execute --> Execute Plugins ---> Plugin1 (needs to call a remote api, create a client object, per say new Client(WS.Client)). This is where the injection should happen, not at the controller.

Mohican answered 18/4, 2016 at 1:21 Comment(7)
Show the code where the warning is appearing. Are you sure you are using the injected WSClient there instead of WS?Carbonization
the warning is being shown on new Client(WS.client) line. My question is how to use use injected WSClient @CarbonizationMohican
Is this caller a test or other regular class in your project? Any reason to not use dependency injection to create and bind the Client?Carbonization
@marcospereira, that is my question, how do i inject the dependency? i dont know how to do that, can you share the code. This is a class in my project.Mohican
Post the class where this warning happens.Carbonization
Could you post some actual code?Carbonization
Did you ever get a good solution to this? (marco's answer is interesting, but quite verbose...)Traveled
C
12

Ok. I will assume you have two classes. First we will have your Client class:

@Singleton // this is not necessary, I put it here so you know this is possible
class Client @Inject() (ws:WSClient, baseUrl: String) {

    // Since this controller is not annotated with @Inject
    // it WILL NOT be used when binding components
    def this(ws:WSClient) = this(ws, "<url string>")

    def getResponse() = {
        // do something using ws object
    }
}

Then you have another class that uses Client, per instance, a controller:

class MyController @Inject() (client: Client) extends Controller {

    def someAction = Action {
        // do something with client object
    }

}

The main point here is that the controller did not need to create a Client instance. It was automatically injected by Guice.

Moreover, your client class needs a baseUrl and there is no place telling Play which value is needed there. If this is a configuration, than you can do something like this:

import play.api.Configuration

class Client @Inject() (ws:WSClient, configuration: Configuration) {

    def getResponse() = {
        val baseUrl = configuration.getString("key.to.baseUrl")
        // do something using ws object and baseUrl
    }
}

But, if you really want your Client object to receives a String, then we need to tell Play which String needs to be injected:

package com.acme.modules

import com.google.inject.AbstractModule
import com.google.inject.name.Names

class MyModule extends AbstractModule {
  def configure() = {
    bind(classOf[String])
      .annotatedWith(Names.named("baseUrl")) // attention to the name here. It will be used below
      .toInstance("http://api.example.com/")
  }
}

And then enable this module by adding the following line to your application.conf:

play.modules.enabled += "com.acme.modules.MyModule"

After that, we will change Client to be specific about which String it is expecting:

import play.api.Configuration

// @Named needs to receive the same value defined at the module class.
class Client @Inject() (ws:WSClient, @Named("baseUrl") baseUrl: String) {

    def getResponse() = {
        val baseUrl = configuration.getString("key.to.baseUrl")
        // do something using ws object and baseUrl
    }
}

Update after question edit:

Give the structure you want/need:

Controller Action --> Determine Plugins to Execute --> Execute Plugins ---> Plugin1

Your code can also follow that path with classes like this:

MyController -> PluginResolver -> Plugin
             -> PluginRunner ->

And, then, you can have:

Controller:

class MyController @Inject() (
    pluginResolver: PluginResolver,
    pluginRunner: PluginRunner
) extends Controller {

    def action = Action {
        val plugins = pluginsResolver.resolve(/* give a criteria to select plugins */)
        val someResultFromPluginsExecution = pluginsRunner.run(plugins)

        // map result from plugins execution to a play play.api.mvc.Result
        // return the play.api.mvc.Result
    }
}

Plugin classes:

import play.api.inject.Injector

class PluginResolver @Inject()(injector: Injector) {

    def resolve(/* some criteria to resolve plugins */): Seq[Plugin] = {
        val pluginsClasses = ... // find the necessary plugins based on the criteria
        pluginsClasses.map { pluginClass => injector.instanceOf(pluginClass) }
    }

}

// ExecutionContext is not really necessary, but maybe you want/need
// another thread pool to execute plugins
class PluginRunner @Inject()(implicit executionContext: ExecutionContext) {

    def run(plugins: Seq[Plugin]): Seq[PluginExecutionResult] = {
        // run the plugins
        // return the result
    }
}

trait Plugin {
    def execute(): PluginExecutionResult
}

The real magic here happens at the PluginResolver. It uses a play.api.inject.Injector to create plugins instances and then your plugins can use Dependency Injection. Per instance:

class PluginThatNeedsWSClient @Inject(wsClient: WSClient) extends Plugin {
    def execute(): PluginExecutionResult = {
        // Use wsClient to call a remote service
        // return the execution result
    }
}

Reference:

  1. Scala: Dependency Injection
  2. Scala: Play WS API
  3. play.api.inject.Injector
Carbonization answered 18/4, 2016 at 4:44 Comment(5)
thanks, but i dont want to inject Client or WSClient from the controller. My app design dynamically decides which object gets created. I dont want to pass Client object from controller to every object that was created by my controller. Can i inject WSClient without injecting Client from controller?Mohican
You can adapt the explanation above to your use case. Just inject the WSClient where the Client object is created and, instead of new Client(WS.client), do new Client(myInjectedWSClient).Carbonization
where would myInjectedWSClient be injected from? the controller? i dont want to inject any dependencies in the controller.Mohican
Why not? Moreover, is practically impossible to me to answer your question without knowing how your classes are structured. Where is the Client object being created? If it is being created at the controller, then you must inject WSClient at the controller.Carbonization
I updated the question. My controller would endup creating objects. Inside these objects i want to create new Client and inject WSClient instance.Mohican
B
3

I saw this awesome post last week: http://www.schibsted.pl/2016/04/dependency-injection-play-framework-scala/

Broz answered 18/4, 2016 at 8:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.