Gradle で始める Payara 5

f:id:Naotsugu:20201229231421p:plain


はじめに

いまさら何故? という気はしますが、JavaEE 8 の環境を (Payara Micro ではなく) Payara 5 の embedded モードで動かす手順です。


プロジェクトの作成

Gradle 6.7 を使います。

プロジェクトディレクトリを作成して gradle init タスクでプロジェクトを作成します。

$ mkdir javaee8-starter
$ cd javaee8-starter
$ gradle init

gradle init のオプションは以下のように選択します。

Select type of project to generate:
  2: application
Enter selection (default: basic) [1..4] 2

Select implementation language:
  3: Java
Enter selection (default: Java) [1..6] 3

Split functionality across multiple subprojects?:
  1: no - only one application project
Enter selection (default: no - only one application project) [1..2] 1

Select build script DSL:
  2: Kotlin
Enter selection (default: Groovy) [1..2] 2

Select test framework:
  4: JUnit Jupiter
Enter selection (default: JUnit 4) [1..4] 4

Project name (default: javaee8-starter):
Source package (default: javaee8.starter):

ビルドスクリプトには Gradle Kotlin DSL を使うこととしました。


Payara を組み込みモードで動かす

作成されたプロジェクトの app/build.gradle.kts を以下のように編集します。

plugins {
    application
}

repositories {
    jcenter()
}

dependencies {
    implementation("fish.payara.extras:payara-embedded-all:5.2020.7")
    testImplementation("org.junit.jupiter:junit-jupiter-api:5.6.2")
    testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
}

application {
    mainClass.set("javaee8.starter.App")
}

tasks.test {
    useJUnitPlatform()
}

fish.payara.extras:payara-embedded-all:5.2020.7 を依存に追加しました。


App.java を編集して Payara 5 を embedded で起動します。

package javaee8.starter;

import org.glassfish.embeddable.BootstrapProperties;
import org.glassfish.embeddable.GlassFish;
import org.glassfish.embeddable.GlassFishProperties;
import org.glassfish.embeddable.GlassFishRuntime;

public class App {

    public static void main(String[] args) throws Exception {

        BootstrapProperties bootstrap = new BootstrapProperties();
        GlassFishRuntime runtime = GlassFishRuntime.bootstrap(bootstrap);

        GlassFishProperties properties = new GlassFishProperties();
        properties.setPort("http-listener", 8083);
        properties.setPort("https-listener", 8184);

        GlassFish glassfish = runtime.newGlassFish(properties);
        glassfish.start();
    }

}

GlassFishRuntime から newGlassFish() することで GlassFish のインスタンスが得られます。

glassfish.start() にてサーバが起動できます。以下で実行してみましょう。

$ gradlew run

アプリケーションのデプロイは行っていないため、404 になりますが、サーバは起動できました。

f:id:Naotsugu:20201229141243p:plain


後ほど行いますが、アプリケーションのデプロイは以下のようにすることができます。

Path war = Paths.get("path/to/app.war");
Deployer deployer = glassfish.getDeployer();
deployer.deploy(war.toFile(),
        "--name", "app",
        "--contextroot", "app",
        "--force", "true");


JUL(Java Util Logger) から SLF4J への変更

先程起動したサーバは JULによりログ出力されて見にくいため、SLF4J に変更しておきましょう。

JULのプロパティファイルを作成してシステムプロパティで設定します。

app/src/main/resources/logging.properties に以下のようなプロパティファイルを作成します。

handlers=org.slf4j.bridge.SLF4JBridgeHandler

ロガーのハンドラとして org.slf4j.bridge.SLF4JBridgeHandler を指定する設定となります。

このブリッジを使うには app/build.gradle.ktsorg.slf4j:jul-to-slf4j の依存を追加します。

org.slf4j:slf4j-apich.qos.logback:logback-classic とともに追加しましょう。

dependencies {
    // ...
    implementation("org.slf4j:slf4j-api:1.7.30")
    implementation("ch.qos.logback:logback-classic:1.2.3")
    implementation("org.slf4j:jul-to-slf4j:1.7.30")
}

App.java のメインメソッドの先頭に以下を追加することで、 SLF4J によるログ出力が行われます。

System.setProperty("java.util.logging.config.file",
        App.class.getResource("/logging.properties").getPath());

ログの設定は、logback の設定ファイルを app/src/main/resources/logback.xml に作成することで行うことができます。

例えば以下のようになります。

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <!-- encoders are assigned the type
             ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="debug">
        <appender-ref ref="STDOUT" />
    </root>
</configuration>


buildSrc の追加

先程作成したサーバにデプロイするアプリケーションを作成するために、プロジェクトをマルチモジュールプロジェクトに変えます。

これに伴い、ビルドスクリプトを Gradle のベストプラクティスに従い buildSrc を利用したものに変更していきます。


buildSrc ディレクトリを以下のように作成し、 build.gradle.kts を作成します。

$ mkdir -p buildSrc/src/main/kotlin
$ touch buildSrc/build.gradle.kts

作成した buildSrc/build.gradle.kts は以下のように編集します。

plugins {
    `kotlin-dsl`
}

repositories {
    gradlePluginPortal()
}

この辺はお決まりのパターンです。詳細は以下を参照してください。

blog1.mammb.com


conventions.gradle.kts を作成していきます。

今回は以下の3つを作成することにします。

$ touch buildSrc/src/main/kotlin/javaee8.starter.java-application-conventions.gradle.kts
$ touch buildSrc/src/main/kotlin/javaee8.starter.java-common-conventions.gradle.kts
$ touch buildSrc/src/main/kotlin/javaee8.starter.java-war-conventions.gradle.kts

最後の javaee8.starter.java-war-conventions.gradle.kts は、この後で作成する war モジュール用のスクリプトになります。

それぞれを以下のように編集します。


  • javaee8.starter.java-application-conventions.gradle.kts
plugins {
    id("javaee8.starter.java-common-conventions")
    application
}

application {
    applicationDefaultJvmArgs = listOf("--add-opens", "java.base/jdk.internal.loader=ALL-UNNAMED")
}

デプロイ時のリフレクションでエラーとなるため、実行時のコマンドライン引数に --add-opens を指定しています。


  • javaee8.starter.java-common-conventions.gradle.kts
plugins {
    java
}

repositories {
    jcenter()
}

dependencies {
    implementation("org.slf4j:slf4j-api:1.7.30")
    implementation("ch.qos.logback:logback-classic:1.2.3")
    implementation("org.slf4j:jul-to-slf4j:1.7.30")

    testImplementation("org.junit.jupiter:junit-jupiter-api:5.6.2")
    testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
}

tasks.test {
    useJUnitPlatform()
}

java {
    toolchain {
        languageVersion.set(JavaLanguageVersion.of(11))
    }
}

Payara 5 は Java 11 サポートとなっているため、toolchain で定義しました。

Toolchain については以下を参照してください。

blog1.mammb.com


  • javaee8.starter.java-war-conventions.gradle.kts
plugins {
    id("javaee8.starter.java-common-conventions")
    war
}

dependencies {
    annotationProcessor("org.hibernate:hibernate-jpamodelgen:5.4.27.Final")
    providedCompile("fish.payara.extras:payara-embedded-all:5.2020.7")
}

後ほど利用するため JPA のアノテーションプロセッサを追加しておきました(eclipseLink のものは使いにくいので hibernate-jpamodelgen を使っています)。


作成した conventions に合わせて app/build.gradle.kts を以下のように変更します。

plugins {
    id("javaee8.starter.java-application-conventions")
}

dependencies {
    implementation("fish.payara.extras:payara-embedded-all:5.2020.7")
    runtimeOnly(project(":war", "archives"))
}

application {
    mainClass.set("javaee8.starter.App")
}

依存には、これから作成する war モジュールを runtimeOnly として追加しています。

これで buildSrc の準備は完了しましたので、war を作成するモジュールを準備しましょう。


war モジュールの作成

war モジュール用のディレクトリを作成します。

$ mkdir -p war/src/main/java/javaee8/starter
$ mkdir -p war/src/main/resources/META-INF

build.gradle.kts も作成しましょう。

$ touch war/build.gradle.kts

war/build.gradle.kts を以下のように編集します。

plugins {
    id("javaee8.starter.java-war-conventions")
}


追加したモジュールは settings.gradle.kts にインクルード設定します。

rootProject.name = "javaee8-starter"
include("app", "war")

これで war 用のモジュールが作成できました。


ここまでで、ディレクトリ構成は以下のようになります。

f:id:Naotsugu:20201229223845p:plain


Hello サーブレット

簡単なサーブレットを作成します。

$ touch war/src/main/java/javaee8/starter/HelloServlet.java

以下のように編集します。

package javaee8.starter;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

@WebServlet(name = "hello", urlPatterns = { "/hello" })
public class HelloServlet extends HttpServlet {

    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        response.setContentType("text/html; charset=UTF-8");
        PrintWriter writer = response.getWriter();
        writer.println("<html>");
        writer.println("<html><head><title>HelloServlet</title></head>");
        writer.println("<body><h1>Hello Servlet</h1></body>");
        writer.println("</html>");
    }
}

@WebServlet でアノテートすることで、web.xml などの設定ファイルは不要です。

では実行してみましょう。とその前にデプロイする処理を書きます。


アプリケーションのデプロイ

App.java を編集してデプロイ処理を追加します。

glassfish.start(); 以下に追加しました。

public class App {

    public static void main(String[] args) throws Exception {

        System.setProperty("java.util.logging.config.file",
                App.class.getResource("/logging.properties").getPath());

        BootstrapProperties bootstrap = new BootstrapProperties();
        GlassFishRuntime runtime = GlassFishRuntime.bootstrap(bootstrap);

        GlassFishProperties properties = new GlassFishProperties();
        properties.setPort("http-listener", 8083);
        properties.setPort("https-listener", 8184);

        GlassFish glassfish = runtime.newGlassFish(properties);
        glassfish.start();

        File war = Arrays.stream(System.getProperty("java.class.path").split(":"))
                .filter(s -> s.endsWith(".war"))
                .map(File::new)
                .findFirst()
                .orElseThrow();
        Deployer deployer = glassfish.getDeployer();
        deployer.deploy(
                war,
                "--name", "app",
                "--contextroot", "app",
                "--force", "true");
    }

}

システムプロパティからクラスパスを取得し、その中に含まれる war ファイルを見つけてデプロイ処理しています。

今回は、Java9以降のモジュールシステムを利用していないため、単にクラスパスから取得したものを利用していますが、モジュールシステムを利用する場合には ModuleLayer などから取得する必要があると思います(がここでは触れません)。


実行しましょう。

$ gradlew run

http://localhost:8083/app/hello にアクセスすれば以下のようにサーブレットの実行結果が得られます。

f:id:Naotsugu:20201229222350p:plain


Hello JAX-RS

ついでに JAX-RS も入れておきましょう。

JAX-RS を有効化するため、Application を継承したクラスを作成します。

package javaee8.starter;

import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;

@ApplicationPath("rs")
public class JaxRsActivator extends Application {
}


リソースとして以下を作成します。

package javaee8.starter;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

@Path("hello")
public class HelloResource {

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public Greeting get() {
        return new Greeting("Hello", "Thom");
    }

    public static class Greeting {
        private String message;
        private String name;
        public Greeting(String message, String name) {
            this.message = message;
            this.name = name;
        }
        public String getMessage() { return message; }
        public String getName() { return name; }
    }
}

JavaEE 8 からは JSON-B が入っているので、何も考えずに、オブジェクトをリターンすれば以下のような JSON が得られます。

f:id:Naotsugu:20201229224705p:plain


まとめ

Payara 5 を Gradle のマルチモジュールプロジェクトで embedded モードとして動かす方法について見てきました。

ちなみに、つい先日(2020年11月22日) Jakarta EE 9 がリリースとなりましたが、javax-> jakarta への名前空間の変更(Eclipse Transformerというツールで変換可能)と、古いAPIのプルーニングが主なので、あまり大きなインパクトはありません。