CLOVER🍀

That was when it all began.

組み込みTomcat+JAX-RS+CDIを、Fat JARとして動かす

こちらのエントリの続きです。

組み込みTomcatJAX-RS(RESTEasy)とCDIを使う
http://d.hatena.ne.jp/Kazuhira/20150308/1425780313

こちらを書いた時は、とりあえずsbt runで起動して動作確認したところまでですが、なんとなくFat JARにしてみたいものです。

というわけで、やってみました。やるために設定したファイルは、こちらになります。
pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>org.littlewings</groupId>
  <artifactId>embedded-tomcat-jaxrs-cdi</artifactId>
  <packaging>jar</packaging>
  <version>0.0.1-SNAPSHOT</version>

  <dependencies>
    <dependency>
      <groupId>org.apache.tomcat.embed</groupId>
      <artifactId>tomcat-embed-core</artifactId>
      <version>8.0.20</version>
    </dependency>
    <dependency>
      <groupId>org.apache.tomcat.embed</groupId>
      <artifactId>tomcat-embed-jasper</artifactId>
      <version>8.0.20</version>
    </dependency>
    <dependency>
      <groupId>org.apache.tomcat.embed</groupId>
      <artifactId>tomcat-embed-logging-juli</artifactId>
      <version>8.0.20</version>
    </dependency>

    <dependency>
      <groupId>org.jboss.resteasy</groupId>
      <artifactId>resteasy-servlet-initializer</artifactId>
      <version>3.0.10.Final</version>
    </dependency>
    <dependency>
      <groupId>org.jboss.resteasy</groupId>
      <artifactId>resteasy-cdi</artifactId>
      <version>3.0.10.Final</version>
    </dependency>

    <dependency>
      <groupId>org.jboss.weld.servlet</groupId>
      <artifactId>weld-servlet</artifactId>
      <version>2.2.9.Final</version>
    </dependency>

    <dependency>
      <groupId>org.scala-lang</groupId>
      <artifactId>scala-library</artifactId>
      <version>2.11.6</version>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <version>1.2.2.RELEASE</version>
        <executions>
          <execution>
            <goals>
              <goal>repackage</goal>
            </goals>
          </execution>
        </executions>
      </plugin>

      <plugin>
        <groupId>net.alchim31.maven</groupId>
        <artifactId>scala-maven-plugin</artifactId>
        <version>3.2.0</version>
        <executions>
          <execution>
            <goals>
              <goal>compile</goal>
              <goal>testCompile</goal>
            </goals>
          </execution>
        </executions>
        <configuration>
          <scalaVersion>2.11.6</scalaVersion>
          <args>
            <arg>-Xlint</arg>
            <arg>-unchecked</arg>
            <arg>-deprecation</arg>
            <arg>-feature</arg>
          </args>
          <recompileMode>incremental</recompileMode>
        </configuration>
      </plugin>
    </plugins>
  </build>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>
</project>

というわけで、Mavenになりました。

いや、sbt-assemblyとかちゃんと見てみたのですけど、JARファイルをJARの中に持つみたいな形式はサポートしてなさそうでして。sbt-one-jarというものもあるのですが、こちらは停止して長いようですし。

用途が用途なので、素直にMavenにしました。

で、よくよく見ると使っているのがSpring Boot Maven Pluginです。

    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <version>1.2.2.RELEASE</version>
        <executions>
          <execution>
            <goals>
              <goal>repackage</goal>
            </goals>
          </execution>
        </executions>
      </plugin>

なんか、ランチャーっぽくて単体でも使えそうな感じだったので、使ってみたらOKそうでした。

Spring Boot Maven plugin
http://docs.spring.io/spring-boot/docs/current/reference/html/build-tool-plugins-maven-plugin.html

executionを指定するところがポイントのようです。

        <executions>
          <execution>
            <goals>
              <goal>repackage</goal>
            </goals>
          </execution>
        </executions>

maven-shade-pluginというのも思い浮かびますが、依存するJARは展開したくないということと、unpackしない設定もできるようなのですがうまくやれずに挫折しました…。

コードは基本的に前回のままですが、mainクラスのみ再度載せます。
src/main/scala/org/littlewings/javaee7/TomcatBootstrap.scala

package org.littlewings.javaee7

import scala.io.StdIn

import java.io.File

import org.apache.catalina.startup.Tomcat
import org.apache.tomcat.util.descriptor.web.ContextResource

object TomcatBootstrap {
  def main(args: Array[String]): Unit = {
    val port = 8080
    val tomcat = new Tomcat

    // ポートはデフォルトで8080
    tomcat.setPort(port)

    try {
      // ベースのディレクトリ、DocbaseはSpring Bootを参考に
      tomcat.setBaseDir(createTempDir("tomcat", port).getAbsolutePath)
      val context =
        tomcat.addWebapp("", createTempDir("tomcat-docbase", port).getAbsolutePath)
  
      // CDIでWEB-INF/classesに配置されていなくても対象とされる、「flat」に設定
      context.addParameter("org.jboss.weld.environment.servlet.archive.isolation", "false")
      // RESTEasyとCDIの統合
      context.addParameter("resteasy.injector.factory", "org.jboss.resteasy.cdi.CdiInjectorFactory")

      // 組み込みTomcatでJNDIを有効に
      tomcat.enableNaming()

      // BeanManaerをJNDIリソースとして定義
      val resource = new ContextResource
      resource.setAuth("Container")
      resource.setName("BeanManager")
      resource.setType("javax.enterprise.inject.spi.BeanManager")
      resource.setProperty("factory", "org.jboss.weld.resources.ManagerObjectFactory")
      context.getNamingResources.addResource(resource)

      // Tomcatの起動
      tomcat.start()

      // Enter打ったら終了
      StdIn.readLine("> Enter stop")
      // 普通、待機はこっち
      // tomcat.getServer.await()

    } finally {
      // Tomcatの破棄と停止
      tomcat.stop()
      tomcat.destroy()
    }
  }

  def createTempDir(prefix: String, port: Int): File = {
    val tempDir = File.createTempFile(s"${prefix}.", s".${port}")
    tempDir.delete()
    tempDir.mkdir()
    tempDir.deleteOnExit()
    tempDir
  }
}

JAX-RSCDI管理Beanは省略。

で、こちらをパッケージング。

$ mvn package

実行!

$ java -jar target/embedded-tomcat-jaxrs-cdi-0.0.1-SNAPSHOT.jar

すると、立ち上がってくれそうな挙動をしますが

3 09, 2015 12:14:01 午前 org.apache.coyote.AbstractProtocol init
情報: Initializing ProtocolHandler ["http-nio-8080"]
3 09, 2015 12:14:01 午前 org.apache.tomcat.util.net.NioSelectorPool getSharedSelector
情報: Using a shared selector for servlet write/read

途中でJspServletが見つからないと言って、コケてくれます。

重大: Servlet [jsp] in web application [] threw load() exception
java.lang.ClassNotFoundException: org.apache.jasper.servlet.JspServlet

Fat JARの中にはJspServletは含まれているのですが(tomcat-jni-X.Y.Z.jarの中にいます)、これが見えていない様子。ClassLoaderっぽいですね。

Spring Bootではこういうことにならないので、細工があるのだろうと確認してみると、Contextに親ClassLoaderを設定すればよさそうです。

https://github.com/spring-projects/spring-boot/blob/v1.2.2.RELEASE/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactory.java#L166

Contextに対して親ClassLoaderを設定するには、1度StandardContextにキャストする必要があります。

なので、import文を追加して

import org.apache.catalina.core.StandardContext

取得したContextに対して、ClassLoaderを設定。

      val context =
        tomcat.addWebapp("", createTempDir("tomcat-docbase", port).getAbsolutePath)

      // 親ClassLoaderを、ContextClassLoaderに設定
      context
        .asInstanceOf[StandardContext]
        .setParentClassLoader(Thread.currentThread.getContextClassLoader)

ここまでやって、再度パッケージングして実行すると、今度は起動するようになります。

なんかJava EEとSpring Bootが入り乱れたみたいな感じになりましたけど、とりあえず目的は達成(?)できました。

こちらに置いているソースコードにも、反映してあります。
https://github.com/kazuhira-r/javaee7-scala-examples/tree/master/embedded-tomcat-jaxrs-cdi