How do I perform an action on server startup in the Scala Play Framework?
Asked Answered
Z

1

19

I have a config file servers.conf in my conf/ directory that is read by my ServerController whenever the route /servers is hit. This isn't performant because it requires a re-read of the configuration file on each successive hit when the file won't change. Further if there are problems with the config file, I can tell the user ASAP rather than throw an exception on a page hit.

Currently I have this in my ServerController.scala:

case class Server(ip: String, port: String)

/**
  * This controller creates an `Action` to handle HTTP requests to the
  * application's server page.
  */
@Singleton
class ServerController @Inject() extends Controller {

  /**
    * Create an Action to render an HTML page with a the list of servers.
    * The configuration in the `routes` file means that this method
    * will be called when the application receives a `GET` request with
    * a path of `/servers`.
    */
  def index = Action {

    val serverList = ConfigFactory.load().getConfigList("servers")
    val servers: List[Server] = serverList match {
      case null => Nil
      case _ => serverList map { s =>
        Server(s.getString("ip"), s.getString("port"))
      } filter { s =>
        s.ip != null && s.port != null
      }.toList
    }

    Ok(views.html.servers(servers))
  }
}

My goal is to have the server read the config file at startup and pass the list of servers to the ServerController when the route is hit if there are no problems reading in the config file. If there are problems, I want an exception to be thrown immediately.

I can't seem to find an entry point for my application, though, so I don't know how to perform actions on startup.

Does anyone know how to do this? I'm using Play 2.5.x.

Zarate answered 6/4, 2016 at 14:20 Comment(3)
Which version of Play are you using?Bremer
@Anton Sorry. Edited the question.Zarate
Have you considered putting that entire block of code outside the index function? (It executes when the Controller is launched only once, i.e. any HTTP request made to that controller)Anhydride
W
21

If you're using the latest version of Play, it looks on startup for any class called Module that is in the root package (that is, there is no package definition at the top of the file). Here is an example taken from the latest Activator template for Play 2.5.x, which I have modified for demonstration of running code on application startup and shutdown:

In services/Say.scala, this would be a simple service to say "Hello!" on startup and "Goodbye!" when the application shuts down:

package services

import javax.inject._
import play.api.inject.ApplicationLifecycle
import scala.concurrent.Future

trait Say {
  def hello(): Unit
  def goodbye(): Unit
}

@Singleton
class SayImpl @Inject() (appLifecycle: ApplicationLifecycle) extends Say {  
    override def hello(): Unit = println("Hello!")
    override def goodbye(): Unit = println("Goodbye!")

    // You can do this, or just explicitly call `hello()` at the end
    def start(): Unit = hello()

    // When the application starts, register a stop hook with the
    // ApplicationLifecycle object. The code inside the stop hook will
    // be run when the application stops.
    appLifecycle.addStopHook { () =>
        goodbye()
        Future.successful(())
    }

    // Called when this singleton is constructed (could be replaced by `hello()`)
    start()
}

In Module.scala,

import com.google.inject.AbstractModule
import services._

/**
 * This class is a Guice module that tells Guice how to bind several
 * different types. This Guice module is created when the Play
 * application starts.

 * Play will automatically use any class called `Module` that is in
 * the root package. You can create modules in other locations by
 * adding `play.modules.enabled` settings to the `application.conf`
 * configuration file.
 */
class Module extends AbstractModule {

  override def configure() = {
    // We bind the implementation to the interface (trait) as an eager singleton,
    // which means it is bound immediately when the application starts.
    bind(classOf[Say]).to(classOf[SayImpl]).asEagerSingleton()
  }
}

Some further resources you may find useful are the Scala dependency injection (DI) documentation and the Guice documentation. Guice is the default DI framework used by Play.

Wenger answered 6/4, 2016 at 15:14 Comment(12)
Nice. This seems to be pretty close to what I want. Do you know in which directory root should live?Zarate
Well, it's not a directory named root. It's just a file that lives in the root package, which is to say it has no package declaration at the top. Alternatively, you can define the module somewhere else, say in a modules directory, and enable it in your application.conf like so: play.modules.enabled += "com.example.modules", where com.example.modules should be the package to which your module belongs.Wenger
Right, so my root package is expected to live in app/ by default? That is, if I put Say.scala in service/ and Module.scala lives in app/ packaged as root, it should work?Zarate
I updated my previous comment since I misunderstood what was being asked. It's in app/ by default in the template, but it doesn't have to be. Just don't add a package declaration to Module.scala and it should work.Wenger
This seems to only work once the user hits / for the first time. I can live with this, but is there no way to do this all before?Zarate
By the way, this is a fantastic answer.Zarate
@Zarate I'm fairly new to DI, but compile-time DI might be what you're looking for: playframework.com/documentation/2.5.x/…Wenger
Does this work without first making an HTTP request to startup play? I have code right now in my Controller and has the same behavior: A GET request is required for it to implement the Controller class. Anyway to get around this?Anhydride
@Anhydride I'm not sure what you're asking.Wenger
@Wenger I have a listener for a queue. And currently it forks from inside the Controller. The child thread does not start until someone makes a GET call to "/" or any other. I'm trying to eliminate the need to do this GET call. Will this method work for that purpose?Anhydride
If I understand correctly, this method will work for that purpose. If you try it and discover that it doesn't work, I think it would be better to post a separate question with a minimum working example rather than continuing the discussion in comments here.Wenger
Cool thanks @Eric. A question directly related to the above. Why do I need a trait here in this instance. Why can't I just have an implementation I want to bind as an eager singleton?Anhydride

© 2022 - 2024 — McMap. All rights reserved.