Reputation: 57815
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?
Upvotes: 4
Views: 965
Reputation: 57815
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
Upvotes: 2
Reputation: 2404
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)
}
Upvotes: 1