CLOVER🍀

That was when it all began.

BusyBoxを使って、単一で実行可能なコマンドのバイナリをDockerコンテナ内に放り込む

これは、なにをしたくて書いたもの?

Dockerコンテナ内で解析などの作業をする必要が出た時に、コマンドなどが入っていないDockerイメージが相手の場合は
どうすればいい?という時の選択肢のひとつとして、BusyBoxが使えそうな気がしたので。

BusyBox

Case

例えば、以下のようにDebianのDockerイメージを使った時に、デフォルトではpsやmpstatなど、調べものをする際に
使いたいコマンドが入っていません。

$ docker container run -it --rm --name debian debian:stretch
root@f28f50fe47ca:/# ps
bash: ps: command not found
root@f28f50fe47ca:/# wget
bash: wget: command not found
root@f28f50fe47ca:/# mpstat
bash: mpstat: command not found

もちろん、「docker container exec」でコンテナ内に入り、aptなどでインストールするのもありです。

$ docker container exec -it debian bash
# apt update -y
# apt install sysstat -y

実行可能なコマンドを見つけてきて、「docker container cp」でコンテナ内に放り込むのも良いでしょう。

しかし、Distrolessのようなシェルすら入っていないDockerイメージの場合は、それも困難です。

GitHub - GoogleContainerTools/distroless: 🥑 Language focused docker images, minus the operating system.

こういうケースでは、BusyBoxを覚えておくと役に立ちそうだな、と。

BusyBox

BusyBoxとは、Aboutページによると「多くの一般的なUnixユーティリティを小さな実行可能ファイルにまとめたもの」だそうです。

BusyBox

About BusyBox

BusyBoxを使うと、単独で実行可能なコマンド、もしくはコマンドをまとめたバイナリを単独で使うことができます。

Aboutページを見ると、「組み込みLinuxのスイスアーミーナイフ」だとか。

BusyBox: The Swiss Army Knife of Embedded Linux

ちなみに、Dockerイメージとして提供されているのと

busybox / DockerHub

Alpine Linuxでも利用されているようです。

index | Alpine Linux

about | Alpine Linux

BusyBoxを使ってみる

それでは、試してみましょう。

今回利用するBusyBoxは、1.30.0とします。

ダウンロードは、こちらから対象のバージョン、必要なバイナリをダウンロードします。

Index of /downloads/binaries

まずは、対象としてDebianのコンテナを起動しておきます。

$ docker container run -it --rm --name debian debian:stretch

ここに、BusyBoxからPSコマンドをダウンロードして、実行権限を付与します。

$ wget https://busybox.net/downloads/binaries/1.30.0-i686/busybox_PS
$ chmod a+x busybox_PS

あとはこちらを、コンテナ内の「/usr/local/bin」にでも放り込んであげましょう。このあたりに置けば、PATHが通っているので
便利かなと。

$ docker container cp busybox_PS debian:/usr/local/bin

すると、コンテナ内でこのコマンドが利用できるようになります。

root@eeab426fb585:/# busybox_PS 
PID   USER     TIME  COMMAND
    1 root      0:00 bash
    6 root      0:00 busybox_PS

wgetの場合。

$ wget https://busybox.net/downloads/binaries/1.30.0-i686/busybox_WGET
$ chmod a+x busybox_WGET
$ docker container cp busybox_WGET debian:/usr/local/bin

wgetを使って、コンテナ内でBusyBoxの他のコマンドをダウンロード。

# busybox_WGET -q https://busybox.net/downloads/binaries/1.30.0-i686/busybox_CHMOD
Connecting to busybox.net (140.211.167.122:443)
wget: note: TLS certificate validation not implemented
busybox_CHMOD        100% |***********************************************************************************************************************************| 36780  0:00:00 ETA

対象のDockerイメージ内にシェルすらない場合は、ashを使えば良いでしょう。

$ wget https://busybox.net/downloads/binaries/1.30.0-i686/busybox_ASH
$ chmod a+x busybox_ASH
$ docker container cp busybox_ASH debian:/usr/local/bin

これで、シェルがないコンテナでもashで入れるようになります。

$ docker container exec -it debian busybox_ASH
/ # 

良さそうですが、通常のコマンドと比べるとオプションが少なかったりするので、それなりに制限はありそうです。

# busybox_WGET -h
Usage: wget [-c|--continue] [--spider] [-q|--quiet] [-O|--output-document FILE]
    [--header 'header: value'] [-Y|--proxy on/off] [-P DIR]
    [-S|--server-response] [-U|--user-agent AGENT] [-T SEC] URL...

Retrieve files via HTTP or FTP

    --spider    Only check URL existence: $? is 0 if exists
    -c      Continue retrieval of aborted transfer
    -q      Quiet
    -P DIR      Save to DIR (default .)
    -S          Show server response
    -T SEC      Network read timeout is SEC seconds
    -O FILE     Save to FILE ('-' for stdout)
    -U STR      Use STR for User-Agent header
    -Y on/off   Use proxy

とはいえ、即席としてはやはり便利ですね。

全部入りのバイナリを使う

ここまでは、単一のコマンドを使用してきましたが、「busybox」というバイナリを使えば、BusyBoxで使えるコマンドが
全部入りのものとして利用することができます。

$ wget https://busybox.net/downloads/binaries/1.30.0-i686/busybox
$ chmod a+x busybox
$ docker container cp busybox debian:/usr/local/bin

コマンドを実行すると、利用できるコマンドの一覧が表示されます。

# busybox
BusyBox v1.30.0 (2018-12-30 22:25:27 CET) multi-call binary.
BusyBox is copyrighted by many authors between 1998-2015.
Licensed under GPLv2. See source distribution for detailed
copyright notices.

Usage: busybox [function [arguments]...]
   or: busybox --list[-full]
   or: busybox --show SCRIPT
   or: busybox --install [-s] [DIR]
   or: function [arguments]...

    BusyBox is a multi-call binary that combines many common Unix
    utilities into a single executable.  Most people will create a
    link to busybox for each function they wish to use and BusyBox
    will act like whatever it was invoked as.

Currently defined functions:
    [, [[, acpid, add-shell, addgroup, adduser, adjtimex, ar, arch, arp, arping, ash, awk, base64, basename, bc, blkdiscard, blkid, blockdev, bootchartd, brctl, bunzip2,
    bzcat, bzip2, cal, cat, chat, chattr, chgrp, chmod, chown, chpasswd, chpst, chroot, chrt, chvt, cksum, clear, cmp, comm, conspy, cp, cpio, crond, crontab, cryptpw,
    cttyhack, cut, date, dc, dd, deallocvt, delgroup, deluser, depmod, devmem, df, dhcprelay, diff, dirname, dmesg, dnsd, dnsdomainname, dos2unix, dpkg, dpkg-deb, du,
    dumpkmap, dumpleases, echo, ed, egrep, eject, env, envdir, envuidgid, expand, expr, factor, fakeidentd, false, fatattr, fbset, fbsplash, fdflush, fdformat, fdisk,
    fgconsole, fgrep, find, findfs, flash_eraseall, flash_lock, flash_unlock, flashcp, flock, fold, free, freeramdisk, fsck, fsck.minix, fsfreeze, fstrim, fsync, ftpd,
    ftpget, ftpput, fuser, getopt, getty, grep, groups, gunzip, gzip, halt, hd, hdparm, head, hexdump, hexedit, hostid, hostname, httpd, hush, hwclock, i2cdetect, i2cdump,
    i2cget, i2cset, id, ifconfig, ifenslave, ifplugd, inetd, init, inotifyd, insmod, install, ionice, iostat, ip, ipaddr, ipcalc, ipcrm, ipcs, iplink, ipneigh, iproute,
    iprule, iptunnel, kbd_mode, kill, killall, killall5, klogd, last, less, link, linux32, linux64, linuxrc, ln, loadfont, loadkmap, logger, login, logname, losetup, lpd,
    lpq, lpr, ls, lsattr, lsmod, lsof, lspci, lsscsi, lsusb, lzcat, lzma, lzop, lzopcat, makedevs, makemime, man, md5sum, mdev, mesg, microcom, mkdir, mkdosfs, mke2fs,
    mkfifo, mkfs.ext2, mkfs.minix, mkfs.vfat, mknod, mkpasswd, mkswap, mktemp, modinfo, modprobe, more, mount, mountpoint, mpstat, mt, mv, nameif, nbd-client, nc, netstat,
    nice, nl, nmeter, nohup, nologin, nproc, ntpd, nuke, od, openvt, partprobe, passwd, paste, patch, pgrep, pidof, ping, ping6, pipe_progress, pivot_root, pkill, pmap,
    popmaildir, poweroff, powertop, printenv, printf, ps, pscan, pstree, pwd, pwdx, raidautorun, rdate, rdev, readlink, readprofile, realpath, reboot, reformime,
    remove-shell, renice, reset, resize, resume, rev, rm, rmdir, rmmod, route, rpm, rpm2cpio, rtcwake, run-init, run-parts, runlevel, runsv, runsvdir, rx, script,
    scriptreplay, sed, sendmail, seq, setarch, setconsole, setfattr, setfont, setkeycodes, setlogcons, setpriv, setserial, setsid, setuidgid, sh, sha1sum, sha256sum, sha3sum,
    sha512sum, showkey, shred, shuf, slattach, sleep, smemcap, softlimit, sort, split, ssl_client, start-stop-daemon, stat, strings, stty, su, sulogin, sum, sv, svc, svlogd,
    svok, swapoff, swapon, switch_root, sync, sysctl, syslogd, tac, tail, tar, taskset, tc, tcpsvd, tee, telnet, telnetd, test, tftp, tftpd, time, timeout, top, touch, tr,
    traceroute, traceroute6, true, truncate, tty, ttysize, tunctl, tune2fs, ubiattach, ubidetach, ubimkvol, ubirename, ubirmvol, ubirsvol, ubiupdatevol, udhcpc, udhcpd,
    udpsvd, uevent, umount, uname, uncompress, unexpand, uniq, unix2dos, unlink, unlzma, unlzop, unxz, unzip, uptime, users, usleep, uudecode, uuencode, vconfig, vi, vlock,
    volname, w, wall, watch, watchdog, wc, wget, which, who, whoami, whois, xargs, xxd, xz, xzcat, yes, zcat, zcip

この場合、利用したいコマンドは「busybox」コマンドのサブコマンドとして実行します。

# busybox ps
PID   USER     TIME  COMMAND
    1 root      0:00 bash
   58 root      0:00 busybox ps



# busybox wget   
BusyBox v1.30.0 (2018-12-30 22:25:27 CET) multi-call binary.

Usage: wget [-c|--continue] [--spider] [-q|--quiet] [-O|--output-document FILE]
    [--header 'header: value'] [-Y|--proxy on/off] [-P DIR]
    [-S|--server-response] [-U|--user-agent AGENT] [-T SEC] URL...

Retrieve files via HTTP or FTP

    --spider    Only check URL existence: $? is 0 if exists
    -c      Continue retrieval of aborted transfer
    -q      Quiet
    -P DIR      Save to DIR (default .)
    -S          Show server response
    -T SEC      Network read timeout is SEC seconds
    -O FILE     Save to FILE ('-' for stdout)
    -U STR      Use STR for User-Agent header
    -Y on/off   Use proxy


# busybox mpstat
Linux 4.15.0-48-generic (eeab426fb585)  04/27/19    _x86_64_    (8 CPU)

14:11:18     CPU    %usr   %nice    %sys %iowait    %irq   %soft  %steal  %guest   %idle
14:11:18     all   10.29    0.02    1.48    2.32    0.00    0.05    0.00    0.00   85.85

ashでコンテナに入る場合。

$ docker container exec -it debian busybox ash
/ # 

Dockerコンテナ内の調査時で困った時に知っておくと、便利そうですね。

覚えておきましょう。

RESTEasy+Undertowで作ったアプリケーションを、GraalVMでネイティブイメージにしてみる

これは、なにをしたくて書いたもの?

以前に、簡単なJavaアプリケーションをGraalVM(Substrate VM)を使ってネイティブイメージにしてみたのですが、
もう少しライブラリなどを使った複雑な(?)ものをネイティブイメージにしてみようと思いまして。

で、どのくらい大変か?というのを知ってみましょう、というお題で。

環境

今回の環境は、こちら。

$ java -version
openjdk version "1.8.0_202"
OpenJDK Runtime Environment (build 1.8.0_202-20190206132807.buildslave.jdk8u-src-tar--b08)
OpenJDK GraalVM CE 1.0.0-rc16 (build 25.202-b08-jvmci-0.59, mixed mode)


$ mvn -version
Apache Maven 3.6.1 (d66c9c0b3152b2e69ee9bac180bb8fcc8e6af555; 2019-04-05T04:00:29+09:00)
Maven home: $HOME/maven/current
Java version: 1.8.0_202, vendor: Oracle Corporation, runtime: /usr/local/graalvm-ce/jre
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "4.15.0-48-generic", arch: "amd64", family: "unix"

お題

RESTEasyとUndertowを使った、超簡単なアプリケーションをGraalVMを使ってネイティブイメージにしてみます。

使用する依存関係は、こちら。

        <dependency>
            <groupId>org.jboss.resteasy</groupId>
            <artifactId>resteasy-undertow</artifactId>
            <version>3.6.3.SP1</version>
        </dependency>

JAX-RSリソースクラス。
src/main/java/org/littlewings/graal/nativeimage/HelloResource.java

package org.littlewings.graal.nativeimage;

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.TEXT_PLAIN)
    public String message() {
        return "Hello RESTEasy!!";
    }
}

Applicationクラス。
src/main/java/org/littlewings/graal/nativeimage/JaxrsActivator.java

package org.littlewings.graal.nativeimage;

import java.util.HashSet;
import java.util.Set;
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;

@ApplicationPath("")
public class JaxrsActivator extends Application {
    private Set<Class<?>> resourceClasses;

    public JaxrsActivator() {
        resourceClasses = new HashSet<>();
        resourceClasses.add(HelloResource.class);
    }

    @Override
    public Set<Class<?>> getClasses() {
        return resourceClasses;
    }
}

起動クラス。
src/main/java/org/littlewings/graal/nativeimage/App.java

package org.littlewings.graal.nativeimage;

import io.undertow.Undertow;
import org.jboss.resteasy.plugins.server.undertow.UndertowJaxrsServer;
import org.jboss.resteasy.spi.ResteasyDeployment;

public class App {
    public static void main(String... args) {
        UndertowJaxrsServer server = new UndertowJaxrsServer();

        ResteasyDeployment deployment = new ResteasyDeployment();
        deployment.setApplication(new JaxrsActivator());

        server.deploy(deployment);

        server.start(Undertow.builder().addHttpListener(8080, "0.0.0.0"));
    }
}

確認。

$ mvn compile exec:java -Dexec.mainClass=org.littlewings.graal.nativeimage.App


$ curl localhost:8080/hello
Hello RESTEasy!!

OKです。これを、ネイティブイメージにしていきます。

Native Image Maven Pluginを追加して、とりあえず動かしてみる

pom.xmlに、以下のようにNative Image Maven Pluginを追加して、まずは動かしてみましょう。なんとなく、Profileは分けました。

    <profiles>
        <profile>
            <id>native</id>
            <build>
                <plugins>
                    <plugin>
                        <groupId>com.oracle.substratevm</groupId>
                        <artifactId>native-image-maven-plugin</artifactId>
                        <version>1.0.0-rc16</version>
                        <executions>
                            <execution>
                                <goals>
                                    <goal>native-image</goal>
                                </goals>
                                <phase>package</phase>
                            </execution>
                        </executions>
                        <configuration>
                            <mainClass>org.littlewings.graal.nativeimage.App</mainClass>
                            <imageName>resteasy-undertow</imageName>
                        </configuration>
                    </plugin>
                </plugins>
            </build>
        </profile>
    </profiles>

ビルド。

$ mvn package -Pnative

警告が出ます。

Warning: Abort stand-alone image build. com.oracle.graal.pointsto.constraints.UnresolvedElementException: Discovered unresolved type during parsing: org.osgi.framework.FrameworkUtil. To diagnose the issue you can use the --allow-incomplete-classpath option. The missing type is then reported at run time when it is accessed the first time.
Detailed message:
Trace: 
    at parsing org.xnio.Xnio$OsgiSupport.doGetOsgiService(Xnio.java:276)
Call path from entry point to org.xnio.Xnio$OsgiSupport.doGetOsgiService(): 
    at org.xnio.Xnio$OsgiSupport.doGetOsgiService(Xnio.java:276)
    at org.xnio.Xnio.doGetInstance(Xnio.java:261)
    at org.xnio.Xnio.getInstance(Xnio.java:187)
    at io.undertow.Undertow.start(Undertow.java:118)
    at org.jboss.resteasy.plugins.server.undertow.UndertowJaxrsServer.start(UndertowJaxrsServer.java:281)
    at org.littlewings.graal.resteay.App.main(App.java:20)
    at com.oracle.svm.core.JavaMainWrapper.run(JavaMainWrapper.java:153)
    at com.oracle.svm.core.code.IsolateEnterStub.JavaMainWrapper_run_5087f5482cc9a6abc971913ece43acb471d2631b(generated:0)

Warning: Use -H:+ReportExceptionStackTraces to print stacktrace of underlying exception

スタンドアロンなイメージのビルドが失敗したと言っています。

Warning: Abort stand-alone image build.

最後には、実行時にJDKが必要なイメージにフォールバックしたよ、と言われます。

Warning: Image 'resteasy-undertow' is a fallback image that requires a JDK for execution (use --no-fallback to suppress fallback image generation).

ファイル自体はできています。

実際、Javaがインストールされていない環境で作成された動作させると、起動に失敗します。

$ target/resteasy-undertow 
Error: No bin/java and no environment variable JAVA_HOME

Javaがインストールされている環境であれば、起動させることができます。

$ target/resteasy-undertow 
4 27, 2019 9:05:38 午後 org.jboss.resteasy.spi.ResteasyDeployment processApplication
INFO: RESTEASY002225: Deploying javax.ws.rs.core.Application: class org.littlewings.graal.nativeimage.JaxrsActivator
4 27, 2019 9:05:38 午後 org.jboss.resteasy.spi.ResteasyDeployment processApplication
INFO: RESTEASY002200: Adding class resource org.littlewings.graal.nativeimage.HelloResource from Application class org.littlewings.graal.nativeimage.JaxrsActivator
4 27, 2019 9:05:38 午後 org.xnio.Xnio <clinit>
INFO: XNIO version 3.3.8.Final
4 27, 2019 9:05:38 午後 org.xnio.nio.NioXnio <clinit>
INFO: XNIO NIO Implementation Version 3.3.8.Final

微妙ですね。

ではこれを、Javaがインストールされていない環境でも動作するネイティブイメージができるまで、頑張って変えていって
みましょう。

参考にしたのは、こちらです。

Netty HTTP HelloWorldの起動が600msくらいだったんだけどGraalVMのNative Imageを使うと15msくらいになったー - Mitsuyuki.Shiiba

GitHub - cstancu/netty-native-demo: Instant Netty startup using GraalVM's Native Image Generation

ビルド時に存在しないクラスを無視してみる

先ほどの警告を見ると、ビルド時に存在しないクラスに対して怒っているようです。

Warning: Abort stand-alone image build. com.oracle.graal.pointsto.constraints.UnresolvedElementException: Discovered unresolved type during parsing: org.osgi.framework.FrameworkUtil. To diagnose the issue you can use the --allow-incomplete-classpath option. The missing type is then reported at run time when it is accessed the first time.

「--allow-incomplete-classpath」オプションを付けてみたら?と言っているので、試してみましょう。

以下のように、Native Image Maven Pluginの設定に反映。

                        <configuration>
                            <mainClass>org.littlewings.graal.nativeimage.App</mainClass>
                            <imageName>resteasy-undertow</imageName>
                            <buildArgs>--allow-incomplete-classpath</buildArgs>
                        </configuration>

すると、警告の内容が変わって、数がだいぶ増えます。

Warning: class initialization of class io.undertow.protocols.alpn.JettyAlpnProvider$Impl failed with exception java.lang.NoClassDefFoundError: org/eclipse/jetty/alpn/ALPN$Provider. This class will be initialized at run time because either option --report-unsupported-elements-at-runtime or option --allow-incomplete-classpath is used for image building. Use the option --delay-class-initialization-to-runtime=io.undertow.protocols.alpn.JettyAlpnProvider$Impl to explicitly request delayed initialization of this class.
[resteasy-undertow:14110]     analysis:   9,815.47 ms
Warning: Abort stand-alone image build. Unsupported features in 3 methods
Detailed message:
Error: com.oracle.graal.pointsto.constraints.UnsupportedFeatureException: Detected a direct/mapped ByteBuffer in the image heap. A direct ByteBuffer has a pointer to unmanaged C memory, and C memory from the image generator is not available at image run time. A mapped ByteBuffer references a file descriptor, which is no longer open and mapped at run time. The object was probably created by a class initializer and is reachable from a static field. By default, all class initialization is done during native image building.You can manually delay class initialization to image run time by using the option --delay-class-initialization-to-runtime=<class-name>. Or you can write your own initialization methods and call them explicitly from your main entry point.
Trace: 
    at parsing io.undertow.server.protocol.ajp.AjpServerRequestConduit.read(AjpServerRequestConduit.java:195)
Call path from entry point to io.undertow.server.protocol.ajp.AjpServerRequestConduit.read(ByteBuffer): 
    at io.undertow.server.protocol.ajp.AjpServerRequestConduit.read(AjpServerRequestConduit.java:183)
    at io.undertow.protocols.ssl.SslConduit.doUnwrap(SslConduit.java:703)
    at io.undertow.protocols.ssl.SslConduit.doHandshake(SslConduit.java:648)
    at io.undertow.protocols.ssl.SslConduit.access$900(SslConduit.java:63)
    at io.undertow.protocols.ssl.SslConduit$5$1.run(SslConduit.java:1084)
    at com.oracle.svm.core.jdk.RuntimeSupport.executeHooks(RuntimeSupport.java:144)
    at com.oracle.svm.core.jdk.RuntimeSupport.executeStartupHooks(RuntimeSupport.java:89)
    at com.oracle.svm.core.JavaMainWrapper.run(JavaMainWrapper.java:145)
    at com.oracle.svm.core.code.IsolateEnterStub.JavaMainWrapper_run_5087f5482cc9a6abc971913ece43acb471d2631b(generated:0)
Error: com.oracle.graal.pointsto.constraints.UnsupportedFeatureException: Detected a direct/mapped ByteBuffer in the image heap. A direct ByteBuffer has a pointer to unmanaged C memory, and C memory from the image generator is not available at image run time. A mapped ByteBuffer references a file descriptor, which is no longer open and mapped at run time. The object was probably created by a class initializer and is reachable from a static field. By default, all class initialization is done during native image building.You can manually delay class initialization to image run time by using the option --delay-class-initialization-to-runtime=<class-name>. Or you can write your own initialization methods and call them explicitly from your main entry point.
Trace: 
    at parsing io.undertow.server.protocol.ajp.AjpServerResponseConduit.flush(AjpServerResponseConduit.java:404)
Call path from entry point to io.undertow.server.protocol.ajp.AjpServerResponseConduit.flush(): 
    at io.undertow.server.protocol.ajp.AjpServerResponseConduit.flush(AjpServerResponseConduit.java:402)
    at org.xnio.conduits.ConduitStreamSinkChannel.flush(ConduitStreamSinkChannel.java:162)
    at io.undertow.server.protocol.framed.AbstractFramedChannel.flushSenders(AbstractFramedChannel.java:609)
    at io.undertow.server.protocol.framed.AbstractFramedChannel$5.run(AbstractFramedChannel.java:724)
    at com.oracle.svm.core.jdk.RuntimeSupport.executeHooks(RuntimeSupport.java:144)
    at com.oracle.svm.core.jdk.RuntimeSupport.executeStartupHooks(RuntimeSupport.java:89)
    at com.oracle.svm.core.JavaMainWrapper.run(JavaMainWrapper.java:145)
    at com.oracle.svm.core.code.IsolateEnterStub.JavaMainWrapper_run_5087f5482cc9a6abc971913ece43acb471d2631b(generated:0)
Error: com.oracle.graal.pointsto.constraints.UnsupportedFeatureException: Detected a direct/mapped ByteBuffer in the image heap. A direct ByteBuffer has a pointer to unmanaged C memory, and C memory from the image generator is not available at image run time. A mapped ByteBuffer references a file descriptor, which is no longer open and mapped at run time. The object was probably created by a class initializer and is reachable from a static field. By default, all class initialization is done during native image building.You can manually delay class initialization to image run time by using the option --delay-class-initialization-to-runtime=<class-name>. Or you can write your own initialization methods and call them explicitly from your main entry point.
Trace: 
    at parsing org.xnio.channels.Channels.drain(Channels.java:817)
Call path from entry point to org.xnio.channels.Channels.drain(StreamSourceChannel, long): 
    at org.xnio.channels.Channels.drain(Channels.java:801)
    at io.undertow.server.HttpServerExchange.endExchange(HttpServerExchange.java:1646)
    at io.undertow.server.Connectors.executeRootHandler(Connectors.java:381)
    at io.undertow.server.HttpServerExchange$1.run(HttpServerExchange.java:830)
    at com.oracle.svm.core.jdk.RuntimeSupport.executeHooks(RuntimeSupport.java:144)
    at com.oracle.svm.core.jdk.RuntimeSupport.executeStartupHooks(RuntimeSupport.java:89)
    at com.oracle.svm.core.JavaMainWrapper.run(JavaMainWrapper.java:145)
    at com.oracle.svm.core.code.IsolateEnterStub.JavaMainWrapper_run_5087f5482cc9a6abc971913ece43acb471d2631b(generated:0)

Warning: Use -H:+ReportExceptionStackTraces to print stacktrace of underlying exception

そして、やっぱりJavaがないと起動できないイメージにフォールバックします…。

Warning: Image 'resteasy-undertow' is a fallback image that requires a JDK for execution (use --no-fallback to suppress fallback image generation).

そもそもフォールバックして欲しくないので、フォールバックする状況になったらエラーになるように、「--no-fallback」オプションを
つけてネイティブイメージを作成することにしましょう。

                        <configuration>
                            <mainClass>org.littlewings.graal.nativeimage.App</mainClass>
                            <imageName>resteasy-undertow</imageName>
                            <buildArgs>--no-fallback</buildArgs>
                        </configuration>

この状態でビルドすると、以下のように警告からエラーに変わります。

Error: com.oracle.graal.pointsto.constraints.UnresolvedElementException: Discovered unresolved type during parsing: org.osgi.framework.FrameworkUtil. To diagnose the issue you can use the --allow-incomplete-classpath option. The missing type is then reported at run time when it is accessed the first time.
Detailed message:
Trace: 
    at parsing org.xnio.Xnio$OsgiSupport.doGetOsgiService(Xnio.java:276)
Call path from entry point to org.xnio.Xnio$OsgiSupport.doGetOsgiService(): 
    at org.xnio.Xnio$OsgiSupport.doGetOsgiService(Xnio.java:276)
    at org.xnio.Xnio.doGetInstance(Xnio.java:261)
    at org.xnio.Xnio.getInstance(Xnio.java:187)
    at io.undertow.Undertow.start(Undertow.java:118)
    at org.jboss.resteasy.plugins.server.undertow.UndertowJaxrsServer.start(UndertowJaxrsServer.java:281)
    at org.littlewings.graal.resteay.App.main(App.java:20)
    at com.oracle.svm.core.JavaMainWrapper.run(JavaMainWrapper.java:153)
    at com.oracle.svm.core.code.IsolateEnterStub.JavaMainWrapper_run_5087f5482cc9a6abc971913ece43acb471d2631b(generated:0)

Error: Use -H:+ReportExceptionStackTraces to print stacktrace of underlying exception
Error: Image build request failed with exit status 1

以降は、このbuildArgsを調整する時は、buildArgs要素のみを記載します。

ちなみにですね、このアプリケーション、うちの環境だとネイティブイメージにビルドするのに、1回あたり
50秒くらいかかります。

代替のメソッドを作成する

というわけで、この部分をなんとかしていきましょう。

Error: com.oracle.graal.pointsto.constraints.UnresolvedElementException: Discovered unresolved type during parsing: org.osgi.framework.FrameworkUtil. To diagnose the issue you can use the --allow-incomplete-classpath option. The missing type is then reported at run time when it is accessed the first time.
Detailed message:
Trace: 
    at parsing org.xnio.Xnio$OsgiSupport.doGetOsgiService(Xnio.java:276)
Call path from entry point to org.xnio.Xnio$OsgiSupport.doGetOsgiService(): 
    at org.xnio.Xnio$OsgiSupport.doGetOsgiService(Xnio.java:276)
    at org.xnio.Xnio.doGetInstance(Xnio.java:261)
    at org.xnio.Xnio.getInstance(Xnio.java:187)
    at io.undertow.Undertow.start(Undertow.java:118)
    at org.jboss.resteasy.plugins.server.undertow.UndertowJaxrsServer.start(UndertowJaxrsServer.java:281)
    at org.littlewings.graal.resteay.App.main(App.java:20)
    at com.oracle.svm.core.JavaMainWrapper.run(JavaMainWrapper.java:153)
    at com.oracle.svm.core.code.IsolateEnterStub.JavaMainWrapper_run_5087f5482cc9a6abc971913ece43acb471d2631b(generated:0)

今回エラーになっているのは、XNIOのこの部分なのですが

        try {
            Xnio xnio = OsgiSupport.doGetOsgiService();
            if (xnio != null) {
                return xnio;
            }
        } catch (NoClassDefFoundError t) {
            // Ignore
        } catch (Throwable t) {
            msg.debugf(t, "Not using OSGi service");
        }
        throw msg.noProviderFound();
    }

    static class OsgiSupport {

        static Xnio doGetOsgiService() {
            Bundle bundle = FrameworkUtil.getBundle(Xnio.class);
            BundleContext context = bundle.getBundleContext();
            if (context == null) {
                throw new IllegalStateException("Bundle not started");
            }
            ServiceReference<Xnio> sr = context.getServiceReference(Xnio.class);
            if (sr == null) {
                return null;
            }
            return context.getService(sr);
        }

    }

今回、OSGiは使わないので無視したいところです。ここで、この代替となる処理を書いていきます。

まずは、pom.xmlに以下の依存関係をprovidedスコープで追加します。

        <dependency>
          <groupId>com.oracle.substratevm</groupId>
          <artifactId>svm</artifactId>
          <version>1.0.0-rc16</version>
          <scope>provided</scope>
        </dependency>

そして、以下のようなクラスを作成します。
src/main/java/org/littlewings/graal/nativeimage/RestEasyUndertowSubstitutions.java

package org.littlewings.graal.nativeimage;

import com.oracle.svm.core.annotate.Substitute;
import com.oracle.svm.core.annotate.TargetClass;
import org.xnio.Xnio;

public class RestEasyUndertowSubstitutions {
}

@TargetClass(className = "org.xnio.Xnio$OsgiSupport")
final class Target_org_xnio_Xnio_OsgiSupport {
    @Substitute
    static Xnio doGetOsgiService() {
        return null;
    }
}

@TargetClassアノテーションには、Classクラス、可視性の都合でアクセスできないクラスは文字列で渡し、@Substituteアノテーション
使うことでその代替処理を書くことができます。

https://github.com/oracle/graal/tree/vm-1.0.0-rc16/substratevm/src/com.oracle.svm.core/src/com/oracle/svm/core/annotate

以下の例だと、org.xnio.Xnio$OsgiSupportクラスの、staticメソッドdoGetOsgiServiceを置き換えています。

@TargetClass(className = "org.xnio.Xnio$OsgiSupport")
final class Target_org_xnio_Xnio_OsgiSupport {
    @Substitute
    static Xnio doGetOsgiService() {
        return null;
    }
}

今回はこのメソッドの結果は使わないので、nullを返すことにしました。

ちなみに、作成したクラスにはfinal修飾子が付いていますが、これを外すとエラーになります…。

Error: Annotated class must be final: class org.littlewings.graal.nativeimage.Target_org_xnio_Xnio_OsgiSupport

で、ビルド…まだまだエラーは出ます…。

Error: Unsupported features in 5 methods
Detailed message:
Error: Class initialization failed: io.undertow.protocols.alpn.JettyAlpnProvider$Impl
Original exception that caused the problem: java.lang.NoClassDefFoundError: org/eclipse/jetty/alpn/ALPN$Provider
    at sun.misc.Unsafe.ensureClassInitialized(Native Method)
    at com.oracle.svm.hosted.classinitialization.CommonClassInitializationSupport.ensureClassInitialized(CommonClassInitializationSupport.java:127)
    at com.oracle.svm.hosted.classinitialization.EagerClassInitialization.computeInitKindAndMaybeInitializeClass(EagerClassInitialization.java:66)
    at com.oracle.svm.hosted.classinitialization.CommonClassInitializationSupport.computeInitKindAndMaybeInitializeClass(CommonClassInitializationSupport.java:84)
    at com.oracle.svm.hosted.classinitialization.CommonClassInitializationSupport.maybeInitializeHosted(CommonClassInitializationSupport.java:113)
    at com.oracle.svm.hosted.SVMHost.registerType(SVMHost.java:179)
    at com.oracle.graal.pointsto.meta.AnalysisUniverse.createType(AnalysisUniverse.java:263)
    at com.oracle.graal.pointsto.meta.AnalysisUniverse.lookupAllowUnresolved(AnalysisUniverse.java:204)
    at com.oracle.graal.pointsto.meta.AnalysisUniverse.lookup(AnalysisUniverse.java:181)
    at com.oracle.graal.pointsto.meta.AnalysisMethod.getDeclaringClass(AnalysisMethod.java:336)
    at com.oracle.graal.pointsto.meta.AnalysisMethod.<init>(AnalysisMethod.java:115)
    at com.oracle.graal.pointsto.meta.AnalysisUniverse.createMethod(AnalysisUniverse.java:411)
    at com.oracle.graal.pointsto.meta.AnalysisUniverse.lookupAllowUnresolved(AnalysisUniverse.java:399)
    at com.oracle.graal.pointsto.infrastructure.WrappedConstantPool.lookupMethod(WrappedConstantPool.java:115)
    at org.graalvm.compiler.java.BytecodeParser.lookupMethod(BytecodeParser.java:4272)
    at org.graalvm.compiler.java.BytecodeParser.genInvokeStatic(BytecodeParser.java:1506)
    at org.graalvm.compiler.java.BytecodeParser.processBytecode(BytecodeParser.java:5257)
    at org.graalvm.compiler.java.BytecodeParser.iterateBytecodesForBlock(BytecodeParser.java:3416)
    at org.graalvm.compiler.java.BytecodeParser.processBlock(BytecodeParser.java:3223)
    at org.graalvm.compiler.java.BytecodeParser.build(BytecodeParser.java:944)
    at org.graalvm.compiler.java.BytecodeParser.buildRootMethod(BytecodeParser.java:838)
    at org.graalvm.compiler.java.GraphBuilderPhase$Instance.run(GraphBuilderPhase.java:84)
    at org.graalvm.compiler.phases.Phase.run(Phase.java:49)
    at org.graalvm.compiler.phases.BasePhase.apply(BasePhase.java:197)
    at org.graalvm.compiler.phases.Phase.apply(Phase.java:42)
    at org.graalvm.compiler.phases.Phase.apply(Phase.java:38)
    at com.oracle.graal.pointsto.flow.MethodTypeFlowBuilder.parse(MethodTypeFlowBuilder.java:211)
    at com.oracle.graal.pointsto.flow.MethodTypeFlowBuilder.apply(MethodTypeFlowBuilder.java:330)
    at com.oracle.graal.pointsto.flow.MethodTypeFlow.doParse(MethodTypeFlow.java:310)
    at com.oracle.graal.pointsto.flow.MethodTypeFlow.ensureParsed(MethodTypeFlow.java:300)
    at com.oracle.graal.pointsto.flow.MethodTypeFlow.addContext(MethodTypeFlow.java:107)
    at com.oracle.graal.pointsto.DefaultAnalysisPolicy$DefaultVirtualInvokeTypeFlow.onObservedUpdate(DefaultAnalysisPolicy.java:191)
    at com.oracle.graal.pointsto.flow.TypeFlow.notifyObservers(TypeFlow.java:352)
    at com.oracle.graal.pointsto.flow.TypeFlow.update(TypeFlow.java:394)
    at com.oracle.graal.pointsto.BigBang$2.run(BigBang.java:509)
    at com.oracle.graal.pointsto.util.CompletionExecutor.lambda$execute$0(CompletionExecutor.java:171)
    at java.util.concurrent.ForkJoinTask$RunnableExecuteAction.exec(ForkJoinTask.java:1402)
    at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:289)
    at java.util.concurrent.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1056)
    at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1692)
    at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:157)
Caused by: java.lang.ClassNotFoundException: org.eclipse.jetty.alpn.ALPN$Provider
    at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    ... 41 more
Error: com.oracle.graal.pointsto.constraints.UnresolvedElementException: Discovered unresolved method during parsing: io.undertow.protocols.alpn.JettyAlpnProvider$Impl.setProtocols(javax.net.ssl.SSLEngine, java.lang.String[]). To diagnose the issue you can use the --allow-incomplete-classpath option. The missing method is then reported at run time when it is accessed the first time.
Trace: 
    at parsing io.undertow.protocols.alpn.JettyAlpnProvider.setProtocols(JettyAlpnProvider.java:62)
Call path from entry point to io.undertow.protocols.alpn.JettyAlpnProvider.setProtocols(SSLEngine, String[]): 
    at io.undertow.protocols.alpn.JettyAlpnProvider.setProtocols(JettyAlpnProvider.java:62)
    at io.undertow.server.protocol.http.AlpnOpenListener$1$1.run(AlpnOpenListener.java:294)
    at com.oracle.svm.core.jdk.RuntimeSupport.executeHooks(RuntimeSupport.java:144)
    at com.oracle.svm.core.jdk.RuntimeSupport.executeStartupHooks(RuntimeSupport.java:89)
    at com.oracle.svm.core.JavaMainWrapper.run(JavaMainWrapper.java:145)
    at com.oracle.svm.core.code.IsolateEnterStub.JavaMainWrapper_run_5087f5482cc9a6abc971913ece43acb471d2631b(generated:0)
Error: com.oracle.graal.pointsto.constraints.UnsupportedFeatureException: Detected a direct/mapped ByteBuffer in the image heap. A direct ByteBuffer has a pointer to unmanaged C memory, and C memory from the image generator is not available at image run time. A mapped ByteBuffer references a file descriptor, which is no longer open and mapped at run time. The object was probably created by a class initializer and is reachable from a static field. By default, all class initialization is done during native image building.You can manually delay class initialization to image run time by using the option --delay-class-initialization-to-runtime=<class-name>. Or you can write your own initialization methods and call them explicitly from your main entry point.
Trace: 
    at parsing io.undertow.server.protocol.ajp.AjpServerRequestConduit.read(AjpServerRequestConduit.java:195)
Call path from entry point to io.undertow.server.protocol.ajp.AjpServerRequestConduit.read(ByteBuffer): 
    at io.undertow.server.protocol.ajp.AjpServerRequestConduit.read(AjpServerRequestConduit.java:183)
    at org.xnio.conduits.ConduitStreamSourceChannel.read(ConduitStreamSourceChannel.java:127)
    at io.undertow.server.protocol.http.HttpReadListener.handleEventWithNoRunningRequest(HttpReadListener.java:158)
    at io.undertow.server.protocol.http.HttpReadListener.handleEvent(HttpReadListener.java:136)
    at io.undertow.server.protocol.http.HttpReadListener.run(HttpReadListener.java:401)
    at com.oracle.svm.core.jdk.RuntimeSupport.executeHooks(RuntimeSupport.java:144)
    at com.oracle.svm.core.jdk.RuntimeSupport.executeStartupHooks(RuntimeSupport.java:89)
    at com.oracle.svm.core.JavaMainWrapper.run(JavaMainWrapper.java:145)
    at com.oracle.svm.core.code.IsolateEnterStub.JavaMainWrapper_run_5087f5482cc9a6abc971913ece43acb471d2631b(generated:0)
Error: com.oracle.graal.pointsto.constraints.UnsupportedFeatureException: Detected a direct/mapped ByteBuffer in the image heap. A direct ByteBuffer has a pointer to unmanaged C memory, and C memory from the image generator is not available at image run time. A mapped ByteBuffer references a file descriptor, which is no longer open and mapped at run time. The object was probably created by a class initializer and is reachable from a static field. By default, all class initialization is done during native image building.You can manually delay class initialization to image run time by using the option --delay-class-initialization-to-runtime=<class-name>. Or you can write your own initialization methods and call them explicitly from your main entry point.
Trace: 
    at parsing io.undertow.server.protocol.ajp.AjpServerResponseConduit.flush(AjpServerResponseConduit.java:404)
Call path from entry point to io.undertow.server.protocol.ajp.AjpServerResponseConduit.flush(): 
    at io.undertow.server.protocol.ajp.AjpServerResponseConduit.flush(AjpServerResponseConduit.java:402)
    at org.xnio.conduits.ConduitStreamSinkChannel.flush(ConduitStreamSinkChannel.java:162)
    at io.undertow.server.protocol.framed.AbstractFramedChannel.flushSenders(AbstractFramedChannel.java:609)
    at io.undertow.server.protocol.framed.AbstractFramedChannel$5.run(AbstractFramedChannel.java:724)
    at com.oracle.svm.core.jdk.RuntimeSupport.executeHooks(RuntimeSupport.java:144)
    at com.oracle.svm.core.jdk.RuntimeSupport.executeStartupHooks(RuntimeSupport.java:89)
    at com.oracle.svm.core.JavaMainWrapper.run(JavaMainWrapper.java:145)
    at com.oracle.svm.core.code.IsolateEnterStub.JavaMainWrapper_run_5087f5482cc9a6abc971913ece43acb471d2631b(generated:0)
Error: com.oracle.graal.pointsto.constraints.UnsupportedFeatureException: Detected a direct/mapped ByteBuffer in the image heap. A direct ByteBuffer has a pointer to unmanaged C memory, and C memory from the image generator is not available at image run time. A mapped ByteBuffer references a file descriptor, which is no longer open and mapped at run time. The object was probably created by a class initializer and is reachable from a static field. By default, all class initialization is done during native image building.You can manually delay class initialization to image run time by using the option --delay-class-initialization-to-runtime=<class-name>. Or you can write your own initialization methods and call them explicitly from your main entry point.
Trace: 
    at parsing org.xnio.channels.Channels.drain(Channels.java:817)
Call path from entry point to org.xnio.channels.Channels.drain(StreamSourceChannel, long): 
    at org.xnio.channels.Channels.drain(Channels.java:801)
    at io.undertow.server.HttpServerExchange.endExchange(HttpServerExchange.java:1646)
    at io.undertow.server.Connectors.executeRootHandler(Connectors.java:381)
    at io.undertow.server.HttpServerExchange$1.run(HttpServerExchange.java:830)
    at com.oracle.svm.core.jdk.RuntimeSupport.executeHooks(RuntimeSupport.java:144)
    at com.oracle.svm.core.jdk.RuntimeSupport.executeStartupHooks(RuntimeSupport.java:89)
    at com.oracle.svm.core.JavaMainWrapper.run(JavaMainWrapper.java:145)
    at com.oracle.svm.core.code.IsolateEnterStub.JavaMainWrapper_run_5087f5482cc9a6abc971913ece43acb471d2631b(generated:0)

Error: Use -H:+ReportExceptionStackTraces to print stacktrace of underlying exception
Error: Image build request failed with exit status 1

こちらについては、ALPNは今回使わないので代替処理を書くようにします。

Detailed message:
Error: Class initialization failed: io.undertow.protocols.alpn.JettyAlpnProvider$Impl
Original exception that caused the problem: java.lang.NoClassDefFoundError: org/eclipse/jetty/alpn/ALPN$Provider
    at sun.misc.Unsafe.ensureClassInitialized(Native Method)

先ほど作成した、RestEasyUndertowSubstitutionsクラスのファイルの中に、以下を追加。

@TargetClass(io.undertow.protocols.alpn.JettyAlpnProvider.class)
final class Target_io_undertow_protocols_alpn_JettyAlpnProvider {
    @Substitute
    public SSLEngine setProtocols(SSLEngine engine, String[] protocols) {
        // no-op
        return null;
    }
}

遅延初期化する

こういうものについては、クラスの初期化を遅延させてあげる必要があるようです。

Error: com.oracle.graal.pointsto.constraints.UnsupportedFeatureException: Detected a direct/mapped ByteBuffer in the image heap. A direct ByteBuffer has a pointer to unmanaged C memory, and C memory from the image generator is not available at image run time. A mapped ByteBuffer references a file descriptor, which is no longer open and mapped at run time. The object was probably created by a class initializer and is reachable from a static field. By default, all class initialization is done during native image building.You can manually delay class initialization to image run time by using the option --delay-class-initialization-to-runtime=<class-name>. Or you can write your own initialization methods and call them explicitly from your main entry point.
Trace: 
    at parsing org.xnio.channels.Channels.drain(Channels.java:817)
Call path from entry point to org.xnio.channels.Channels.drain(StreamSourceChannel, long): 
    at org.xnio.channels.Channels.drain(Channels.java:801)
    at io.undertow.server.HttpServerExchange.endExchange(HttpServerExchange.java:1646)
    at io.undertow.server.Connectors.executeRootHandler(Connectors.java:381)
    at io.undertow.server.HttpServerExchange$1.run(HttpServerExchange.java:830)
    at com.oracle.svm.core.jdk.RuntimeSupport.executeHooks(RuntimeSupport.java:144)
    at com.oracle.svm.core.jdk.RuntimeSupport.executeStartupHooks(RuntimeSupport.java:89)
    at com.oracle.svm.core.JavaMainWrapper.run(JavaMainWrapper.java:145)
    at com.oracle.svm.core.code.IsolateEnterStub.JavaMainWrapper_run_5087f5482cc9a6abc971913ece43acb471d2631b(generated:0)

エラーメッセージとして、「--delay-class-initialization-to-runtime=<class-name>」が出力されているものですね。

pom.xmlに、以下のように必要な分だけ「--delay-class-initialization-to-runtime」オプションとクラス名のペアを追加します。

                            <buildArgs>--no-fallback --delay-class-initialization-to-runtime=io.undertow.server.protocol.ajp.AjpServerRequestConduit --delay-class-initialization-to-runtime=io.undertow.server.protocol.ajp.AjpServerResponseConduit --delay-class-initialization-to-runtime=org.xnio.channels.Channels</buildArgs>

すると、ビルドが通るようになります。

ちょっとこういう警告が出るのですが、今回は無視します…。

WARNING: Could not register reflection metadata for io.undertow.protocols.alpn.JettyAlpnProvider. Reason: java.lang.NoClassDefFoundError: org/eclipse/jetty/alpn/ALPN$ClientProvider.

実行してみましょう。

$ target/resteasy-undertow 
Exception in thread "main" java.lang.IllegalArgumentException: UT010010: Servlet of type class org.jboss.resteasy.plugins.server.servlet.HttpServlet30Dispatcher must have a default constructor
    at io.undertow.servlet.api.ServletInfo.<init>(ServletInfo.java:85)
    at io.undertow.servlet.Servlets.servlet(Servlets.java:102)
    at org.jboss.resteasy.plugins.server.undertow.UndertowJaxrsServer.undertowDeployment(UndertowJaxrsServer.java:66)
    at org.jboss.resteasy.plugins.server.undertow.UndertowJaxrsServer.undertowDeployment(UndertowJaxrsServer.java:90)
    at org.jboss.resteasy.plugins.server.undertow.UndertowJaxrsServer.deploy(UndertowJaxrsServer.java:164)
    at org.jboss.resteasy.plugins.server.undertow.UndertowJaxrsServer.deploy(UndertowJaxrsServer.java:157)
    at org.jboss.resteasy.plugins.server.undertow.UndertowJaxrsServer.deploy(UndertowJaxrsServer.java:145)
    at org.littlewings.graal.resteay.App.main(App.java:16)

エラーになりました…。どうやら、コンストラクタがわからないようです。

リフレクションの情報を登録する

ネイティブイメージを作る際には、リフレクションを使ったコードは置き換えられることになりますが、ある程度ビルド時に
解析してはくれるようです。

graal/REFLECTION.md at vm-1.0.0-rc16 · oracle/graal · GitHub

Automatic detection

ですが、コード解析でわからないものは、登録が必要になります。

登録には、設定ファイルで行う方法とAPIで行う方法があります。

Manual configuration

参考にしたサイトは設定ファイルを作成し、「native-image」コマンドのオプション(「-H:ReflectionConfigurationResources」)
として設定していますが、今回はAPIを使って登録してみます。

import文を追加。

package org.littlewings.graal.nativeimage;

import javax.net.ssl.SSLEngine;

import com.oracle.svm.core.annotate.AutomaticFeature;
import com.oracle.svm.core.annotate.Substitute;
import com.oracle.svm.core.annotate.TargetClass;
import org.graalvm.nativeimage.hosted.Feature;
import org.graalvm.nativeimage.hosted.RuntimeReflection;
import org.jboss.resteasy.plugins.server.servlet.HttpServlet30Dispatcher;
import org.xnio.Xnio;

public class RestEasyUndertowSubstitutions {
}

以下のように、対象のコンストラクタを登録します。

@AutomaticFeature
class RuntimeReflectionRegistrationFeature implements Feature {
    @Override
    public void beforeAnalysis(Feature.BeforeAnalysisAccess access) {
        try {
            RuntimeReflection.register(HttpServlet30Dispatcher.class.getDeclaredConstructor());
        } catch (NoSuchMethodException e) {
            throw new RuntimeException(e);
        }
    }
}

このように、対象となるメソッドなどを登録していきます。

今回は、最終的にこのようになりました。

import com.oracle.svm.core.annotate.AutomaticFeature;
import com.oracle.svm.core.annotate.Substitute;
import com.oracle.svm.core.annotate.TargetClass;
import io.undertow.server.protocol.http.HttpRequestParser$$generated;
import io.undertow.servlet.handlers.DefaultServlet;
import org.graalvm.nativeimage.hosted.Feature;
import org.graalvm.nativeimage.hosted.RuntimeReflection;
import org.jboss.resteasy.plugins.server.servlet.HttpServlet30Dispatcher;
import org.xnio.OptionMap;
import org.xnio.Xnio;

// 省略

@AutomaticFeature
class RuntimeReflectionRegistrationFeature implements Feature {
    @Override
    public void beforeAnalysis(Feature.BeforeAnalysisAccess access) {
        try {
            RuntimeReflection.register(HelloResource.class.getDeclaredConstructor());
            RuntimeReflection.register(HelloResource.class.getDeclaredMethod("message"));

            RuntimeReflection.register(HttpServlet30Dispatcher.class.getDeclaredConstructor());
            RuntimeReflection.register(DefaultServlet.class.getDeclaredConstructor());

            RuntimeReflection.register(HttpRequestParser$$generated.class);
            RuntimeReflection.register(HttpRequestParser$$generated.class.getDeclaredConstructor(OptionMap.class));
        } catch (NoSuchMethodException e) {
            throw new RuntimeException(e);
        }
    }
}

自分が作成したクラスやメソッドも登録しないと、今回のケースではJAX-RSリソースとして機能しませんでした…。

            RuntimeReflection.register(HelloResource.class.getDeclaredConstructor());
            RuntimeReflection.register(HelloResource.class.getDeclaredMethod("message"));

スタックトレースの足跡。

$ target/resteasy-undertow
Exception in thread "main" java.lang.RuntimeException: RESTEASY003190: Could not find constructor for class: org.littlewings.graal.resteay.HelloResource
    at org.jboss.resteasy.spi.metadata.ResourceBuilder.getConstructor(ResourceBuilder.java:805)
    at org.jboss.resteasy.plugins.server.resourcefactory.POJOResourceFactory.registered(POJOResourceFactory.java:56)
    at org.jboss.resteasy.core.ResourceMethodRegistry.addResourceFactory(ResourceMethodRegistry.java:213)
    at org.jboss.resteasy.core.ResourceMethodRegistry.addResourceFactory(ResourceMethodRegistry.java:199)
    at org.jboss.resteasy.core.ResourceMethodRegistry.addResourceFactory(ResourceMethodRegistry.java:185)
    at org.jboss.resteasy.core.ResourceMethodRegistry.addResourceFactory(ResourceMethodRegistry.java:162)
    at org.jboss.resteasy.core.ResourceMethodRegistry.addPerRequestResource(ResourceMethodRegistry.java:79)
    at org.jboss.resteasy.spi.ResteasyDeployment.registration(ResteasyDeployment.java:499)
    at org.jboss.resteasy.spi.ResteasyDeployment.startInternal(ResteasyDeployment.java:282)
    at org.jboss.resteasy.spi.ResteasyDeployment.start(ResteasyDeployment.java:89)
    at org.jboss.resteasy.plugins.server.servlet.ServletContainerDispatcher.init(ServletContainerDispatcher.java:119)
    at org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher.init(HttpServletDispatcher.java:36)
    at io.undertow.servlet.core.LifecyleInterceptorInvocation.proceed(LifecyleInterceptorInvocation.java:117)
    at io.undertow.servlet.core.ManagedServlet$DefaultInstanceStrategy.start(ManagedServlet.java:303)
    at io.undertow.servlet.core.ManagedServlet.createServlet(ManagedServlet.java:143)
    at io.undertow.servlet.core.DeploymentManagerImpl$2.call(DeploymentManagerImpl.java:583)
    at io.undertow.servlet.core.DeploymentManagerImpl$2.call(DeploymentManagerImpl.java:554)
    at io.undertow.servlet.core.ServletRequestContextThreadSetupAction$1.call(ServletRequestContextThreadSetupAction.java:42)
    at io.undertow.servlet.core.ContextClassLoaderSetupAction$1.call(ContextClassLoaderSetupAction.java:43)
    at io.undertow.servlet.core.DeploymentManagerImpl.start(DeploymentManagerImpl.java:596)
    at org.jboss.resteasy.plugins.server.undertow.UndertowJaxrsServer.deploy(UndertowJaxrsServer.java:269)
    at org.jboss.resteasy.plugins.server.undertow.UndertowJaxrsServer.deploy(UndertowJaxrsServer.java:183)
    at org.jboss.resteasy.plugins.server.undertow.UndertowJaxrsServer.deploy(UndertowJaxrsServer.java:157)
    at org.jboss.resteasy.plugins.server.undertow.UndertowJaxrsServer.deploy(UndertowJaxrsServer.java:145)
    at org.littlewings.graal.resteay.App.main(App.java:16)



$ target/resteasy-undertow
Exception in thread "main" java.lang.RuntimeException: java.lang.RuntimeException: java.lang.ClassNotFoundException: io.undertow.server.protocol.http.HttpRequestParser$$generated
    at io.undertow.Undertow.start(Undertow.java:249)
    at org.jboss.resteasy.plugins.server.undertow.UndertowJaxrsServer.start(UndertowJaxrsServer.java:281)
    at org.littlewings.graal.resteay.App.main(App.java:18)
Caused by: java.lang.RuntimeException: java.lang.ClassNotFoundException: io.undertow.server.protocol.http.HttpRequestParser$$generated
    at io.undertow.server.protocol.http.HttpRequestParser.instance(HttpRequestParser.java:221)
    at io.undertow.server.protocol.http.HttpOpenListener.<init>(HttpOpenListener.java:93)
    at io.undertow.Undertow.start(Undertow.java:179)
    ... 2 more
Caused by: java.lang.ClassNotFoundException: io.undertow.server.protocol.http.HttpRequestParser$$generated
    at com.oracle.svm.core.hub.ClassForNameSupport.forName(ClassForNameSupport.java:51)
    at java.lang.Class.forName(DynamicHub.java:1143)
    at io.undertow.server.protocol.http.HttpRequestParser.instance(HttpRequestParser.java:216)
    ... 4 more

脱線:JAXP

で、これでネイティブイメージを作成してビルドしてみると、今度はXerces関係で怒られます。

Caused by: java.lang.ClassNotFoundException: com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl

こんな感じに。

$ target/resteasy-undertow
Exception in thread "main" java.lang.RuntimeException: java.lang.RuntimeException: RESTEASY003940: Unable to instantiate MessageBodyReader
    at org.jboss.resteasy.plugins.providers.RegisterBuiltin.register(RegisterBuiltin.java:49)
    at org.jboss.resteasy.spi.ResteasyDeployment.startInternal(ResteasyDeployment.java:262)
    at org.jboss.resteasy.spi.ResteasyDeployment.start(ResteasyDeployment.java:89)
    at org.jboss.resteasy.plugins.server.servlet.ServletContainerDispatcher.init(ServletContainerDispatcher.java:119)
    at org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher.init(HttpServletDispatcher.java:36)
    at io.undertow.servlet.core.LifecyleInterceptorInvocation.proceed(LifecyleInterceptorInvocation.java:117)
    at io.undertow.servlet.core.ManagedServlet$DefaultInstanceStrategy.start(ManagedServlet.java:303)
    at io.undertow.servlet.core.ManagedServlet.createServlet(ManagedServlet.java:143)
    at io.undertow.servlet.core.DeploymentManagerImpl$2.call(DeploymentManagerImpl.java:583)
    at io.undertow.servlet.core.DeploymentManagerImpl$2.call(DeploymentManagerImpl.java:554)
    at io.undertow.servlet.core.ServletRequestContextThreadSetupAction$1.call(ServletRequestContextThreadSetupAction.java:42)
    at io.undertow.servlet.core.ContextClassLoaderSetupAction$1.call(ContextClassLoaderSetupAction.java:43)
    at io.undertow.servlet.core.DeploymentManagerImpl.start(DeploymentManagerImpl.java:596)
    at org.jboss.resteasy.plugins.server.undertow.UndertowJaxrsServer.deploy(UndertowJaxrsServer.java:269)
    at org.jboss.resteasy.plugins.server.undertow.UndertowJaxrsServer.deploy(UndertowJaxrsServer.java:183)
    at org.jboss.resteasy.plugins.server.undertow.UndertowJaxrsServer.deploy(UndertowJaxrsServer.java:157)
    at org.jboss.resteasy.plugins.server.undertow.UndertowJaxrsServer.deploy(UndertowJaxrsServer.java:145)
    at org.littlewings.graal.resteay.App.main(App.java:16)
Caused by: java.lang.RuntimeException: RESTEASY003940: Unable to instantiate MessageBodyReader
    at org.jboss.resteasy.spi.ResteasyProviderFactory.registerProvider(ResteasyProviderFactory.java:1761)
    at org.jboss.resteasy.spi.ResteasyProviderFactory.registerProvider(ResteasyProviderFactory.java:1684)
    at org.jboss.resteasy.plugins.providers.RegisterBuiltin.registerProviders(RegisterBuiltin.java:133)
    at org.jboss.resteasy.plugins.providers.RegisterBuiltin.register(RegisterBuiltin.java:45)
    ... 17 more
Caused by: java.lang.RuntimeException: RESTEASY003325: Failed to construct public org.jboss.resteasy.plugins.providers.DocumentProvider()
    at org.jboss.resteasy.core.ConstructorInjectorImpl.construct(ConstructorInjectorImpl.java:164)
    at org.jboss.resteasy.spi.ResteasyProviderFactory.createProviderInstance(ResteasyProviderFactory.java:2750)
    at org.jboss.resteasy.spi.ResteasyProviderFactory.addMessageBodyReader(ResteasyProviderFactory.java:1019)
    at org.jboss.resteasy.spi.ResteasyProviderFactory.registerProvider(ResteasyProviderFactory.java:1756)
    ... 20 more
Caused by: javax.xml.parsers.FactoryConfigurationError: Provider com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl not found
    at javax.xml.parsers.FactoryFinder.newInstance(FactoryFinder.java:200)
    at javax.xml.parsers.FactoryFinder.newInstance(FactoryFinder.java:152)
    at javax.xml.parsers.FactoryFinder.find(FactoryFinder.java:277)
    at javax.xml.parsers.DocumentBuilderFactory.newInstance(DocumentBuilderFactory.java:120)
    at org.jboss.resteasy.plugins.providers.DocumentProvider.<init>(DocumentProvider.java:57)
    at org.jboss.resteasy.plugins.providers.DocumentProvider.<init>(DocumentProvider.java:51)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
    at org.jboss.resteasy.core.ConstructorInjectorImpl.construct(ConstructorInjectorImpl.java:152)
    ... 23 more
Caused by: java.lang.ClassNotFoundException: com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl
    at com.oracle.svm.core.hub.ClassForNameSupport.forName(ClassForNameSupport.java:51)
    at java.lang.Class.forName(DynamicHub.java:1143)
    at javax.xml.parsers.FactoryFinder.getProviderClass(FactoryFinder.java:124)
    at javax.xml.parsers.FactoryFinder.newInstance(FactoryFinder.java:188)
    ... 30 more

以下の部分で、フォールバックしたデフォルトクラスがわからないようです。

   public DocumentProvider(final @Context ResteasyConfiguration config)
   {
      LogMessages.LOGGER.debugf("Provider : %s,  Method : DocumentProvider", getClass().getName());
      this.documentBuilder = DocumentBuilderFactory.newInstance();
      this.transformerFactory = TransformerFactory.newInstance();
    public static DocumentBuilderFactory newInstance() {
        return (DocumentBuilderFactory)FactoryFinder.find(DocumentBuilderFactory.class, "com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl");
    }

もう面倒になったので、以下のように置き換えました。

@TargetClass(javax.xml.parsers.DocumentBuilderFactory.class)
final class Target_javax_xml_parsers_DocumentBuilderFactory {
    @Substitute
    public static DocumentBuilderFactory newInstance() {
        return new DocumentBuilderFactoryImpl();
    }
}

@TargetClass(javax.xml.transform.TransformerFactory.class)
final class Target_javax_xml_transform_TransformerFactory {
    @Substitute
    public static TransformerFactory newInstance() throws TransformerFactoryConfigurationError {
        return new TransformerFactoryImpl();
    }
}

これでビルドすると、今度は起動するようになります。

$ target/resteasy-undertow

やっと動きました!

$ curl localhost:8080/hello
Hello RESTEasy!!

ログが出ていない?

ところで、この起動したネイティブイメージ、Javaアプリケーションの時やJavaが必要なフォールバックイメージの時と
比べると、標準出力にログが出なくなりました。

どうしたんでしょう?

ここで使われているのは、JBoss Logging越しのJavaの標準のLogger(JUL)です。

デフォルトの設定内容を見てみましょう。

$ grep -v '#' /usr/local/graalvm-ce/jre/lib/logging.properties


handlers= java.util.logging.ConsoleHandler


.level= INFO


java.util.logging.FileHandler.pattern = %h/java%u.log
java.util.logging.FileHandler.limit = 50000
java.util.logging.FileHandler.count = 1
java.util.logging.FileHandler.formatter = java.util.logging.XMLFormatter

java.util.logging.ConsoleHandler.level = INFO
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter



com.xyz.foo.level = SEVERE

この内容を失ったみたいですね…。

仕方がないので、今回はアプリケーション内でRoot Loggerを作成してあげることにします。

package org.littlewings.graal.nativeimage;

import java.util.logging.ConsoleHandler;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;

import io.undertow.Undertow;
import org.jboss.resteasy.plugins.server.undertow.UndertowJaxrsServer;
import org.jboss.resteasy.spi.ResteasyDeployment;

public class App {
    public static void main(String... args) {
        ConsoleHandler consoleHandler = new ConsoleHandler();
        consoleHandler.setLevel(Level.INFO);
        consoleHandler.setFormatter(new SimpleFormatter());

        Logger.getLogger("").addHandler(consoleHandler);

        UndertowJaxrsServer server = new UndertowJaxrsServer();

        ResteasyDeployment deployment = new ResteasyDeployment();
        deployment.setApplication(new JaxrsActivator());

        server.deploy(deployment);

        server.start(Undertow.builder().addHttpListener(8080, "0.0.0.0"));
    }
}

再度ビルド。

これで、今度はログが出るようになりました…。

$ target/resteasy-undertow
Apr 27, 2019 10:04:53 PM org.jboss.resteasy.spi.ResteasyDeployment processApplication
INFO: RESTEASY002225: Deploying javax.ws.rs.core.Application: class org.littlewings.graal.nativeimage.JaxrsActivator
Apr 27, 2019 10:04:53 PM org.jboss.resteasy.spi.ResteasyDeployment processApplication
INFO: RESTEASY002200: Adding class resource org.littlewings.graal.nativeimage.HelloResource from Application class org.littlewings.graal.nativeimage.JaxrsActivator

遠かった…。

まとめ

GraalVM(Substrate VM)を使ってネイティブイメージを作る時には、リフレクションや動的ロードに関する部分など、
その制限事項を回避したり置き換えたりするための細工をいろいろしないといけないことがよくわかりました。

graal/LIMITATIONS.md at vm-1.0.0-rc16 · oracle/graal · GitHub

いやぁ、大変でしたが、触った部分の話はまあまあわかったので、よしとしましょう。

ちなみに、1回のビルドにかかる時間とその内訳は、こんな感じです。

Build on Server(pid: 14110, port: 36203)
[resteasy-undertow:14110]    classlist:   2,017.46 ms
[resteasy-undertow:14110]        (cap):     761.32 ms
[resteasy-undertow:14110]        setup:   1,025.83 ms
WARNING: Could not register reflection metadata for io.undertow.protocols.alpn.JettyAlpnProvider. Reason: java.lang.NoClassDefFoundError: org/eclipse/jetty/alpn/ALPN$ClientProvider.
WARNING: Could not register reflection metadata for io.undertow.protocols.alpn.JettyAlpnProvider. Reason: java.lang.NoClassDefFoundError: io/undertow/protocols/alpn/JettyAlpnProvider$ALPNClientSelectionProvider.
[resteasy-undertow:14110]   (typeflow):  12,887.02 ms
[resteasy-undertow:14110]    (objects):  14,266.38 ms
[resteasy-undertow:14110]   (features):   1,301.23 ms
[resteasy-undertow:14110]     analysis:  29,128.81 ms
[resteasy-undertow:14110]     universe:     431.87 ms
[resteasy-undertow:14110]      (parse):   1,647.91 ms
[resteasy-undertow:14110]     (inline):   3,288.45 ms
[resteasy-undertow:14110]    (compile):   8,829.37 ms
[resteasy-undertow:14110]      compile:  14,800.83 ms
[resteasy-undertow:14110]        image:   1,704.56 ms
[resteasy-undertow:14110]        write:     612.32 ms
[resteasy-undertow:14110]      [total]:  49,791.81 ms
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  53.236 s
[INFO] Finished at: 2019-04-27T22:03:49+09:00
[INFO] ------------------------------------------------------------------------

オマケ

部分部分で書いてきた、GraalVMに関するコードやpom.xmlの部分を載せておきます。

pom.xmlのNative Image Maven Pluginの部分。

                    <plugin>
                        <groupId>com.oracle.substratevm</groupId>
                        <artifactId>native-image-maven-plugin</artifactId>
                        <version>1.0.0-rc16</version>
                        <executions>
                            <execution>
                                <goals>
                                    <goal>native-image</goal>
                                </goals>
                                <phase>package</phase>
                            </execution>
                        </executions>
                        <configuration>
                            <mainClass>org.littlewings.graal.nativeimage.App</mainClass>
                            <imageName>resteasy-undertow</imageName>
                            <buildArgs>--no-fallback --delay-class-initialization-to-runtime=io.undertow.server.protocol.ajp.AjpServerRequestConduit --delay-class-initialization-to-runtime=io.undertow.server.protocol.ajp.AjpServerResponseConduit --delay-class-initialization-to-runtime=org.xnio.channels.Channels</buildArgs>
                        </configuration>
                    </plugin>

代替機能や、リフレクションに関する情報を登録するクラス。
src/main/java/org/littlewings/graal/nativeimage/RestEasyUndertowSubstitutions.java

package org.littlewings.graal.nativeimage;

import javax.net.ssl.SSLEngine;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerFactoryConfigurationError;

import com.oracle.svm.core.annotate.AutomaticFeature;
import com.oracle.svm.core.annotate.Substitute;
import com.oracle.svm.core.annotate.TargetClass;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl;
import io.undertow.server.protocol.http.HttpRequestParser$$generated;
import io.undertow.servlet.handlers.DefaultServlet;
import org.graalvm.nativeimage.hosted.Feature;
import org.graalvm.nativeimage.hosted.RuntimeReflection;
import org.jboss.resteasy.plugins.server.servlet.HttpServlet30Dispatcher;
import org.xnio.OptionMap;
import org.xnio.Xnio;

public class RestEasyUndertowSubstitutions {
}

@TargetClass(className = "org.xnio.Xnio$OsgiSupport")
final class Target_org_xnio_Xnio_OsgiSupport {
    @Substitute
    static Xnio doGetOsgiService() {
        return null;
    }
}

@TargetClass(io.undertow.protocols.alpn.JettyAlpnProvider.class)
final class Target_io_undertow_protocols_alpn_JettyAlpnProvider {
    @Substitute
    public SSLEngine setProtocols(SSLEngine engine, String[] protocols) {
        // no-op
        return null;
    }
}

@TargetClass(javax.xml.parsers.DocumentBuilderFactory.class)
final class Target_javax_xml_parsers_DocumentBuilderFactory {
    @Substitute
    public static DocumentBuilderFactory newInstance() {
        return new DocumentBuilderFactoryImpl();
    }
}

@TargetClass(javax.xml.transform.TransformerFactory.class)
final class Target_javax_xml_transform_TransformerFactory {
    @Substitute
    public static TransformerFactory newInstance() throws TransformerFactoryConfigurationError {
        return new TransformerFactoryImpl();
    }
}

@AutomaticFeature
class RuntimeReflectionRegistrationFeature implements Feature {
    @Override
    public void beforeAnalysis(Feature.BeforeAnalysisAccess access) {
        try {
            RuntimeReflection.register(HelloResource.class.getDeclaredConstructor());
            RuntimeReflection.register(HelloResource.class.getDeclaredMethod("message"));

            RuntimeReflection.register(HttpServlet30Dispatcher.class.getDeclaredConstructor());
            RuntimeReflection.register(DefaultServlet.class.getDeclaredConstructor());

            RuntimeReflection.register(HttpRequestParser$$generated.class);
            RuntimeReflection.register(HttpRequestParser$$generated.class.getDeclaredConstructor(OptionMap.class));
        } catch (NoSuchMethodException e) {
            throw new RuntimeException(e);
        }
    }
}