clojurewarboot-clj

Create a war with boot-clj


I want to create a war that I can deploy with tomcat. Using lein uberwar did the job just fine, however doing the same with boot doesn't seem to work. I can build a jar and run it, but the war fails with

Dec 09, 2015 12:15:31 AM org.apache.catalina.loader.WebappClassLoader validateJarFile INFO: validateJarFile(/var/lib/tomcat7/sites/geeknow.guru/DEBUG##0.1.7/WEB-INF/lib/javax.servlet-api-3.1.0.jar) - jar not loaded. See Servlet Spec 3.0, section 10.7.2. Offending class: javax/servlet/Servlet.class

I'm using the following build.boot

(set-env!
 :source-paths #{"src/clj"}
 :resource-paths #{"resources" "src/clj"}
 :dependencies '[[org.clojure/clojure "1.7.0"]
                 [clj-time "0.9.0"]
                 [org.clojure/java.jdbc "0.3.7"]
                 [org.postgresql/postgresql "9.4-1202-jdbc41"]
                 [yesql "0.5.1"]
                 [migratus "0.8.6"]
                 [markdown-clj "0.9.67"]
                 [jarohen/nomad "0.7.2"]
                 [com.draines/postal "1.11.3"]
                 [compojure "1.4.0"]
                 [ring/ring-core "1.4.0"]
                 [ring/ring-devel "1.4.0"]
                 [ring/ring-defaults "0.1.5"] 
                 [ring/ring-jetty-adapter "1.4.0"]
                 [ring-refresh "0.1.1"]
                 [ring-logger-timbre "0.7.5"]
                 [com.taoensso/timbre "4.1.4"]
                 [hiccup "1.0.5"]
                 [garden "1.3.0"]
                 [danlentz/clj-uuid "0.1.6"]
                 [speclj "3.3.1" :scope "test"]
                 [pandeiro/boot-http "0.7.1-SNAPSHOT"]])

(require '[pandeiro.boot-http :as http])
(require '[ring.middleware.reload :refer [wrap-reload]])
(require '[ring.adapter.jetty :as jetty])
(require '[ring.middleware.refresh :refer [wrap-refresh]])
(require '[blog.handler])

;;;;taken from boot-http.util
(defn resolve-sym [sym]
  (require (symbol (namespace sym)) :reload)
  (resolve sym))

(deftask ring-server
  []
  (comp (jetty/run-jetty (wrap-refresh (wrap-reload (resolve-sym 'blog.handler/app))) {:port 3000}) (wait)))

(deftask uberwar
  []
  (comp (aot) (pom) (web) (uber) (war)))

(deftask uberjar
  []
  (comp (aot) (pom) (uber) (jar)))

(task-options!
 pom {:project 'geeknow
      :version "0.1.7"}
 ;uber {:as-jars true}
 aot {:all true}
 jar {:main 'blog.core
      :manifest {"Description" "blog"}}
 web {:serve 'blog.handler/app}
 war {:main 'blog.core
      :manifest {"Description" "blog"}}
 repl {:init-ns 'blog.core})

Solution

  • Tomcat is complaining because one of the jars included in your war file contains a class that only the container environment can provide. The offending jar in the war is WEB-INF/lib/javax.servlet-api-3.1.0.jar.

    By default, the uber task adds all dependencies, direct and transitive, to the war at WEB-INF/lib when --as-jars is set to true. --as-jars is the preferred way of bundling dependencies for deployment to servlet containers.

    The problem appears to be that one of your direct dependencies is bringing in javax.servlet-api and uber is packaging it, causing the Tomcat problem.

    First we need to figure out which dependency is bringing in javax.servlet-api. boot show contains many options for diagnosing dependency problems like this. You can learn about them all with boot show -h. The one we want now is boot show -d, which prints the dependency tree.

    Here is the fragment of relevant output:

    [ring/ring-jetty-adapter "1.4.0"]
    ├── [org.eclipse.jetty/jetty-server "9.2.10.v20150310"]
    │   ├── [javax.servlet/javax.servlet-api "3.1.0"]
    │   ├── [org.eclipse.jetty/jetty-http "9.2.10.v20150310"]
    │   │   └── [org.eclipse.jetty/jetty-util "9.2.10.v20150310"]
    │   └── [org.eclipse.jetty/jetty-io "9.2.10.v20150310"]
    └── [ring/ring-servlet "1.4.0"]
    

    From this output, we know that our dependency on ring/ring-jetty-adapter is what's causing javax.servlet/javax.servlet-api to be brought in.

    Because this dependency is necessary for local development we don't want to omit it entirely. Instead, we can add the dependency with "test" scope:

    [ring/ring-jetty-adapter "1.4.0" :scope "test"]
    

    Scopes are a Maven concept for limiting the transitivity of dependencies in scenarios such as these. Boot uses Maven for its underlying dependency resolution machinery. uber is sensitive to Maven scopes, and can be configured to include or exclude various scopes depending on your needs. See boot uber -h for more information.

    By default, the uber task won't package dependencies in "test" scope. By marking ring/ring-jetty-adapter this way, we've excluded it from our uber war.

    The "test" scope is also useful for dependencies you might use in tests (of course!), or for deployment, or for other tasks during which you need a dependency but don't want to distribute it with your artifact, whether it's a library jar or an uberwar web app.