简体   繁体   中英

Play Framework without dependency injection?

Without going into why, say someone wanted an old-fashioned Play Framework web service and did not want to use dependency injection nor rely on Google's Guice. Is it still possible in Play 2.8.x?

The api documentation along with the current Play examples recommend this as a "typical" HomeController.scala:

package controllers
import javax.inject._
import play.api.mvc._
class HomeController @Inject() (val controllerComponents: ControllerComponents) extends BaseController {
  def index = Action {
    Ok("It works!")
  }
}

My desired code is the same, but without @Inject() (similar to when I last used Play 2.4.0 in 2016)? Back in the day my code looked like this:

package controllers
import play.api.mvc.{Action, AnyContent, Controller}
object TestController {
  def index:Action[AnyContent] = Action {
    Ok("It used to work.")
  }
}

Console:

[info] Compiling 1 Scala source to /Volumes/.../play-scala-seed/target/scala-2.13/classes ...
[error] p.a.h.DefaultHttpErrorHandler - 

! @7ef69nl6l - Internal server error, for (GET) [/test/] ->

play.api.UnexpectedException: Unexpected exception[CreationException: Unable to create injector, see the following errors:

1) Could not find a suitable constructor in controllers.TestController. Classes must have either one (and only one) constructor annotated with @Inject or a zero-argument constructor that is not private.
  at controllers.TestController.class(TestController.scala:3)
  while locating controllers.TestController
    for the 4th parameter of router.Routes.<init>(Routes.scala:33)
  at play.api.inject.RoutesProvider$.bindingsFromConfiguration(BuiltinModule.scala:137):
Binding(class router.Routes to self) (via modules: com.google.inject.util.Modules$OverrideModule -> play.api.inject.guice.GuiceableModuleConversions$$anon$4)

1 error]
    at play.core.server.DevServerStart$$anon$1.reload(DevServerStart.scala:210)
    at play.core.server.DevServerStart$$anon$1.get(DevServerStart.scala:141)
    at play.core.server.AkkaHttpServer.handleRequest(AkkaHttpServer.scala:296)
    at play.core.server.AkkaHttpServer.$anonfun$createServerBinding$1(AkkaHttpServer.scala:186)
    at akka.stream.impl.fusing.MapAsync$$anon$30.onPush(Ops.scala:1261)
    at akka.stream.impl.fusing.GraphInterpreter.processPush(GraphInterpreter.scala:541)
    at akka.stream.impl.fusing.GraphInterpreter.execute(GraphInterpreter.scala:423)
    at akka.stream.impl.fusing.GraphInterpreterShell.runBatch(ActorGraphInterpreter.scala:624)
    at akka.stream.impl.fusing.GraphInterpreterShell$AsyncInput.execute(ActorGraphInterpreter.scala:501)
    at akka.stream.impl.fusing.GraphInterpreterShell.processEvent(ActorGraphInterpreter.scala:599)
Caused by: com.google.inject.CreationException: Unable to create injector, see the following errors:

1) Could not find a suitable constructor in controllers.TestController. Classes must have either one (and only one) constructor annotated with @Inject or a zero-argument constructor that is not private.
  at controllers.TestController.class(TestController.scala:3)
  while locating controllers.TestController
    for the 4th parameter of router.Routes.<init>(Routes.scala:33)
  at play.api.inject.RoutesProvider$.bindingsFromConfiguration(BuiltinModule.scala:137):
Binding(class router.Routes to self) (via modules: com.google.inject.util.Modules$OverrideModule -> play.api.inject.guice.GuiceableModuleConversions$$anon$4)

1 error
    at com.google.inject.internal.Errors.throwCreationExceptionIfErrorsExist(Errors.java:543)
    at com.google.inject.internal.InternalInjectorCreator.initializeStatically(InternalInjectorCreator.java:159)
    at com.google.inject.internal.InternalInjectorCreator.build(InternalInjectorCreator.java:106)
    at com.google.inject.Guice.createInjector(Guice.java:87)
    at com.google.inject.Guice.createInjector(Guice.java:78)
    at play.api.inject.guice.GuiceBuilder.injector(GuiceInjectorBuilder.scala:200)
    at play.api.inject.guice.GuiceApplicationBuilder.build(GuiceApplicationBuilder.scala:155)
    at play.api.inject.guice.GuiceApplicationLoader.load(GuiceApplicationLoader.scala:21)
    at play.core.server.DevServerStart$$anon$1.$anonfun$reload$3(DevServerStart.scala:189)
    at play.utils.Threads$.withContextClassLoader(Threads.scala:21)

Is there a simple fix to stay old school--without going here ?

I acknowledge but don't totally understand https://www.playframework.com/documentation/2.4.x/Migration24 . I assume my problem has to do with static routing having been removed in 2.7 .

My reputation doesn't allow me to comment to the answer by Mario Galic , but you can easily modify his example by using the "right" (non-test) controllerComponents that are provided by BuiltInComponentsFromContext .

The whole example would look like

class HomeController(override protected val controllerComponents: ControllerComponents)
  extends BaseController {
  def index = Action { Ok("It works!") }
}

class MyApplicationLoader extends ApplicationLoader {
  def load(context: ApplicationLoader.Context): Application = {
    new BuiltInComponentsFromContext(context) {
      lazy val homeController = HomeController(controllerComponents)
      override lazy val router: Router = Routes(httpErrorHandler, homeController)
    }.application
  }
}

To answer comment by @kujosHeist about an equivalent example in Java. This seems to work for me (Following the Play docs ):

package controllers;

import play.mvc.Controller;
import play.mvc.Result;

public class HomeController extends Controller {
    public Result index() {
        return ok("It works!");
    }
}
public class MyApplicationLoader implements ApplicationLoader {

    @Override
    public Application load(Context context) {
        return new MyComponents(context).application();
    }
}


class MyComponents extends BuiltInComponentsFromContext implements HttpFiltersComponents, AssetsComponents {

    public MyComponents(ApplicationLoader.Context context) {
        super(context);
    }

    @Override
    public Router router() {
        HomeController homeController = new HomeController();
        Assets assets = new Assets(scalaHttpErrorHandler(), assetsMetadata());
        return new router.Routes(scalaHttpErrorHandler(), homeController, assets).asJava();
    }

}

You might want to define things like eg error handlers differently, but this could be roughly the structure.

Indeed StaticRoutesGenerator has been removed which is need to have controllers as singleton objects. Perhaps using compile time dependency injection , with an example here , might bring you closer to what you were used to, however ControllerComponents will still need to be injected. Technically, it might be possible to do something ill-advised by putting play-test on the Compile classpath and make use stubControllerComponents like so

class HomeController extends BaseController {
  def index = Action { Ok("It works!") }

  override protected def controllerComponents: ControllerComponents = 
     play.api.test.Helpers.stubControllerComponents()
}

and corresponding minimal ApplicationLoader

class MyApplicationLoader extends ApplicationLoader {
  def load(context: ApplicationLoader.Context): Application = {
    new BuiltInComponentsFromContext(context) {
      override def httpFilters: Seq[EssentialFilter] = Nil
      lazy val homeController = new _root_.controllers.HomeController
      lazy val router: Router = new _root_.router.Routes(httpErrorHandler, homeController)
    }.application
  }
}

This way HomeController , although still a class, is now completely hardwired, and there is only a single instance of it created in ApplicationLoader .

Personally, I would advise against such shenanigans, and believe there are good arguments why Play moved away from singletons, for example, testability, thread-safety , etc.

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