xsbt-web-plugin is an sbt plugin for building web applications with Java servlets.
- Package a project as a .war file
- Test and run under Jetty or Tomcat
- Deploy directly to Heroku or AWS
- Supports sbt 0.13.6 and up
- Supports Scala 2.10.2 and up
- Look for earldouglas in sbt/sbt on Gitter
- Use the xsbt-web-plugin tag on Stack Overflow
- Submit a bug report or feature request as a new GitHub issue
- See examples in the examples/ directory
Add xsbt-web-plugin to project/plugins.sbt:
addSbtPlugin("com.earldouglas" % "xsbt-web-plugin" % "4.2.4")Enable the Jetty plugin:
build.sbt:
enablePlugins(JettyPlugin)From the sbt console:
- Start (or restart) the container with
jetty:start - Stop the container with
jetty:stop - Build a .war file with
package
To use Tomcat instead of Jetty:
- Substitute
TomcatPluginforJettyPlugin - Substitute
tomcat:startforjetty:start - Substitute
tomcat:stopforjetty:stop
sbt new earldouglas/xsbt-web-plugin.g8
Create a new empty project:
mkdir myproject
cd myproject
Set up the project structure:
mkdir project
mkdir -p src/main/scala/mypackage
Configure sbt:
project/build.properties:
sbt.version=1.6.2
project/plugins.sbt:
addSbtPlugin("com.earldouglas" % "xsbt-web-plugin" % "4.2.4")build.sbt:
scalaVersion := "3.0.1"
libraryDependencies += "javax.servlet" % "javax.servlet-api" % "3.0.1" % "provided"
enablePlugins(TomcatPlugin)Add a servlet:
src/main/scala/mypackage/MyServlet.scala:
package mypackage
import javax.servlet.annotation.WebServlet
import javax.servlet.http.HttpServlet
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
@WebServlet(urlPatterns = Array("/hello"))
class MyServlet extends HttpServlet:
println("HEY")
override def doGet(req: HttpServletRequest, res: HttpServletResponse): Unit =
res.setContentType("text/html")
res.setCharacterEncoding("UTF-8")
res.getWriter.write("""<h1>Hello, world!</h1>""")Run it with tomcat:start:
$ sbt
> tomcat:start
$ curl localhost:8080/hello
<h1>Hello, world!</h1>
xsbt-web-plugin supports sbt's triggered
execution
by prefixing commands with ~.
sbt console:
> ~jetty:start
This starts the Jetty container, then monitors the sources, resources, and webapp directories for changes, which triggers a container restart.
To run a projects tests against a running instance of the webapp, use
<container>:quicktest or <container>:test:
> ~jetty:quicktest
To pass extra arguments to the Jetty or Tomcat container, set
containerArgs:
containerArgs := Seq("--path", "/myservice")- For available Jetty arguments, see the Jetty Runner docs
- For available Tomcat arguments, see webapp-runner#options
To use a custom J2EE container, e.g. a main class named runner.Run,
enable ContainerPlugin and set containerLibs and
containerLaunchCmd:
enablePlugins(ContainerPlugin)
containerLibs in Container := Seq(
"org.eclipse.jetty" % "jetty-webapp" % "9.1.0.v20131115"
, "org.eclipse.jetty" % "jetty-plus" % "9.1.0.v20131115"
, "test" %% "runner" % "0.1.0-SNAPSHOT"
)
containerLaunchCmd in Container :=
{ (port, path) => Seq("runner.Run", port.toString, path) }sbt:
> container:start
> container:stop
To set system properties for the forked container JVM, set
containerForkOptions:
containerForkOptions := new ForkOptions(runJVMOptions = Seq("-Dh2g2=42"))Alternatively, set javaOptions in the Jetty (or Tomcat)
configuration:
javaOptions in Jetty += "-Dh2g2=42"To attach a debugger, set -Xdebug and -Xrunjdwp:
build.sbt:
javaOptions in Jetty ++= Seq(
"-Xdebug",
"-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8000"
)In Eclipse:
- Create and run a new Remote Java Application launch configuration
- Set Connection Type to Scala debugger (Socket Attach)
- Configure to connect to localhost on port 8000
In IntelliJ IDEA:
- Add a Remote run configuration: Run -> Edit Configurations...
- Under Defaults select Remote and push
+to add a new configuration - By default the configuration uses port 5005; update it to 8000 as above
- Name this configuration, and run it in debug mode
To enable debugging through
JDWP,
use jetty:debug or tomcat:debug. Optionally set debugAddress,
which defaults to "debug" under Windows and "8888" otherwise, and
debugOptions, which defaults to:
port =>
Seq( "-Xdebug"
, Seq( "-Xrunjdwp:transport=dt_socket"
, "address=" + port
, "server=y"
, "suspend=n"
).mkString(",")
)By default, Jetty
Runner
9.4.42 is used. To use a different version, set
containerLibs:
containerLibs in Jetty := Seq("org.mortbay.jetty" % "jetty-runner" % "7.0.0.v20091005" intransitive())Depending on the version, it may also be necessary to specify the name of Jetty's runner:
containerMain := "org.mortbay.jetty.runner.Runner"By default, the container runs on port 8080. To use a different port,
set containerPort:
containerPort := 9090To use a jetty.xml configuration file, set --config in
containerArgs:
containerArgs := Seq("--config", "/path/to/jetty.xml")This option can be used to enable SSL and HTTPS.
By default, Webapp Runner
9.0.41.0 is used. To use a different version, set containerLibs:
41
containerLibs in Tomcat := Seq("com.heroku" % "webapp-runner" % "8.5.61.0" intransitive())Depending on the version, it may also be necessary to specify the name of Tomcat's runner:
containerMain in Tomcat := "webapp.runner.launch.Main"Tomcat's webapp-runner does not ship with all of the libraries that can
be found in a complete Tomcat installation. To include extras, use
containerLibs in Tomcat:
containerLibs in Tomcat += "org.apache.tomcat" % "tomcat-jdbc" % "8.5.15"This can be useful for keeping the version number out of the .war file name, using a non-conventional file name or path, adding additional information to the file name, etc.
artifactName := { (v: ScalaVersion, m: ModuleID, a: Artifact) =>
a.name + "." + a.extension
}See "Modifying default artifacts" in the sbt documentation for additional information.
After the /target/webapp directory is prepared, it can be
modified with an arbitrary File => Unit function by setting
webappPostProcess.
To list the contents of the webapp directory after it is prepared:
webappPostProcess := {
val log = streams.value.log
webappDir: File =>
def listFiles(level: Int)(f: File): Unit = {
val indent = ((1 until level) map { _ => " " }).mkString
if (f.isDirectory) {
log.info(indent + f.getName + "/")
f.listFiles foreach { listFiles(level + 1) }
} else log.info(indent + f.getName)
}
listFiles(1)(webappDir)
}To include webapp resources from multiple directories in the prepared webapp directory:
webappPostProcess := {
webappDir: File =>
val baseDir = baseDirectory.value / "src" / "main"
IO.copyDirectory(baseDir / "webapp1", webappDir)
IO.copyDirectory(baseDir / "webapp2", webappDir)
IO.copyDirectory(baseDir / "webapp3", webappDir)
}Files in the extra resource directory are not compiled, and are bundled directly in the project artifact .jar file.
To add a custom resources directory, set unmanagedResourceDirectories:
unmanagedResourceDirectories in Compile += (sourceDirectory in Compile).value / "extra"Scala files in the extra source directory are compiled, and bundled in the project artifact .jar file.
To add a custom sources directory, set unmanagedSourceDirectories:
unmanagedSourceDirectories in Compile += (sourceDirectory in Compile).value / "extra"By default, project classes are packaged into a .jar file, shipped in
the WEB-INF/lib directory of the .war file. To instead keep them
extracted in WEB-INF/classes, set webappWebInfClasses:
webappWebInfClasses := trueThe web application destination directory is where the static Web content, compiled Scala classes, library .jar files, etc. are placed. By default, they go to /target/webapp.
To specify a different directory, set target in the webappPrepare
configuration:
target in webappPrepare := target.value / "WebContent"The web application resources directory is where static Web content (including .html, .css, and .js files, the web.xml container configuration file, etc. By default, this is kept in /src/main/webapp.
To specify a different directory, set sourceDirectory in the
webappPrepare configuration:
sourceDirectory in webappPrepare := (sourceDirectory in Compile).value / "WebContent"For situations when the prepared /target/webapp directory is needed, but the packaged .war file isn't.
sbt console:
webappPrepare
Manifest attributes of the .war file can be configured via
packageOptions in sbt.Keys.package in build.sbt:
packageOptions in sbt.Keys.`package` +=
Package.ManifestAttributes( java.util.jar.Attributes.Name.SEALED -> "true" )To configure the .war file to inherit the manifest attributes of the
.jar file, typically set via packageOptions in (Compile, packageBin), set inheritJarManifest to true:
inheritJarManifest := trueBy default, sbt will shutdown the running container when exiting sbt.
To allow the container to continue running after sbt exits, set
containerShutdownOnExit:
containerShutdownOnExit := falseEnable the HerokuDeploy plugin and configure your app name:
enablePlugins(HerokuDeploy)
herokuAppName := "my-heroku-app"Either install the Heroku Toolbelt, or
set your Heroku API key as an environment variable, launch sbt, and
deploy with herokuDeploy:
$ HEROKU_API_KEY="xxx-xxx-xxxx" sbt
> herokuDeploy
Check out your deployed application at
https://my-heroku-app.herokuapp.com.
Before trying to deploy anything, create an application and a Tomcat-based environment for it in Elastic Beanstalk.
Enable the ElasticBeanstalkDeployPlugin plugin, and configure your
application's name, environment, and region:
enablePlugins(ElasticBeanstalkDeployPlugin)
elasticBeanstalkAppName := "my-elastic-beanstalk-app"
elasticBeanstalkEnvName := "production"
elasticBeanstalkRegion := "us-west-1"Add AWS credentials to your environment, launch sbt, and deploy with
elasticBeanstalkDeploy:
$ AWS_ACCESS_KEY="xxx" AWS_SECRET_KEY="xxx" sbt
> elasticBeanstalkDeploy
Check out your deployed application at
http://my-elastic-beanstalk-app.us-west-1.elasticbeanstalk.com.
To start the container from the command line and block sbt from exiting
prematurely, use jetty:join:
$ sbt jetty:start jetty:join
This is useful for running sbt in production (e.g. in a Docker container).
Choose a Docker image that has sbt installed, and use sbt <container>:start <container>:join:
Using hseeberger/scala-sbt;
$ docker run -p 8080:8080 -v $PWD:/root hseeberger/scala-sbt sbt tomcat:start tomcat:join
Using bigtruedata/sbt:
$ docker run -p 8080:8080 -v $PWD:/app bigtruedata/sbt sbt tomcat:start tomcat:join
Test it out:
$ curl localhost:8080/hello
<html>
<body>
<h1>Hello, world!</h1>
</body>
</html>
First, package a .war file:
$ sbt package
[info] Packaging .../target/scala-2.10/getting-started_2.10-0.1-SNAPSHOT.war .
Run it using Jetty:
$ docker run -p 8080:8080 -v /path/to/myproject/target/scala-2.10:/var/lib/jetty/webapps jetty
Run it using Tomcat:
$ docker run -p 8080:8080 -v /path/to/myproject/target/scala-2.10:/usr/local/tomcat/webapps tomcat
Test it out:
$ curl localhost:8080/myproject_2.10-0.1-SNAPSHOT/hello
<html>
<body>
<h1>Hello, world!</h1>
</body>
</html>
Configure a Docker build with sbt-native-packager:
project/plugins.sbt:
addSbtPlugin("com.github.sbt" % "sbt-native-packager" % "1.9.11")build.sbt:
enablePlugins(DockerPlugin)
dockerBaseImage := "tomcat:9.0"
Docker / defaultLinuxInstallLocation := "/usr/local/tomcat/webapps"
dockerExposedVolumes := Seq((Docker / defaultLinuxInstallLocation).value)
Docker / mappings += sbt.Keys.`package`.value -> "/usr/local/tomcat/webapps/ROOT.war"
dockerEntrypoint := Seq("catalina.sh", "run")Build the project from sbt as a Docker image:
> docker:publishLocal
Run it:
$ docker run -it --rm -p 8080:8080 my-web-project:0.1.0-SNAPSHOT
The development cycle can be sped up by serving static resources directly from source, and avoiding packaging of compiled artifacts.
Use <container>:quickstart in place of <container>:start to run the
container in quickstart mode:
> jetty:quickstart
To launch using more than a single container, set containerScale:
containerScale := 5This will configure the container to launch in five forked JVMs, using
five sequential ports starting from containerPort.
In debug mode, five additional sequential debug ports starting from
debugPort will be opened.
The development cycle can be further sped up by skipping server restarts between code recompilation.
Add -agentpath to the container's JVM options:
javaOptions in Jetty += "-agentpath:/path/to/jrebel/lib/libjrebel64.so"
Launch the container with quickstart, and run triggered compilation:
> jetty:quickstart
> ~compile