mike-neckのブログ

Java or Groovy or Swift or Golang

jettyで実行可能warファイルを作る

こんにちわ、みけです。

だんだん寒くなってきて、一日の半分ほどは寝てるような状態です。

ここ数週間(!!!)、JavaFXサーブレットから叩くアプリケーションを作っているのですが、

最初は単純にwarファイルを作って、

jetty-runnerで走るようにするだけのものにしていたのですが、

どうしてもJavaFXの方のスレッドまわりでうまく動かない部分があって、

デバッグが若干面倒になってきたので、

実行可能warを作ることにしました。

(デバッグポートをつなげて、IDE上でデバッグしてもいいのですが、手順が若干面倒だったので)


実行可能warの作り方

実行可能warの作り方については、qiitaにたくさんのっています。

  1. jenkins.war のような実行可能 war ファイル作りたい
  2. Jetty組み込み方メモ

若干不満な点

僕は主にgradleを使っているので、2.の方は参考になるのですが、

下記の点で気に入らなかったので、

自分なりに修正を必要としました。

  • providedスコープで提供されるjarをIDEではcompileスコープにしないとIDEからmainの実行ができないこと(providedはIDEでの実行時にはclasspathに含まれないのでClassNotFoundExceptionが出ます)
  • @WebServlet@WebFilterなどのServlet3.0以降のアノテーションスキャンがmainからの実行では利用できないこと(元々はデバッグをやりたいために実行可能warを作ろうとしている)

jarのスコープ

gradleでdependencyのスコープを変更するのは比較的簡単です。

apply plugin: 'war'
apply plugin: 'idea'

ext {
    jdkLevel = 1.8
    encoding = 'UTF-8'
}
repositories {
    mavenCentral()
}
configurations {
    jetty
}
dependencies {
    jetty 'org.eclipse.jetty:jetty-runner:9.2.0.v20140526'
    testCompile 'junit:junit:4.11'
}
// (1)クラスパスを各種タスクで通す
tasks.withType(Compile) {
    sourceCompatibility = jdkLevel
    targetCompatibility = jdkLevel
    options.encoding = encoding
    classpath += configurations.jetty
}
javadoc {
    options.encoding = encoding
    classpath += configurations.jetty
}
test {
    classpath += configurations.jetty
}
// (2) IDEではコンパイルスコープになるように設定する
idea {
    project {
        languageLevel = jdkLevel
    }
    module {
        scopes.COMPILE.plus += [configurations.jetty]
    }
}

jettyというconfigurationを追加して、そこでjetty関連のjarを指定します。

(1)jetty configurationから下記のタスクに対してclasspathを通します。

(2)jetty configurationからIDEコンパイルスコープにjarを追加します。

これによって、gradleでのビルド時にはclasspathが通っていて、かつ、

IDEではコンパイルに利用できるように設定できます。

eclipse?知らない子ですね。

eclipse {
    classpath {
        plusConfigurations += [configurations.jetty]
    }
}

アノテーションスキャン

実行可能warを作るときに、デバッグ実行すると、

プロジェクト内で@WebServletなどのアノテーションを付与した

サーブレットがことごとくしかとされます。

広いインターネッツ、それへの対処法が全く見当たらず、

これの謎をとくのに、2日くらいかかりました。

僕が最初に書いたMainクラスはこんな感じでした。

Main.java

public class Main implements AutoCloseable, Runnable {

    private static final ClassLoader LOADER = Main.class.getClassLoader();

    private static final URL MAIN_URL = Main.class.getProtectionDomain()
            .getCodeSource().getLocation();

    public static void main(String[] args) throws Exception {
        // web.xmlはダミーでおいてあるだけで、空っぽのファイル
        Optional<URL> url = Optional.ofNullable(LOADER.getResource("WEB-INF/web.xml"));
        try(Main main = new Main(url)) {
            main.run();
        }

    public Main(Optional<URL> warFile) {
        this.server = new Server(8080);
        String war = warFile.map(u -> MAIN_URL.toExternalForm()).orElse("src/main/webapp");

        Configuration[] configurations = {
                new AnnotationConfiguration(),
                new WebInfConfiguration(),
                new WebXmlConfiguration(),
                new MetaInfConfiguration(),
                new FragmentConfiguration(),
                new EnvConfiguration(),
                new PlusConfiguration(),
                new JettyWebXmlConfiguration()
        };

        context = new WebAppContext();
        context.setWar(war);
        context.setContextPath("/");
        context.setConfigurations(configurations);
    }

    @Override
    public void run() {
        server.setHandler(context);
        try {
            server.start();
            server.join();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public void close() throws Exception {
        server.stop();
    }
}

HelloServlet.java

@WebServlet(name = "hello", urlPatterns = "hello")
public class HelloServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setStatus(200);
        PrintWriter writer = resp.getWriter();
        writer.append("hello").flush();
    }
}

で、これをwarにしてから実行する分には、

問題なく表示されます。

しかし、IDEから実行する場合にはもれなくHelloServlet

アノテーションスキャンの対象から外れて、

HelloServletのurlにアクセスしても404が返されます。


stackoverflowで探す

世の中、困ったらstackoverflowということで、ググったらありました。

Can't get Jetty to scan for annotated classes

曰く、

context.setAttribute("org.eclipse.jetty.server.webapp.ContainerIncludeJarPattern","out/production/project-name/.*");

の一文を追加しろとのこと。

で、追加するのですが、相変わらず404を返し続けます。

なんか、stackoverflow様でも解決できてないですね(´・ω・`)

Servlet3.0の仕様

同様の質問に対して、もう一つ別のページではServlet3.0の仕様を答えていたりします。

Embedded Jetty with annotated servlet patterns?

曰く、

WEB-INF/classesWEB-INF/libsのいずれかに入っている場合にスキャンされる

というわけで、どうにもならなくなってきたので、

コード読んでどうやってるかを理解することにしました。

jettyのソース

アノテーションのスキャンはここで行われています。

AnnotationConfiguration#scanForAnnotations(WebAppContext)

ここでは三つの場所からアノテーションをスキャンしています。

  • コンテナ
  • WEB-INF/classes
  • WEB-INF/libs

このうち、コンテナとWEB-INF/libsはjarファイルでないと実行時例外が出るので、

WEB-INF/classesをスキャンする際に、

ついでに自分のクラスファイルをスキャンしてもらうようにします。

で、Main.javaにこれを追加します。

context.getMetaData()
        .setWebInfClassesDirs(
                Arrays.asList(Resource.newResource(MAIN_URL)));

ただし、これは自分がwarファイルになっていると、

自分自身のクラスはWEB-INF/classesに配置されているので、

要らない子です。

デバッグ実行の場合はこの設定を追加して、

warファイルで実行する場合はこの設定を追加しないようにします。

if (!warFile.isPresent()) {
    context.getMetaData()
            .setWebInfClassesDirs(
                    Arrays.asList(Resource.newResource(MAIN_URL)));
}

これで、IDEデバッグにも対応した実行可能warファイルを出力するプロジェクトが出来ました。

最終的なMain.java

Main.java

public class Main implements AutoCloseable, Runnable {

    private static final ClassLoader LOADER = Main.class.getClassLoader();

    private static final URL MAIN_URL = Main.class.getProtectionDomain()
            .getCodeSource().getLocation();

    public static void main(String[] args) throws Exception {
        // web.xmlはダミーでおいてあるだけで、空っぽのファイル
        Optional<URL> url = Optional.ofNullable(LOADER.getResource("WEB-INF/web.xml"));
        try(Main main = new Main(url)) {
            main.run();
        }

    public Main(Optional<URL> warFile) {
        this.server = new Server(8080);
        String war = warFile.map(u -> MAIN_URL.toExternalForm()).orElse("src/main/webapp");

        Configuration[] configurations = {
                new AnnotationConfiguration(),
                new WebInfConfiguration(),
                new WebXmlConfiguration(),
                new MetaInfConfiguration(),
                new FragmentConfiguration(),
                new EnvConfiguration(),
                new PlusConfiguration(),
                new JettyWebXmlConfiguration()
        };

        context = new WebAppContext();
        context.setWar(war);
        context.setContextPath("/");
        if (!warFile.isPresent()) {
            context.getMetaData()
                    .setWebInfClassesDirs(
                            Arrays.asList(Resource.newResource(MAIN_URL)));
        }
        context.setConfigurations(configurations);
    }

    @Override
    public void run() {
        server.setHandler(context);
        try {
            server.start();
            server.join();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public void close() throws Exception {
        server.stop();
    }
}

最終的なbuild.gradle

build.gradle

apply plugin: 'idea'
apply plugin: 'war'

ext {
    jdkLevel = 1.8
    encoding = 'UTF-8'
}

version = '0.1'

repositories {
    mavenCentral()
}

configurations {
    jetty
}

dependencies {
    jetty 'org.eclipse.jetty:jetty-runner:9.2.0.v20140526'
    testCompile 'junit:junit:4.11'
}

tasks.withType(Compile) {
    sourceCompatibility = jdkLevel
    targetCompatibility = jdkLevel
    options.encoding = encoding
    classpath += configurations.jetty
}

javadoc {
    options.encoding = encoding
    classpath += configurations.jetty
}

test {
    classpath += configurations.jetty
}

idea {
    project {
        languageLevel = jdkLevel
    }
    module {
        scopes.COMPILE.plus += [configurations.jetty]
    }
}

war {
    manifest {
        attributes 'Main-Class': 'app.sample.Main'
    }
    from {
        configurations.jetty.collect {
            it.isDirectory()? it: zipTree(it)
        }
    } {
        exclude 'META-INF/*.SF'
        exclude 'META-INF/*.DSA'
        exclude 'META-INF/*.RSA'
    }
    from fileTree(dir: 'build/classes/main', include: '**/Main.class')
}

以上