2018/07/23

事例から学ぶ、Java SE 11移行 その2
-モジュールアプリケーションの場合-

このエントリーをはてなブックマークに追加

前回の続き。

もし、どうしてもモジュールアプリケーションにしたいという場合を考えてみます。

普通のアプリケーションであれば、そんなに急いでモジュール化する必要は私はないと思っています。ただし、ライブラリを開発している場合は、なるべく早くモジュールに対応してほしいです。

前回のサンプルはライブラリではないのですが、モジュール化する場合もあるでしょうし、とりあえずやってみましょう。

前回のサンプルはJersey + Grizzly の単純なアプリケーションでした。

このサンプルは 2 つのクラスから構成されています。リソースを表すMyResouceクラスと、アプリケーションの起動クラスとなるMainクラスです。いずれもcom.exampleパッケージにあります。

MyResourceクラスはこちら。

package com.example;

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

/**
 * Root resource (exposed at "myresource" path)
 */
@Path("myresource")
public class MyResource {

    /**
     * Method handling HTTP GET requests. The returned object will be sent
     * to the client as "text/plain" media type.
     *
     * @return String that will be returned as a text/plain response.
     */
    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String getIt() {
        return "Got it!";
    }
}

Mainクラスはこちらです。

package com.example;

import org.glassfish.grizzly.http.server.HttpServer;
import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory;
import org.glassfish.jersey.server.ResourceConfig;

import java.io.IOException;
import java.net.URI;

/**
 * Main class.
 *
 */
public class Main {
    // Base URI the Grizzly HTTP server will listen on
    public static final String BASE_URI = "http://localhost:8080/myapp/";

    /**
     * Starts Grizzly HTTP server exposing JAX-RS resources defined in this application.
     * @return Grizzly HTTP server.
     */
    public static HttpServer startServer() {
        // create a resource config that scans for JAX-RS resources and providers
        // in com.example package
        final ResourceConfig rc = new ResourceConfig().packages("com.example");

        // create and start a new instance of grizzly http server
        // exposing the Jersey application at BASE_URI
        return GrizzlyHttpServerFactory.createHttpServer(URI.create(BASE_URI), rc);
    }

    /**
     * Main method.
     * @param args
     * @throws IOException
     */
    public static void main(String[] args) throws IOException {
        final HttpServer server = startServer();
        System.out.println(String.format("Jersey app started with WADL available at "
                + "%sapplication.wadl\nHit enter to stop it...", BASE_URI));
        System.in.read();
        server.stop();
    }
}

pom.xmlも示しておきます。

<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>com.example</groupId>
    <artifactId>simple-service</artifactId>
    <packaging>jar</packaging>
    <version>1.0-SNAPSHOT</version>
    <name>simple-service</name>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.glassfish.jersey</groupId>
                <artifactId>jersey-bom</artifactId>
                <version>${jersey.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.glassfish.jersey.containers</groupId>
            <artifactId>jersey-container-grizzly2-http</artifactId>
        </dependency>
        <dependency>
            <groupId>org.glassfish.jersey.inject</groupId>
            <artifactId>jersey-hk2</artifactId>
        </dependency>

        <dependency>
            <groupId>javax.xml.bind</groupId>
            <artifactId>jaxb-api</artifactId>
            <version>2.3.0</version>
        </dependency>      
        <dependency>
            <groupId>com.sun.xml.bind</groupId>
            <artifactId>jaxb-impl</artifactId>
            <version>2.3.0.1</version>
        </dependency>
        <dependency>
            <groupId>javax.activation</groupId>
            <artifactId>javax.activation-api</artifactId>
            <version>1.2.0</version>
        </dependency>

        <!-- uncomment this to get JSON support:
         <dependency>
            <groupId>org.glassfish.jersey.media</groupId>
            <artifactId>jersey-media-json-binding</artifactId>
        </dependency>
        -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.9</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.7.0</version>
                <inherited>true</inherited>
                <configuration>
                    <source>11</source>
                    <target>11</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>exec-maven-plugin</artifactId>
                <version>1.6.0</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>java</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <mainClass>com.example.Main</mainClass>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <properties>
        <jersey.version>2.27</jersey.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
</project>

 

依存性を調べる

アプリケーションをモジュール化する場合、必須なのがモジュールを定義するmodule-info.javaの作成です。

基本的には、module-info.javaには使用するモジュールと、公開するパッケージを記述します。

しかし、どのモジュールを使っているかなんて、よく分からないですよね。そういう時に使用するのが、jdepsコマンドです。

では、さっそくjdepsコマンドを使ってみましょう。

jdepsはJARファイルもしくはクラスファイルを引数にして実行します。サンプルのJARファイルはMavenではtargetディレクトリに作成されるので、そこで実行させてみました。

C:\jersey-sample\simple-service\target>jdeps -s simple-service-1.0-SNAPSHOT.jar
simple-service-1.0-SNAPSHOT.jar -> java.base
simple-service-1.0-SNAPSHOT.jar -> 見つかりません

C:\jersey-sample\simple-service\target>

上記の例で使用している-sオプションはサマリー表示を行うためです。

この結果、java.baseモジュールを使っていることは分かります。java.baseモジュールはJavaの基本となるモジュールでjava.langパッケージなどが含まれています。

しかし、その他のモジュールは「見つかりません」と表示されてしまいます。

これはモジュールパスもしくはクラスパスが設定されていないためです。

しかし、Mavenを使っていると、他に使用しているライブラリはローカルレポジトリにあるため、クラスパスを設定するにもちょっとめんどうです。そこで、MavenのDependency Pluginを使用して、使用しているJARファイルを一か所にコピーしてしまいましょう。

pom.xmlの<plugins>要素に以下の記述を付けくわえます。

            <plugin>
                <artifactId>maven-dependency-plugin</artifactId>
                <executions>
                    <execution>
                        <phase>install</phase>
                        <goals>
                            <goal>copy-dependencies</goal>
                        </goals>
                        <configuration>
                            <outputDirectory>${project.build.directory}/lib</outputDirectory>
                            <includeScope>runtime</includeScope>
                        </configuration>
                    </execution>
                </executions>
            </plugin>

これでtargetディレクトリにlibディレクトリを作成し、使用しているJARファイルをコピーします。

このlibディレクトリ配下のJARファイルをクラスパスに追加して、jdepsを実行してみます。

C:\jersey-sample\simple-service\target>mvn clean install
[INFO] Scanning for projects...
[INFO]
[INFO] ---------------------< com.example:simple-service >---------------------
[INFO] Building simple-service 1.0-SNAPSHOT
[INFO] --------------------------------[ jar ]--------------------------------- 
   <<省略>>

C:\jersey-sample\simple-service\target>cd target
C:\jersey-sample\simple-service\target>jdeps -s -cp lib\* simple-service-1.0-SNAPSHOT.jar
エラー: jaxb-api-2.3.0.jarはマルチリリースjarファイルですが--multi-releaseオプションが設定されていません

C:\jersey-sample\simple-service\target>

ところが、今度はエラーが出てしまいました。

ここで、表示されているマルチリリースJARというのは、Java SE 9で導入された機能で、1つのJARファイルに複数のJavaのバージョンに対応したクラスファイルをまとめることができます。

jdepsはマルチリリースJARファイルがあると、--muliti-releaseオプションを設定する必要があるのですが、これがまた問題なのです。実際にやってみましょう。

C:\jersey-sample\simple-service\target>jdeps -s --multi-release 11 -cp lib\* simple-service-1.0-SNAPSHOT.jar
エラー: simple-service-1.0-SNAPSHOT.jarはマルチリリースjarファイルではありませんが--multi-releaseオプションが設定されています

C:\jersey-sample\simple-service\target>

ここから分かるのは--multi-releaseオプションを設定するには、すべてのJARファイルがマルチリリースJARになっていないといけないということです。しかし、それはほぼ無理なのです。

マルチリリースJARに対するjdepsの対応は間違っていると思うのですが、どうにかならないですかねぇ。

しかたないので、jaxb-api-2.3.0.jarをlibディレクトリから移動させて、再びjdepsを実行しました。

C:\jersey-sample\simple-service\target>move lib\jaxb-api-2.3.0.jar .
        1 個のファイルを移動しました。

C:\jersey-sample\simple-service\target>jdeps -s -cp lib\* simple-service-1.0-SNAPSHOT.jar
simple-service-1.0-SNAPSHOT.jar -> lib\grizzly-http-server-2.4.0.jar
simple-service-1.0-SNAPSHOT.jar -> java.base
simple-service-1.0-SNAPSHOT.jar -> lib\javax.ws.rs-api-2.1.jar
simple-service-1.0-SNAPSHOT.jar -> lib\jersey-container-grizzly2-http-2.27.jar
simple-service-1.0-SNAPSHOT.jar -> lib\jersey-server-2.27.jar

C:\jersey-sample\simple-service\target>

やっと結果が表示されました。

java.baseモジュール以外に、4つのJARファイルを使用していることが分かります。実をいうと、これらの4つのJARファイルはモジュールにはなっていません。

しかし、モジュールアプリケーションが直接アクセスできるのは、モジュールだけです。こういう場合、通常のJARファイルをモジュールとして扱う自動モジュール( Automatic Module)という機能を使用します。

実際にそれを行うのは、もうちょっと後なので、ここではまずモジュールにすることを考えましょう。

module-info.javaの生成

java.baseモジュールはJavaアプリケーションには必ず必要なので、module-info.javaに記述しなくても大丈夫です。

なので、それ以外の4つのJARファイルをmodule-info.javaに記述します。でも、これはめんどうなので、jdepsコマンドにmodule-info.javaを作ってもらいましょう。

そのために使用するのが、--generate-module-infoオプションです。

ただし、--generate-module-infoオプションではクラスパスは使用できません。その理由は、先ほどと同じでモジュールはモジュールしかアクセスできないためです。なので、クラスパスではなく、モジュールパスを設定します。ここではlibディレクトリをモジュールパスとして設定します。自動モジュールの機能があるので、モジュールでないJARファイルであっても、これで大丈夫です。

でも、1つ問題があります。libディレクトリには2つのバージョンのjavax.injectが含まれているはずです。Dependency Pluginはpom.xmlの依存性を見ているだけなので、バージョン違いのJARファイルが含まれることがあるのはしかたありません。

しかし、現状のモジュールシステムはモジュールのバージョンを直接あつかうことができません(バージョンをつけることはできます)。そのため、モジュールパスに複数のバージョンのJARファイルを置くことはできないのです。

しかたないので、libディレクトリのjavax.injectの古い方を削除してから、jdepsを実行します。

下の例では複数のバージョンがあるため、jdepsがエラーを出しています。javax.injectを削除すれば、実行できます。

C:\jersey-sample\simple-service\target>jdeps --generate-module-info . --module-path lib simple-service-1.0-SNAPSHOT.jar
Exception in thread "main" java.lang.module.FindException: Two versions of module javax.inject found in lib (javax.inject-2.5.0-b42.jar and javax.inject-1.jar)
        at java.base/jdk.internal.module.ModulePath.scanDirectory(ModulePath.java:293)
        at java.base/jdk.internal.module.ModulePath.scan(ModulePath.java:231)
        at java.base/jdk.internal.module.ModulePath.scanNextEntry(ModulePath.java:189)
        at java.base/jdk.internal.module.ModulePath.findAll(ModulePath.java:165)
        at jdk.jdeps/com.sun.tools.jdeps.JdepsConfiguration$Builder.build(JdepsConfiguration.java:544)
        at jdk.jdeps/com.sun.tools.jdeps.JdepsTask.buildConfig(JdepsTask.java:589)
        at jdk.jdeps/com.sun.tools.jdeps.JdepsTask.run(JdepsTask.java:543)
        at jdk.jdeps/com.sun.tools.jdeps.JdepsTask.run(JdepsTask.java:519)
        at jdk.jdeps/com.sun.tools.jdeps.Main.main(Main.java:49)

C:\jersey-sample\simple-service\target>del lib\javax.inject-1.jar

C:\jersey-sample\simple-service\target>jdeps --generate-module-info . --module-path lib simple-service-1.0-SNAPSHOT.jar
writing to .\simple.service\module-info.java

C:\jersey-sample\simple-service\target>

なお、--generate-module-infoの引数はmodule-info.javaを出力するディレクトリです。ここではカレントディレクトリを指定しています。

jdeps指定したディレクトリにJARファイルに対応したディレクトリを作成して、そこにmodule-info.javaを生成します。ここではsimple-service-1.0-SNAPSHOT.javaファイルなので、バージョンを取り除いて、ハイフンをピリオドに置き換えたsimple.serviceをディレクトリ名にしています。

では、作成されたmodule-info.javaを見てみましょう。

module simple.service {
    requires jersey.container.grizzly2.http;
    requires jersey.server;

    requires transitive grizzly.http.server;
    requires transitive java.ws.rs;

    exports com.example;

}

はじめの行のmoduleキーワードの後に記述してあるのがモジュール名です。

生成したモジュール名は、jdepsが作成したディレクトリ名と同じです。つまり、JARファイル名から作成したことが分かります。これは自動モジュールのモジュール名を決めるときと同じルールで行っています。

通常、モジュール名はパッケージ名と同様に、ドメインの逆順を使用して決めます。ここではサンプルなので、このままにしておきましょう。

requires文にモジュール名を指定します。

先ほどの4つのJARファイルに対応するのが、この4行のrequires文です。

ここでも、JARファイル名からバージョン名を取り除いて、ハイフンをピリオドに置き換えたものがモジュール名として使用されています。ところが、java.ws.rsだけはJARファイル名とは異なります。

これは、JARファイルのMANIFEST.MFファイルの中にモジュール名を指定しているためです。

javax.ws.rs-api-2.1.jarのMANIFEST.MFファイルは以下のようになっています。

Manifest-Version: 1.0
Automatic-Module-Name: java.ws.rs
Bnd-LastModified: 1501859871759
Build-Id: 08/04/2017 05:17 PM
  <<以下、省略>>

この中の、Automatic-Module-Nameで指定しているjava.ws.rsがモジュール名になります。

こんなのを1つ1つ調べていくのは大変なので、jdepsコマンドで作ってもらうのが手っ取り早いですね。

さて、module-info.javaのrequires文には、transitiveがついているもののと、ついていないものがあります。

transitiveはこのモジュールが使用しているモジュールであり、かつこのモジュールがそれを外部に対して公開しているモジュールです。

たとえば、grizzly.http.serverがtransitiveになっているのはMainクラスのstartServerメソッドの戻り値の方がorg.glassfish.grizzly.http.server.HttpServerクラスだからです。

jdepsコマンドはこのようにクラスの内部までチェックしてtransitiveかどうかを決めています。

ところが、startServerメソッドはmainメソッドで使われるだけなので、実際にはtransitiveである必要はありません。

まぁ、jdepsもそこまでは調べてくれないということですね。

module-info.javaの最後のexports文はsimple.serviceモジュールで公開するパッケージを指定します。このサンプルは1つのパッケージしか使用していないので、それが表示されているだけです。

しかし、複数のパッケージがあれば、それがずらずらとexports文で記述されてしまうので、ほんとうに公開したいパッケージだけを残して、あとは削除しておきましょう。

さて、生成されたmodule-info.javaは、src/main/javaディレクトリにコピーしておきましょう。つまり、module-info.javaはデフォルトパッケージの位置に配置します。

これで、simple-serviceサンプルはモジュールアプリケーションになります。

では、コンパイルしてみましょう。

C:\jersey-sample\simple-service>mvn clean compile
[INFO] Scanning for projects...
[INFO]
[INFO] ---------------------< com.example:simple-service >---------------------
[INFO] Building simple-service 1.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-clean-plugin:2.5:clean (default-clean) @ simple-service ---
[INFO] Deleting C:\Users\yuichi\Desktop\jersey\simple-service\target
[INFO]
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ simple-service ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory C:\Users\yuichi\Desktop\jersey\simple-service\src\main\resources
[INFO]
[INFO] --- maven-compiler-plugin:3.7.0:compile (default-compile) @ simple-service ---
[WARNING] ********************************************************************************************************************
[WARNING] * Required filename-based automodules detected. Please don't publish this project to a public artifact repository! *
[WARNING] ********************************************************************************************************************
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 3 source files to C:\Users\yuichi\Desktop\jersey\simple-service\target\classes
[INFO] /C:/Users/yuichi/Desktop/jersey/simple-service/src/main/java/com/example/Main.java: C:\Users\yuichi\Desktop\jersey\simple-service\src\main\java\com\example\Main.javaは推奨されないAPIを使用またはオーバーライドしています。
[INFO] /C:/Users/yuichi/Desktop/jersey/simple-service/src/main/java/com/example/Main.java: 詳細は、-Xlint:deprecationオプションを指定して再コンパイルしてください。
[INFO] -------------------------------------------------------------
[ERROR] COMPILATION ERROR :
[INFO] -------------------------------------------------------------
[ERROR] /C:/Users/yuichi/Desktop/jersey/simple-service/src/main/java/com/example/Main.java:[25,55] org.glassfish.jersey.ExtendedConfigにアクセスできません
  org.glassfish.jersey.ExtendedConfigのクラス・ファイルが見つかりません
[INFO] 1 error
[INFO] -------------------------------------------------------------
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 3.897 s
[INFO] Finished at: 2018-07-21T21:45:47+09:00
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.7.0:compile (default-compile) on project simple-service: Compilation failure
[ERROR] /C:/Users/yuichi/Desktop/jersey/simple-service/src/main/java/com/example/Main.java:[25,55] org.glassfish.jersey.ExtendedConfigにアクセスできません
[ERROR]   org.glassfish.jersey.ExtendedConfigのクラス・ファイルが見つかりません
[ERROR]
[ERROR] -> [Help 1]
[ERROR]
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR]
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoFailureException

C:\jersey-sample\simple-service>

クラスが見つからないと、エラーになってしまいました。

ここで見つからないといわれているorg.glassfish.jersey.ExtendedConfigインタフェースは、Mainクラスで使用してるorg.glassfish.jersey.server.ResourceConfigクラスが実装しているインタフェースです。

そして、ExtendedConfigインタフェースはjersey-server-2.27.jarファイルではなく、jersey-common-2.27.jarに含まれているのです。

このことから分かるように、jdepsの--generate-module-infoオプションで作成されるmodule-info.javaは完璧なものではありません。

あくまでも、ひな型としてあつかい、必要に応じて編集しなくてはなりません。

特にリフレクションに関しては、jdepsは無力です。リフレクションを使用している場合は、自分で依存性を記述してください。

ここでは、jersey.commonのrequires文を追加します。

module simple.service {
    requires jersey.container.grizzly2.http;
    requires jersey.server;
    requires jersey.common;

    requires transitive grizzly.http.server;
    requires transitive java.ws.rs;

    exports com.example;

}

これでコンパイルはできるはずです。

ただし、テストは通りません。これはCompiler Pluginのバグで3.7.1で直るということなのですが、テストできないのは困ります。

これについては、ワークアラウンドがあって、pom.xmlのコンパイルのオプションで<source>と<target>を共に9にすれば、テストが通ります。

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.7.0</version>
                <inherited>true</inherited>
                <configuration>
                    <source>9</source>
                    <target>9</target>
                </configuration>
            </plugin>

とはいうものの、せっかくJava 11を使っているのに、9にするのは何か負けているような気がして、個人的にはイヤw

実行

コンパイルできたので、実行してみましょう。

C:\jersey-sample\simple-service>mvn exec:java
[INFO] Scanning for projects...
[INFO]
[INFO] ---------------------< com.example:simple-service >---------------------
[INFO] Building simple-service 1.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- exec-maven-plugin:1.6.0:java (default-cli) @ simple-service ---
7月 22, 2018 11:46:46 午前 org.glassfish.grizzly.http.server.NetworkListener start
情報: Started listener bound to [localhost:8080]
7月 22, 2018 11:46:46 午前 org.glassfish.grizzly.http.server.HttpServer start
情報: [HttpServer] Started.
Jersey app started with WADL available at http://localhost:8080/myapp/application.wadl
Hit enter to stop it...

あっさりと実行できてしまいました。http://localhost:8080/myapp/myresource にアクセスすれば、"Got it!" と表示されるはずです。

ところで、このサンプルで実行のために使用しているExec Maven Pluginでは、ゴールとしてexec:javaとexec:execが定義されています。

exec:javaがMavenと同じVMで実行され、exec:execは異なるVMで実行されるという違いがあります。

どうやら、現状のExec Maven Pluginのexec:javaの場合、モジュールであってもモジュールのロードではなく、単なるクラスロードが使われているようです。このため、特に設定をしなくても実行ができてしまったのです。

しかし、これではモジュールとしての動作の確認ができないので、exec:execで実行できるようにしてみます。

そのためには、pom.xmlの編集が必要です。

現在のpom.xmlのExec Maven Pluginの設定は次のようになっています。

            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>exec-maven-plugin</artifactId>
                <version>1.6.0</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>java</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <mainClass>com.example.Main</mainClass>
                </configuration>
            </plugin>

これを次のように変更します。

            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>exec-maven-plugin</artifactId>
                <version>1.6.0</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>exec</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <executable>java</executable>
                    <arguments>
                        <argument>-p</argument> <modulepath />
                        <argument>-cp</argument> <classpath />
                        <argument>-m</argument>
                            <argument>simple.service/com.example.Main</argument>
                    </arguments>
                </configuration>
            </plugin>

変更したのは<configuration>要素です。

残念ながら、現バージョンのExec Maven Pluginではモジュールを使用するには、javaコマンドの引数を<argument>要素で指定する必要があるのです。

-pオプションは、--module-pathオプションの省略形で、モジュールパスを指定するオプションです。

ただし、クラスパスと同様に、モジュールパスでも<modulepath />要素を使用すれば、実際にモジュールパスを指定する必要はありません。これだけでも、ちょっと楽ができる感じですね。

メインクラスを指定するのは-mオプションです。-mオプションは--moduleオプションの省略形で、モジュールをロードする基点となるモジュールとメインクラスを指定します。

先ほどは、exec:javaで実行しましたが、今回はexec:execです。

C:\jersey-sample\simple-service>mvn exec:exec
[INFO] Scanning for projects...
[INFO]
[INFO] ---------------------< com.example:simple-service >---------------------
[INFO] Building simple-service 1.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- exec-maven-plugin:1.6.0:java (default-cli) @ simple-service ---
7月 22, 2018 13:23:06 午前 org.glassfish.grizzly.http.server.NetworkListener start
情報: Started listener bound to [localhost:8080]
7月 22, 2018 13:23:06 午前 org.glassfish.grizzly.http.server.HttpServer start
情報: [HttpServer] Started.
Jersey app started with WADL available at http://localhost:8080/myapp/application.wadl
Hit enter to stop it...

同じように実行することができたはずです。

単体で実行

ここまではMavenを使ってコンパイル、実行を行ってきました。

しかし、アプリケーションを配布することを考えると、ずっとMavenに頼っているわけにはいきません。

というわけで、単体で実行できるようにしてみましょう。

まずは、Mavenでinstallまで行って、サンプルのJARファイルを作成しておきます。

C:\jersey-sample\simple-service>mvn clean install -Dmaven.test.skip=true
[INFO] Scanning for projects...
[INFO]
[INFO] ---------------------< com.example:simple-service >---------------------
[INFO] Building simple-service 1.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
  <<以下、省略>>

前述したように、sourceとtargetが11のままだとテストが通りません。そこで、ここではテストをスキップさせています。

これで、targetディレクトリにsimple-service-1.0-SNAPSHOT.jarファイルができているはずです。

また、Maven Dependency Pluginによってlibディレクトリには依存性のあるJARファイルがコピーされています。

ここまでモジュールと自動モジュール、モジュールではないJARファイルの3種類が出てきていましたが、それを一度整理しましょう。

  • モジュール: module-info.javaでモジュール名、依存性、公開範囲が定義されたモジュール
  • 自動モジュール (Automatic Module): モジュールの定義はないが、自動的にモジュールに格上げされたJARファイル
  • 無名モジュール (Unnamed Module): モジュールではないJARファイル

自動モジュールは自動的に格上げされたため、すべてのパッケージが外部に公開されています。自動モジュールのモジュール名は前述したようにMANIFEST.MFのAutomatic-Module-Nameで定義された名前が使用されます。Automatic-Module-Nameがない場合はJARファイル名から自動的に作られます。

モジュールと自動モジュールはモジュールパスで指定したディレクトリに配置します。

無名モジュールは、モジュールという名前がついていますが、単なるJARファイルです。もちろん、すべてのパッケージが公開されています。無名モジュールは、今まで同様にクラスパスで指定します。

ここで、重要なのは通常のモジュールはモジュールもしくは自動モジュールにしかアクセスできません。アクセスできるのは、module-info.javaのrequires文に記述したモジュール(自動モジュールを含む)だけです。

つまり、モジュールは無名モジュールにはアクセスできないのです。無名モジュールにアクセスできるのは、自動モジュールだけです。

ただし、例外もあります。これについては、別エントリーで触れたいと思います。

さて、ここではルートとなるモジュールがsimple-service-1.0-SNAPSHOT.jarファイルで、このモジュールは前述したように5つの自動モジュールに依存しています。

つまり、この6個のJARファイルをモジュールパスで指定するディレクトリに配置します。そして、それ以外のJARファイルをクラスパスで指定します。

ここでは、modsディレクトリを作成して、そこに6個のJARファイルを移動させてみます。

C:\jersey-sample\simple-service\target>dir mods /B
grizzly-http-server-2.4.0.jar
javax.ws.rs-api-2.1.jar
jersey-common-2.27.jar
jersey-container-grizzly2-http-2.27.jar
jersey-server-2.27.jar
simple-service-1.0-SNAPSHOT.jar

C:\jersey-sample\simple-service\target>

では、実行してみましょう。

C:\jersey-sample\simple-service\target>java -p mods -cp lib\* -m simple.service/com.example.Main
7月 22, 2018 8:57:34 午後 org.glassfish.grizzly.http.server.NetworkListener start
情報: Started listener bound to [localhost:8080]
7月 22, 2018 8:57:34 午後 org.glassfish.grizzly.http.server.HttpServer start
情報: [HttpServer] Started.
Jersey app started with WADL available at http://localhost:8080/myapp/application.wadl
Hit enter to stop it...

今までと同じように実行することができました。

まとめ

さて、サンプルアプリケーションをモジュール化して、単体で実行できるところまでやってみました。正直な話、かなりめんどくさいです。

新規に作成するアプリケーションであればさほどではないかもしれませんが、既存のアプリケーションをモジュール化するのはかなりタフなことになるのではないでしょうか。

ここまでの手段をまとめてみました。

  • 依存性を調べるにはjdepsコマンドを使用する
  • マルチリリースJARには気をつける
  • module-info.javaのひな型はjdepsコマンドで生成させる
  • 必要に応じて、生成したmodule-info.javaを編集する
  • Mavenで実行する場合、ゴールがexec:javaであればメインクラスだけの指定でOK
  • ゴールがexec:execの場合、モジュールパス、クラスパス、ルートモジュールとメインクラスを指定する
  • Mavenを使用せずに実行する場合、モジュールと自動モジュールをモジュールパス、その他をクラスパスで指定する

やっぱり、たいへんですね。

0 件のコメント: