简体   繁体   中英

Using play.api.cache.Cache with compile time dependency injection

I am trying to migrate my Play application from 2.3.9 to 2.4.3 and am using compile time dependency injection. I am getting an InstantiationException when using the old global Cache api object ( play.api.cache.Cache ). I have included EhCacheComponents in my components (which provides a cache implementation) but it seems Play is trying to instantiate the abstract CacheApi directly:

play.api.http.HttpErrorHandlerExceptions$$anon$1: Execution exception[[InstantiationException: play.api.cache.CacheApi]]

<snip>

Caused by: java.lang.InstantiationException: play.api.cache.CacheApi
at java.lang.Class.newInstance(Class.java:427) ~[na:1.8.0_51]
at play.api.inject.NewInstanceInjector$.instanceOf(Injector.scala:49) ~[play_2.11-2.4.3.jar:2.4.3]
at play.api.inject.SimpleInjector$$anonfun$instanceOf$1.apply(Injector.scala:85) ~[play_2.11-2.4.3.jar:2.4.3]

I am aware that the recommendation is to use the new dependency injected components, but the documentation suggests this should still work, and I would like to get my application running without having to change it all in one go.

Here is a simplified application which demonstrates the problem:

class AppApplicationLoader extends ApplicationLoader {
  def load(context : play.api.ApplicationLoader.Context) : play.api.Application = {
    Logger.configure(context.environment)
    new AppComponents(context).application
  }
}

class AppComponents(context : play.api.ApplicationLoader.Context) extends BuiltInComponentsFromContext(context) with EhCacheComponents {
  lazy val assets = new controllers.Assets(httpErrorHandler)
  lazy val router: Router = new Routes(httpErrorHandler, assets, new controllers.TestController())
}

-

package controllers

class TestController extends Controller {
    def test = Action {
        Cache.getAs[String]("hello") map { result => 
            Ok(result)
        } getOrElse {
            Ok("not found")
        }
    }
}

Configuration:

# key, langs, etc. removed
play.application.loader = "AppApplicationLoader"
play.modules.enabled += "play.api.cache.EhCacheModule"

How can I make this work?

It is possible to do this by replacing the default injector and adding more components. I suppose this is no longer compile-time DI (as dependencies are now being resolved up at runtime), but it works.

When extending BuiltInComponents :

trait AppComponents(context: Context) extends BuiltInComponents 
  with I18nComponents 
  with EhCacheComponents {

    // other dependencies (e.g. router, assets) here

    //need to add any other components here that you want to reference via the global APIs  - 
    //e.g. csrfConfig from CSRFComponents      
    override lazy val injector: Injector = new SimpleInjector(
      NewInstanceInjector
    ) + router + crypto + httpConfiguration + defaultCacheApi  + messagesApi
}

Unfortunately you cannot reference super.injector because it is a lazy val , so you are forced to redefine what is already in BuiltInComponents , which isn't great. When upgrading Play in future it would be important to check that any new components added to the base definitions are copied to the new implementation.

In my actual application I am using MacWire , so I have written a custom injector:

class MacwireInjector(fallback: Injector, wired: Wired) extends Injector {

  /**
    * Get an instance of the given class from the injector.
    */
  def instanceOf[T](implicit ct: ClassTag[T]) = instanceOf(ct.runtimeClass.asInstanceOf[Class[T]])

  /**
    * Get an instance of the given class from the injector.
    */
   def instanceOf[T](clazz: Class[T]) = wired.lookup(clazz) match {
    case instance :: Nil => instance
    case Nil => fallback.instanceOf(clazz)
    case set => throw new RuntimeException(s"Multiple instances returned for $clazz: $set")
  }

  /**
    * Get an instance bound to the given binding key.
    */
  def instanceOf[T](key: BindingKey[T]) = instanceOf(key.clazz)
}

BuiltInComponents:

// probably need to do this otherwise MacWire finds two candidates from EhCacheComponents
lazy val cacheApi = defaultCacheApi

override lazy val injector: Injector = new MacwireInjector(NewInstanceInjector, wiredInModule(this))

or using the default injector with MacWire as a fallback:

override lazy val injector: Injector = new SimpleInjector(
  new MacwireInjector(NewInstanceInjector, wiredInModule(this))
) + router + crypto + httpConfiguration

If you use Compile Time dependency injection, you should pass the dependencies via the parameters of your objects:

class TestController(cache: CacheApi) extends Controller {
  ...
}

And pass the implementation in the app loader:

class AppComponents(context : play.api.ApplicationLoader.Context) extends BuiltInComponentsFromContext(context) with EhCacheComponents {
  lazy val assets = new controllers.Assets(httpErrorHandler)
  lazy val controller = new controllers.TestController(defaultCacheApi)
  lazy val router: Router = new Routes(httpErrorHandler, assets, controller)
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM