MicronautアプリをIntelliJ IDEAから実行しようとしたときにApplicationContextBuilderが見つからないって言われたら

Micronaut 1.1.0 で mn create-app を使ってアプリを生成して、IndelliJ IDEAに取り込んで、Annotation Processorの設定もして、下の画像のあたりを押して Application.java から実行しようとしたら

f:id:bufferings:20190421173209p:plain

こんなエラーが出て実行できない (´・ω・`)ショボーン

Exception in thread "main" java.lang.NoClassDefFoundError: io/micronaut/context/ApplicationContextBuilder
    at java.base/java.lang.ClassLoader.defineClass1(Native Method)
    at java.base/java.lang.ClassLoader.defineClass(ClassLoader.java:1016)
    at java.base/java.security.SecureClassLoader.defineClass(SecureClassLoader.java:151)
    at java.base/jdk.internal.loader.BuiltinClassLoader.defineClass(BuiltinClassLoader.java:802)
    at java.base/jdk.internal.loader.BuiltinClassLoader.findClassOnClassPathOrNull(BuiltinClassLoader.java:700)
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClassOrNull(BuiltinClassLoader.java:623)
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:581)
    at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178)
    at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:521)
    at hello.mn.Application.main(Application.java:8)
Caused by: java.lang.ClassNotFoundException: io.micronaut.context.ApplicationContextBuilder
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:583)
    at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178)
    at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:521)
    ... 10 more

Process finished with exit code 1

対応はここに書いてた。

https://github.com/micronaut-projects/micronaut-core/issues/685#issuecomment-482415658

Run > Edit Configurations... から「Include dependencies with "Provided" scope」にチェックを入れたら動いた。

f:id:bufferings:20190421173922p:plain

17:41:09.521 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 3485ms. Server Running: http://localhost:8080

(∩´∀`)∩ワーイ

Micronaut 1.1.0 からは、Native Imageのビルドが簡単になってたよ

## 何も指定せずにNative Imageをビルドしても動くのなんでだろう?

この前、Micornautのアプリをnative-imageでビルドして遊んで

bufferings.hatenablog.com

その後に、Micronautとは関係なく、Nettyをnative-imageでビルドして遊んで

bufferings.hatenablog.com

ふと「あれ?MicronautもNetty使ってるはずなんだけど、何も指定せずにNative Imageをビルドしても動くのなんでだろう?」と思って色々見て回ったメモ。

## build-native-image.sh がなくなってる

きしださんのブログや

nowokay.hatenablog.com

kencharosさんのQiita記事を見てると

qiita.com

MicronautがNative Imageビルド用の build-native-image.sh というファイルがあったみたい。なんだけど、僕の生成したプロジェクトの中には見当たらない。

探してみたら、1.1.0.M2までは提供していたんだけど、その次のリリースで削除されたみたいね。

https://github.com/micronaut-projects/micronaut-profiles/blob/v1.1.0.M2/base/features/graal-native-image/skeleton/gradle-build/build-native-image.sh

build-native-image.sh の中身はこうなってる

./gradlew assemble
java -cp build/libs/@app.name@-*.jar io.micronaut.graal.reflect.GraalClassLoadingAnalyzer
native-image --no-server \
             --class-path build/libs/@app.name@-*.jar \
             -H:ReflectionConfigurationFiles=build/reflect.json \
             -H:EnableURLProtocols=http \
             -H:IncludeResources="logback.xml|application.yml" \
             -H:Name=@app.name@ \
             -H:Class=@defaultPackage@.Application \
             -H:+ReportUnsupportedElementsAtRuntime \
             -H:+AllowVMInspection \
             --allow-incomplete-classpath \
             --rerun-class-initialization-at-runtime='sun.security.jca.JCAUtil$CachedSecureRandomHolder,javax.net.ssl.SSLContext' \
             --delay-class-initialization-to-runtime=io.netty.handler.codec.http.HttpObjectEncoder,io.netty.handler.codec.http.websocketx.WebSocket00FrameEncoder,io.netty.handler.ssl.util.ThreadLocalInsecureRandom,com.sun.jndi.dns.DnsClient

それが、現在の最新版の1.1.0ではこんな感じでビルドできる

./gradlew assemble
native-image --no-server -cp build/libs/hello-graal-*.jar

シンプルになったね。だから build-native-image.sh は削除したってことみたい。いいね。でも、じゃあ GraalClassLoadingAnalyzer とか、native-imageのオプションはどこにいったんだろう?

## native-image.properties

mn create-app をするときに --features graal-native-image オプションをつけると、こういうファイルが生成される。(アプリ名は hello-graal にしてる)

❯ cat src/main/resources/META-INF/native-image/hello.graal/hello-graal-application/native-image.properties
Args = -H:IncludeResources=logback.xml|application.yml \
       -H:Name=hello-graal \
       -H:Class=hello.graal.Application

ふむ。この native-image.properties について調べると、ロジコさんが翻訳してくれてるこの記事の後半に書いてあった。

medium.com

GraalVMの最近のリリース以降、ネイティブイメージのビルドについて別の重要な改善をしています。JARファイル中のMETA-INF/native-imageにnative-image.propertiesファイルを埋め込むことができるようになりました。native-imageツールは、このリソースの場所にあるすべてのファイルを自動的に処理し、それらを使用してnative-imageのコマンドライン引数を作成します。

なるほど。

GraalVMのドキュメントを探してみたけど native-image.properties についての記述は見つからなかったので、ソースを見てみた。たぶんここかなぁ。

https://github.com/oracle/graal/blob/vm-1.0.0-rc15/substratevm/src/com.oracle.svm.driver/src/com/oracle/svm/driver/NativeImage.java#L762-L787

META-INF/native-image ディレクトリーの中で native-image.properties って名前で終わってるファイルをチェックしてるみたいね。

## ということは、Nettyは?

と思ってJARファイルの中を見てみたら、あった。そういうことかー。

f:id:bufferings:20190421131400p:plain:w300

## で、その元になってるファイルはどこにあるん?

と思って、JARを見たら micronaut-http-netty-1.1.0.jar の中に入ってた。

f:id:bufferings:20190421141215p:plain:w600

native-image.properties で「自分と同じディレクトリーにある reflection-config.json を読み込んでくれー」って言ってる。

## なんとなく、ソースも見たい

と思って探してみた。これだな。

https://github.com/micronaut-projects/micronaut-core/blob/v1.1.0/http-netty/src/main/java/io/micronaut/http/netty/channel/NettyThreadFactory.java#L40-L46

@TypeHint(value = {
        NioServerSocketChannel.class,
        NioSocketChannel.class
}, typeNames = {"sun.security.ssl.SSLContextImpl$TLSContext", "sun.nio.ch.SelectorImpl"},
   accessType = {TypeHint.AccessType.ALL_DECLARED_CONSTRUCTORS, TypeHint.AccessType.ALL_DECLARED_FIELDS}
)
public class NettyThreadFactory {

@TypeHint アノテーション@Introspected アノテーションがついてると、Native Image用のプロパティファイルを生成するみたい。

このアノテーションの処理はコンパイルタイムに GraalTypeElementVisitor がやってるっぽいね。

https://github.com/micronaut-projects/micronaut-core/blob/v1.1.0/graal/src/main/java/io/micronaut/graal/reflect/GraalTypeElementVisitor.java#L90

この仕組みができたから GraalClassLoadingAnalyzer の役目は終了したっぽい。 1.1.0 では削除されてる。

## Substitutionsに関して

は、ここだな。この前Netty試したときに書いたやつがそのまま書いてある。

https://github.com/micronaut-projects/micronaut-core/blob/v1.1.0/http-netty/src/main/java/io/micronaut/http/netty/graal/MicronautSubstitutions.java

## ライブラリー側でnative-image対応が可能になる

ロジコさんの記事に戻って見てみると、こう書いてある。

正しく適用すれば、Javaライブラリの作者は上述のメカニズムを使ってライブラリにnative-image互換性を持たせることができます。

なるほどなぁ。

## 気になるのは

こんな風に native-image.properties に書いてある場合

https://github.com/micronaut-projects/micronaut-core/blob/master/inject/src/main/resources/META-INF/native-image/io.micronaut/inject/native-image.properties

Args = -H:+ReportUnsupportedElementsAtRuntime \
       --allow-incomplete-classpath

知らないうちにオプションが適用されてしまってるんだろうなぁというところかな。

あと、Nettyのログは java.util.logging に限定されることになるんかな?(よく分かってない

## まとめ

Micronaut 1.1.0 からは、Native Imageのビルドが簡単になってたよ。面白かった。

僕らのモブプログラミングは「全員でプログラミングをする」ということではなかった

## 去年の夏ぐらいからサポートしているチーム

で、それまでもちょこちょこモブプログラミングを試してはいたんだけど、3月からは思い切ってそれを基本として開発をするようにした。つまり、3月からは1日中モブプログラミングをするのを毎日やってる。

プログラミングだけじゃなくて、設計も、運用も、テストも、全部モブでやってるので、僕らはそれをモブワークと呼んでる。

## やっていく中で学んだのは

モブプログラミング(モブワーク)は「全員でプログラミングをする」ということではなくて「全員で考えて取り組む」というだけのことだった。

サービスにとってどう動くのが良いかを全員で考える。

目の前のプロジェクトのことだけではなく、少し先を見据えてメンバー間の知識やスキルの共有や、チームがまだ詳しくない分野の学習をすることも含めて、どこにトレードオフスライダーをセットするのが良いかを全員で考える。

## 全員でプログラミングをするというのは

その手段のひとつに過ぎない。だから、そのチームは常に全員で作業をしている訳ではなくて、状況に応じて、全員モブ(5人)、モブ&ペア(3+2)、モブ&ソロ(4+1)、ペアペアソロ(2+2+1)などを切り替えながら開発を進めている。

2月までと違うのは「全員モブがデフォルト」ということだ。それまでは「全員別々がデフォルト」で、その中でペアやモブを組んでやっていた。でも、今はまず全員が集まることが基本で、そこから必要に応じて「ペアにわかれようか」とかの話をしているのだ。

外から見るとどちらもペアでやっているのだけど、実際に中にいると全く情報の流れが違う。

## 全員で毎日ふりかえって

次の日にトライする。その結果を見てまた次の日に別のやり方を試してみる。そうやって、自分たちに合った形の働き方を探っている。

例えば、そのチームはランチをとる時間がメンバーによって違うのだけど、最初はランチ時間を揃えてみて「まぁ、できなくはない。でもランチは自由に取りたい」って話がでて、じゃあ、ってことで好きな時間に取るようにしたら、今度は「誰かがランチに行ってる間は作業が止まってしまう」ってなって、じゃあ全員が揃ってなくても二人いたら進めようか、ってなって、今はランチの時間も作業が止まらずに進んでいる。

## チームはスクラムの中でモブワークをやっている

ので、2週間のスプリントがある。2週間の初日にスプリントレビュー・スプリントレトロスペクティブ・スプリントプランニングをやって、2日目から10日目まで開発をする。真ん中ぐらいでバックログリファインメントがある。3スプリント回してきて、だんだん自分たちのチカラをスムーズに発揮できるようになってきたと感じる。

## 1日の流れ

は、こんな感じ。

  • 9:00-10:00 学習セッション
  • 10:00-15:30 モブワーク
  • 15:30-16:30 個人の時間
  • 16:30-17:00 今日のレビュー・今日のふりかえり・明日のプランニング

## 良いリズムをつくりだしているのは

毎日のふりかえりだろうな。「何かもやっとする・・・」というときにそれをふせんに書いて口に出しても良い。誰もそれに対して否定をしない。という安心感がチームの中にある。

もう僕のサポートが要らないチームになってしまって、少し寂しい気持ちもあるけど、何か問題にぶつかっても、メンバー全員で意見を出し合って乗り越えていっているのを見ていると、とてもうれしい。

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

下記の記事を読んで「えー。実際に手を動かして、自分の目で見てみたいー。」ってなったので、書いてある通りにやってみたら10ms前後でほんとに起動した。へー。その途中で色々と知らないことがたくさんあって面白かった。

medium.com

## やったことを順番に書いていく

GraalVMでNative Imageを作るまでに色々やらなきゃいけないので、やったことを順番に書いていく。ソースコードはここに置いといて、順番にタグ付けしておいた。

github.com

使ったのは、GraalVM 1.0-RC15。SDKMAN!で指定した。

❯ sdk current java

Using java version 1.0.0-rc-15-grl

## v1 サンプルプロジェクトを作成

記事にもある通り Netty Exampleの中のHTTPのハローワールドを使ってサンプルプロジェクトを作った。v1がタグね。

maven-assembly-plugin でFatJARを作って実行できるようにしてあるので

      <plugin>
        <artifactId>maven-assembly-plugin</artifactId>
        <version>3.1.1</version>
        <configuration>
          <descriptorRefs>
            <descriptorRef>jar-with-dependencies</descriptorRef>
          </descriptorRefs>
          <archive>
            <manifest>
              <mainClass>com.example.demo.helloworld.HttpHelloWorldServer</mainClass>
            </manifest>
          </archive>
        </configuration>
        <executions>
          <execution>
            <id>make-assembly</id>
            <phase>package</phase>
            <goals>
              <goal>single</goal>
            </goals>
          </execution>
        </executions>
      </plugin>

packageすると

❯ ./mvnw clean package

target ディレクトリーに netty-native-demo-1.0-SNAPSHOT-jar-with-dependencies.jar が生成されるから、それを実行。

❯ java -jar target/netty-native-demo-1.0-SNAPSHOT-jar-with-dependencies.jar
Apr 15, 2019 9:59:48 PM io.netty.handler.logging.LoggingHandler channelRegistered
INFO: [id: 0x4b3bc1c7] REGISTERED
Apr 15, 2019 9:59:48 PM io.netty.handler.logging.LoggingHandler bind
INFO: [id: 0x4b3bc1c7] BIND: 0.0.0.0/0.0.0.0:8080
Open your web browser and navigate to http://127.0.0.1:8080/
Apr 15, 2019 9:59:48 PM io.netty.handler.logging.LoggingHandler channelActive
INFO: [id: 0x4b3bc1c7, L:/0:0:0:0:0:0:0:0:8080] ACTIVE

普通に起動して、Hello Worldが見える。

f:id:bufferings:20190415220143p:plain:w300

じゃ、ここからGraalVMでNative Imageを生成できるようにしていく。

## v2 native-image-maven-plugin を追加

記事にあるみたいに直接 native-image コマンドを叩いてもいいんだけど、Mavenでビルドできるのも楽そうでいいなと思って導入してみた。先に言っておくと、この時点ではビルドはエラーになる。

      <plugin>
        <groupId>com.oracle.substratevm</groupId>
        <artifactId>native-image-maven-plugin</artifactId>
        <version>1.0.0-rc15</version>
        <configuration>
          <imageName>netty-example-http</imageName>
        </configuration>
        <executions>
          <execution>
            <goals>
              <goal>native-image</goal>
            </goals>
            <phase>package</phase>
          </execution>
        </executions>
      </plugin>

このプラグインに関しては既に、かずひらさんが書いてくれてた!

❯ ./mvnw clean package

を実行すると、さっきのFatJARが生成された後に、Native Imageのビルドが始まる。で、2種類の警告が出る。

1つ目は sun.misc.Unsafe によるUnsafeなメモリアクセス。 Unsafe を使ってたとしても一般的なパターンで使ってたら native-image がいい感じで対応してくれるんだけど、対応しきれないやつが今回の場合は3つある(最初に書いた参照記事によると、94個はいい感じに対応できてて、3個だけ対応しきれないってことみたい)。

↓適当に改行を入れといた。メッセージが丁寧だね。

Warning: RecomputeFieldValue.ArrayIndexScale automatic substitution failed.
The automatic substitution registration was attempted because a call to sun.misc.Unsafe.arrayIndexScale(Class) was detected
in the static initializer of io.netty.util.internal.PlatformDependent0.
Detailed failure reason(s): The field java.lang.Long.value, where the value produced by the array index scale computation is stored, is not static.                                                                                                                                                                                  

Warning: RecomputeFieldValue.FieldOffset automatic substitution failed.
The automatic substitution registration was attempted because a call to sun.misc.Unsafe.objectFieldOffset(Field) was detected
in the static initializer of io.netty.util.internal.PlatformDependent0.
Add a RecomputeFieldValue.FieldOffset manual substitution for io.netty.util.internal.PlatformDependent0.ADDRESS_FIELD_OFFSET.
Detailed failure reason(s): The argument of Unsafe.objectFieldOffset(Field) is not a constant field.                                                                                                     

Warning: RecomputeFieldValue.ArrayIndexScale automatic substitution failed.
The automatic substitution registration was attempted because a call to sun.misc.Unsafe.arrayIndexScale(Class) was detected
in the static initializer of io.netty.util.internal.shaded.org.jctools.util.UnsafeRefArrayAccess.
Detailed failure reason(s): Could not determine the field where the value produced by the call to sun.misc.Unsafe.arrayIndexScale(Class) for the array index scale computation is stored.
The call is not directly followed by a field store or by a sign extend node followed directly by a field store.  

それともう1つはLogger周り。ロガーはよく「実行時にあるやつを使う」みたいな実装がしてあって、Nettyだと InternalLoggerFactory がそういう実装になってる。 けど、Substrate VM(Native Imageで使われるVM実装)で動かすときはビルド時にクラスが存在しないといけない。だからこんな感じの警告(エラー)がでる。

Warning: Abort stand-alone image build. unsupported features in 5 methods                                                                                                                              
Detailed message:                                                                                                                                                                                      
Error: Class initialization failed: io.netty.util.internal.logging.Log4JLogger                                                                                                                         
Original exception that caused the problem: java.lang.NoClassDefFoundError: org/apache/log4j/Priority 

Error: com.oracle.graal.pointsto.constraints.UnresolvedElementException: Discovered unresolved type during parsing: io.netty.util.internal.logging.Log4J2Logger.
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.
Trace:
        at parsing io.netty.util.internal.logging.Log4J2LoggerFactory.newInstance(Log4J2LoggerFactory.java:33)
Call path from entry point to io.netty.util.internal.logging.Log4J2LoggerFactory.newInstance(String):
        at io.netty.util.internal.logging.Log4J2LoggerFactory.newInstance(Log4J2LoggerFactory.java:33)
        at io.netty.util.internal.logging.InternalLoggerFactory.getInstance(InternalLoggerFactory.java:93)

...

## v3 Unsafe 対応

Unsafe の対応として、Substrate VMのSubstitution(以下、代用)機能を使うので svm への依存を追加。

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

そして、代用機能のためのクラスを作成。

package com.example.demo.substitution;

import com.oracle.svm.core.annotate.Alias;
import com.oracle.svm.core.annotate.RecomputeFieldValue;
import com.oracle.svm.core.annotate.TargetClass;

import static com.oracle.svm.core.annotate.RecomputeFieldValue.Kind;

@TargetClass(className = "io.netty.util.internal.PlatformDependent0")
final class Target_io_netty_util_internal_PlatformDependent0 {
  @Alias
  @RecomputeFieldValue(kind = Kind.FieldOffset, declClassName = "java.nio.Buffer", name = "address")
  private static long ADDRESS_FIELD_OFFSET;
}

@TargetClass(className = "io.netty.util.internal.CleanerJava6")
final class Target_io_netty_util_internal_CleanerJava6 {
  @Alias
  @RecomputeFieldValue(kind = Kind.FieldOffset, declClassName = "java.nio.DirectByteBuffer", name = "cleaner")
  private static long CLEANER_FIELD_OFFSET;
}

@TargetClass(className = "io.netty.util.internal.shaded.org.jctools.util.UnsafeRefArrayAccess")
final class Target_io_netty_util_internal_shaded_org_jctools_util_UnsafeRefArrayAccess {
  @Alias
  @RecomputeFieldValue(kind = Kind.ArrayIndexShift, declClass = Object[].class)
  public static int REF_ELEMENT_SHIFT;
}

public class NettySubstitution {
}

これは元記事のサンプルコードをコピペしただけで、よく分かってない。自分で書けと言われてもまだ書けない。

ロガーの問題はまだ解決してないけど、とりあえずこの時点でビルドしてみるとどうなってるかな?

❯ ./mvnw clean package

お。 Unsafe の警告が消えて、ロガーのエラーだけになってた。よし。

## v4 ロガー対応

さっきの代用機能のためのクラスに下記のクラスを追記

@TargetClass(io.netty.util.internal.logging.InternalLoggerFactory.class)
final class Target_io_netty_util_internal_logging_InternalLoggerFactory {
  @Substitute
  private static InternalLoggerFactory newDefaultFactory(String name) {
    return JdkLoggerFactory.INSTANCE;
  }
}

これは InternalLoggerFactory クラスの newDefaultFactory メソッドが JdkLoggerFactoryインスタンスを返すようにしてるってことなんだろうな。

元々は実行時にクラスの有無を見て、Log4JだとかLog4J2だとかのFactoryを返す実装になってるところを、存在するやつを固定で返すようにしてるってことか。ふむふむ。

さて、これでビルド通るかな?

❯ ./mvnw clean package

お、警告も、フォールバック的なメッセージもなく、ビルドに成功した。 target ディレクトリーの下に netty-example-http って名前(pomで指定した名前)で実行可能ファイルが生成されてる(あ、いや、実は今までも生成されてたんだけど)。実行してみる。

❯ ./target/netty-example-http
Exception in thread "main" java.lang.IllegalArgumentException: Class NioServerSocketChannel does not have a public non-arg constructor
        at io.netty.channel.ReflectiveChannelFactory.<init>(ReflectiveChannelFactory.java:36)
        at io.netty.bootstrap.AbstractBootstrap.channel(AbstractBootstrap.java:106)
        at com.example.demo.helloworld.HttpHelloWorldServer.main(HttpHelloWorldServer.java:56)
Caused by: java.lang.NoSuchMethodException: io.netty.channel.socket.nio.NioServerSocketChannel.<init>
        at java.lang.Class.getConstructor0(DynamicHub.java:3082)
        at java.lang.Class.getConstructor(DynamicHub.java:1825)
        at io.netty.channel.ReflectiveChannelFactory.<init>(ReflectiveChannelFactory.java:34)
        ... 2 more

(´・ω・`)しょぼーん

## v5 リフレクション対応

原因は HttpHelloWorldServer のこの部分

            b.group(bossGroup, workerGroup)
                .channel(NioServerSocketChannel.class)
                .handler(new LoggingHandler(LogLevel.INFO))
                .childHandler(new HttpHelloWorldServerInitializer(sslCtx));

channelのところで NioServerSocketChannel.class を渡して、このコンストラクターをリフレクションで呼び出してる。

ので、それをSubstrate VMに教えてあげる必要があるらしい。プロジェクト直下に netty_reflection_config.json という名前でこんなファイルを置いて

[
  {
    "name": "io.netty.channel.socket.nio.NioServerSocketChannel",
    "methods": [
      { "name": "<init>", "parameterTypes": [] }
    ]
  }
]

それを使うってことを native-image のパラメーターで渡す必要があるので、pomに書き加えておく。

        <configuration>
          <buildArgs>
            -H:ReflectionConfigurationFiles=${project.basedir}/netty_reflection_config.json
          </buildArgs>
          <imageName>netty-example-http</imageName>
        </configuration>

でビルドしたら・・・別の警告でた。

## v6 遅延初期化対応

Substrate VMはビルド時にクラスの初期化までしてるんだけど、それができないクラスがあるよってことみたい。で、その場合は --delay-class-initialization-to-runtime=<class-name> をつけたら良いよって書いてる。

Warning: Abort stand-alone image build. 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.

Detailed message:
Trace:  object io.netty.buffer.UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeNoCleanerDirectByteBuf
        object io.netty.buffer.UnreleasableByteBuf
        method io.netty.handler.codec.http.HttpObjectEncoder.encodeChunkedContent(ChannelHandlerContext, Object, long, List)
Call path from entry point to io.netty.handler.codec.http.HttpObjectEncoder.encodeChunkedContent(ChannelHandlerContext, Object, long, List):
...

ということで、pomのbuildArgsにそのパラメーターも追加する。

        <configuration>
          <buildArgs>
            -H:ReflectionConfigurationFiles=${project.basedir}/netty_reflection_config.json
            --delay-class-initialization-to-runtime=io.netty.handler.codec.http.HttpObjectEncoder
          </buildArgs>
          <imageName>netty-example-http</imageName>
        </configuration>

で、ビルドしたら、成功したー(∩´∀`)∩ワーイ

からの、実行も成功したー(∩´∀`)∩ワーイ

f:id:bufferings:20190416001726p:plain:w300

じゃ、速さを比べてみるかなー。

## v7 速度比較

のために「起動できたよ!」のメッセージ直後にexitするようにする。

            System.err.println("Open your web browser and navigate to " +
                (SSL? "https" : "http") + "://127.0.0.1:" + PORT + '/');
            System.exit(0);

で、ビルドしなおして

❯ ./mvnw clean package

まずはJARで実行

❯ time java -jar target/netty-native-demo-1.0-SNAPSHOT-jar-with-dependencies.jar
Apr 16, 2019 12:22:12 AM io.netty.handler.logging.LoggingHandler channelRegistered
INFO: [id: 0xfdacf35e] REGISTERED
Apr 16, 2019 12:22:12 AM io.netty.handler.logging.LoggingHandler bind
INFO: [id: 0xfdacf35e] BIND: 0.0.0.0/0.0.0.0:8080
Open your web browser and navigate to http://127.0.0.1:8080/
Apr 16, 2019 12:22:12 AM io.netty.handler.logging.LoggingHandler channelActive
INFO: [id: 0xfdacf35e, L:/0:0:0:0:0:0:0:0:8080] ACTIVE
java -jar target/netty-native-demo-1.0-SNAPSHOT-jar-with-dependencies.jar  0.45s user 0.08s system 88% cpu 0.592 total

❯ /usr/bin/time -f "\nmaxRSS\t%MkB" java -jar target/netty-native-demo-1.0-SNAPSHOT-jar-with-dependencies.jar
Apr 16, 2019 12:22:41 AM io.netty.handler.logging.LoggingHandler channelRegistered
INFO: [id: 0x6656e56c] REGISTERED
Apr 16, 2019 12:22:41 AM io.netty.handler.logging.LoggingHandler bind
INFO: [id: 0x6656e56c] BIND: 0.0.0.0/0.0.0.0:8080
Open your web browser and navigate to http://127.0.0.1:8080/
Apr 16, 2019 12:22:41 AM io.netty.handler.logging.LoggingHandler channelActive
INFO: [id: 0x6656e56c, L:/0:0:0:0:0:0:0:0:8080] ACTIVE

maxRSS  105008kB

592msと105MBだね。何度か実行してみてもだいたいこのぐらいだった。

最後に、Native Imageを実行。

❯ time target/netty-example-http
Open your web browser and navigate to http://127.0.0.1:8080/
target/netty-example-http  0.00s user 0.01s system 89% cpu 0.011 total

~/workspace/micronaut/netty-native-demo master*
❯ /usr/bin/time -f "\nmaxRSS\t%MkB" target/netty-example-http
Open your web browser and navigate to http://127.0.0.1:8080/

maxRSS  8700kB

11msと8.7MBだね。何度か実行してみてもだいたいこのぐらいだった。

## 満足した

  • ちょっとだけGraalVMのNative Image周りのことに入門できた
  • 分からないことがいっぱいあった
  • 速かった

面白かったー!

JibでMicronautのアプリをビルドしてGCRにpush

昨日MicronautとDockerを触りながら「そういえばJibってのがあったけど触ってなかったなぁ」と思ったので触ってみた。感想は「気の利くやつだな」。

## Jib?

Googleの提供してるコンテナ化ツール。

github.com

去年の夏頃発表されたんだっけな。

cloud.google.com

通常、Javaのアプリをコンテナ化するときって、例えばSpringBootとかMicronautだと

  1. GradleとかMavenでビルドしてFatJAR(依存ライブラリー全部入りのJARファイル)を作る
  2. そのJARファイルとDockerfileを使ってコンテナをビルドする
  3. レジストリーにpushする

という手順になるのだけど、Jibを使うとDockerfileを書かなくても空気を読んでGradleやMavenでコンテナのビルドやpushまでコマンドひとつでやってくれる。*1

それと、Dockerデーモンがなくてもビルドできるみたい。ローカルだとDockerは入れておくだろうけど、CIサーバーとかだとDockerなくても大丈夫ってのは良さそう。

## MicronautのアプリをGCRにpushしてみる

この辺を読みながらやってみた。

https://github.com/GoogleContainerTools/jib/tree/master/jib-gradle-plugin

### build.gradle

まずは、Micronautのプロジェクトを作るところから始めてみた*2

❯ mn create-app hello-jib --features spock                                                                                                                                                             
| Generating Java project...                                                                                                                                                                           
| Application created at /home/bufferings/workspace/micronaut/hello-jib    

で、build.gradleにjibの設定を入れる。プラグインの部分と、生成するイメージの名前。

plugins {
    id "io.spring.dependency-management" version "1.0.6.RELEASE"
    id "com.github.johnrengelman.shadow" version "4.0.2"
    id "application"
    id "groovy"
    id "java"
    id "net.ltgt.apt-eclipse" version "0.21"
    id "net.ltgt.apt-idea" version "0.21"

    // これ
    id 'com.google.cloud.tools.jib' version '1.0.2'
}
// これ
jib.to.image = 'asia.gcr.io/bufferings/my-app'

Google Container Registryにpushしてみようかなと思ったので、イメージ名はそうしといた。

### GCR用のクレデンシャルを用意

Make sure you have the docker-credential-gcr command line tool. Jib automatically uses docker-credential-gcr for obtaining credentials. See Authentication Methods for other ways of authenticating.

って書いてあるので、設定しといた。

❯ docker-credential-gcr configure-docker                                                                                                                                                               
/home/bufferings/.docker/config.json configured to use this credential helper for GCR registries  

### 実行!

のためのGradleタスクはこの2つ

  • ./gradlew jib → ビルドしてレジストリーにpushする
  • ./graldew jibDockerBuild → ローカルのDockerデーモンに対してイメージのビルドをする

とりあえず、前者の ./gradlew jib を実行してみたら、エラーになった。

❯ ./gradlew jib                                                                                                                                                                                        
                                                                                                                                                                                                       
> Task :compileJava                                                                                                                                                                                    
warning: Supported source version 'RELEASE_11' from annotation processor 'org.gradle.api.internal.tasks.compile.processing.TimeTrackingProcessor' less than -source '12'                               
warning: Supported source version 'RELEASE_11' from annotation processor 'org.gradle.api.internal.tasks.compile.processing.TimeTrackingProcessor' less than -source '12'                               
warning: Supported source version 'RELEASE_11' from annotation processor 'org.gradle.api.internal.tasks.compile.processing.TimeTrackingProcessor' less than -source '12'                               
3 warnings                                                                                                                                                                                             
                                                                                                                                                                                                       
> Task :jib FAILED                                                                                                                                                                                     
                                                                                                                                                                                                       
FAILURE: Build failed with an exception.                                                                                                                                                               
                                                                                                                                                                                                       
* What went wrong:                                                                                                                                                                                     
Execution failed for task ':jib'.                                                                                                                                                                      
> Your project is using Java 12 but the base image is for Java 11, perhaps you should configure a Java 12-compatible base image using the 'jib.from.image' parameter, or set targetCompatibility = 11 o
r below in your build configuration                                                                                                                                                                    
                                                                                                                                                                                                       
* Try:                                                                                                                                                                                                 
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.

* Get more help at https://help.gradle.org

BUILD FAILED in 4s
3 actionable tasks: 3 executed

Java 12使ってたけど、Base Imageは11じゃよ。って怒られた。JibはデフォルトでDistroless/javaを使うらしいんだけど、それがJava 11ってことか。そっか。

❯ sdk use java 11.0.2-open

Using java version 11.0.2-open in this shell.

これでいけるかな。

❯ ./gradlew jib
Starting a Gradle Daemon, 1 incompatible and 2 stopped Daemons could not be reused, use --status for details

Containerizing application to asia.gcr.io/bufferings/my-app...

Container entrypoint set to [java, -cp, /app/resources:/app/classes:/app/libs/*, hello.jib.Application]

Built and pushed image as asia.gcr.io/bufferings/my-app
Executing tasks:
[==============================] 100.0% complete


BUILD SUCCESSFUL in 2m 4s
3 actionable tasks: 2 executed, 1 up-to-date

レジストリー見てみたら、ちゃんとpushされてるー。(∩´∀`)∩ワーイ

f:id:bufferings:20190414152207p:plain:w300

### pullしてみるか

じゃ、pullして実行できるの確認しとこかな?と思ったけど、そのまえに、そもそも、実はビルド時にローカルにも登録されてたりしないのかな?と思って見てみた。

❯ docker images | grep gcr
asia.gcr.io/bufferings/quickstart-image   tag1                       e1ddd7948a1c        8 months ago        1.16MB
gcr.io/distroless/java                    latest                     d53055d7a4da        49 years ago        118MB

ないね。じゃ、やってみよ。

❯ docker run --rm -p 8080:8080 asia.gcr.io/bufferings/my-app
Unable to find image 'asia.gcr.io/bufferings/my-app:latest' locally
latest: Pulling from bufferings/my-app
f6045256ec3f: Pull complete
5f5edd681dcb: Pull complete
3e010093287c: Pull complete
e6476c6c5a02: Pull complete
4988a9948f57: Pull complete
ffc771f05eaa: Pull complete
45a6b543af72: Pull complete
Digest: sha256:eeb9e44406fb12dc4ed318c2ca56d36db065a8dbbae32d435526a50e65f33d39
Status: Downloaded newer image for asia.gcr.io/bufferings/my-app:latest
01:30:48.626 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 2115ms. Server Running: http://a1151605f84c:8080
^C01:32:10.753 [Thread-2] INFO  io.micronaut.runtime.Micronaut - Embedded Application shutting down

OK。

## 中身が気になる

中身が気になるなー。どうなってるんだろうなー?

### Entrypoint

まずはEntrypointを見てみた。さっきビルドしたときにも出てきたな。

❯ docker inspect asia.gcr.io/bufferings/my-app | jq '.[] | .Config.Entrypoint'
[
  "java",
  "-cp",
  "/app/resources:/app/classes:/app/libs/*",
  "hello.jib.Application"
]

ふーん。

### 中に入ってみる

Distrolessにはshも入ってないから、デバッグ用に中に入って色々見てみたりしたいならdebugタグを使ってねってFAQに書いてある。

https://github.com/GoogleContainerTools/jib/blob/master/docs/faq.md#where-is-bash

If you would like to include a shell for debugging, set the base image to gcr.io/distroless/java:debug instead. The shell will be located at /busybox/sh. Note that :debug images are not recommended for production use.

おすおす。こんな感じに書き換えてみた。

    id 'com.google.cloud.tools.jib' version '1.0.2'
}
jib {
    from {
        // これを足した
        image = 'gcr.io/distroless/java:debug'
    }
    to {
        image = 'asia.gcr.io/bufferings/my-app'
    }
}

で、こんどはpushしなくてもいいからローカル用に jibDockerBuild でビルド。したら、エラー。

❯ ./gradlew jibDockerBuild                                                                                                                                                                             
> Task :jibDockerBuild FAILED                                                                                                                                                                          
                                                                                                                                                                                                       
FAILURE: Build failed with an exception.                                                                                                                                                               
                                                                                                                                                                                                       
* What went wrong:                                                                                                                                                                                     
Execution failed for task ':jibDockerBuild'.                                                                                                                                                           
> Your project is using Java 11 but the base image is for Java 8, perhaps you should configure a Java 11-compatible base image using the 'jib.from.image' parameter, or set targetCompatibility = 8 or 
below in your build configuration                                                                                                                                                                      
                                                                                                                                                                                                       
* Try:                                                                                                                                                                                                 
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.                                                   
                                                                                                                                                                                                       
* Get more help at https://help.gradle.org                                                                                                                                                             

BUILD FAILED in 1s
3 actionable tasks: 1 executed, 2 up-to-date

あら。debugタグはJava 8なのか。

❯ sdk use java 8.0.202.hs-adpt

Using java version 8.0.202.hs-adpt in this shell.

8に変えて、ビルド。

❯ ./gradlew jibDockerBuild
Starting a Gradle Daemon, 2 incompatible Daemons could not be reused, use --status for details

Containerizing application to Docker daemon as asia.gcr.io/bufferings/my-app...

Container entrypoint set to [java, -cp, /app/resources:/app/classes:/app/libs/*, hello.jib.Application]

Built image to Docker daemon as asia.gcr.io/bufferings/my-app
Executing tasks:
[==============================] 100.0% complete


BUILD SUCCESSFUL in 35s
3 actionable tasks: 2 executed, 1 up-to-date

おっけー。

じゃ、中に入ってみてみるかー。

❯ docker run --rm -ti --entrypoint /busybox/sh asia.gcr.io/bufferings/my-app
/ # find /app
/app
/app/libs
/app/libs/netty-codec-4.1.30.Final.jar
/app/libs/netty-transport-4.1.30.Final.jar
/app/libs/micronaut-http-client-1.1.0.jar
/app/libs/jackson-core-2.9.8.jar
/app/libs/micronaut-core-1.1.0.jar
/app/libs/netty-handler-4.1.30.Final.jar
/app/libs/netty-buffer-4.1.30.Final.jar
/app/libs/micronaut-http-1.1.0.jar
/app/libs/micronaut-aop-1.1.0.jar
/app/libs/jackson-databind-2.9.8.jar
/app/libs/netty-codec-socks-4.1.30.Final.jar
/app/libs/netty-handler-proxy-4.1.30.Final.jar
/app/libs/validation-api-2.0.1.Final.jar
/app/libs/javax.annotation-api-1.3.2.jar
/app/libs/jackson-annotations-2.9.8.jar
/app/libs/netty-resolver-4.1.30.Final.jar
/app/libs/jsr305-3.0.2.jar
/app/libs/slf4j-api-1.7.25.jar
/app/libs/micronaut-validation-1.1.0.jar
/app/libs/netty-common-4.1.30.Final.jar
/app/libs/micronaut-http-netty-1.1.0.jar
/app/libs/javax.inject-1.jar
/app/libs/micronaut-http-server-netty-1.1.0.jar
/app/libs/micronaut-runtime-1.1.0.jar
/app/libs/logback-core-1.2.3.jar
/app/libs/jackson-datatype-jsr310-2.9.8.jar
/app/libs/reactive-streams-1.0.2.jar
/app/libs/micronaut-buffer-netty-1.1.0.jar
/app/libs/micronaut-websocket-1.1.0.jar
/app/libs/logback-classic-1.2.3.jar
/app/libs/jackson-datatype-jdk8-2.9.8.jar
/app/libs/rxjava-2.2.6.jar
/app/libs/snakeyaml-1.23.jar
/app/libs/netty-codec-http-4.1.30.Final.jar
/app/libs/micronaut-inject-1.1.0.jar
/app/libs/micronaut-http-server-1.1.0.jar
/app/libs/micronaut-router-1.1.0.jar
/app/classes
/app/classes/hello
/app/classes/hello/jib
/app/classes/hello/jib/Application.class
/app/resources
/app/resources/application.yml
/app/resources/logback.xml

ふーん。libsに依存ライブラリーが入って、classesにクラスファイルが、resourcesにはリソースファイルが入るのか。なるほどなー。気の利くやつだな。

## その他

細かく色々設定できるみたい。

https://github.com/GoogleContainerTools/jib/tree/master/jib-gradle-plugin

環境変数とかは普通に外から渡せば良さそう。

https://github.com/GoogleContainerTools/jib/blob/master/docs/faq.md#how-do-i-set-parameters-for-my-image-at-runtime

おしまい。これくらいメモ残しておけば、また使いたくなったときに思い出せるかな。

*1:だからといって何も中身を知らなくていいという訳ではないよなー

*2:例によってSpockは好みでつけてるだけ

MicronautのGraalVM Native ImageはDocker使うより普通にビルドする方が良さそう

## 2019-04-17 追記

ブログ書くだけなのもあれだなと思って、こんな感じにしたらどうかな?ってIssueを書いた。

github.com

そしたら「1.1.0でnative image化が十分シンプルになったから、ビルド用のシェルスクリプトを削除したんだ」って教えてもらった。まぁ、確かにそうね。

なので、普通にビルドするってことでいいし、Dockerでやりたい場合でも、僕がこのブログ記事で書いてるようなので良さそう。かな。

それと、マルチステージビルドをしっかりやってるサンプルも教えてもらった。ふむふむー。

https://github.com/micronaut-projects/micronaut-gcp/blob/master/examples/hello-world-cloud-run-graal/Dockerfile

追記はここまでです!

## Native Image

MicronautにはGraalVMのサポートが入っていて、GraalVMを使ってMicronautアプリのNative Imageを生成することができる。

ドキュメントはこの辺: https://docs.micronaut.io/1.1.0/guide/index.html#graal

Micronautのバージョンは1.1.0。

## graal-native-image feature

Micronautのcreate-appをするときに、graal-native-imageをfeatureオプションにつける。

❯ mn create-app hello-graal --features graal-native-image
| Generating Java project...
| Application created at /home/bufferings/workspace/micronaut/hello-graal

と、プロジェクトの生成と同時に、GraalVMでNative Imageをビルドするときのためのファイルがいくつか生成される。

## Dockerでのビルドがいまいち?

その作成されるファイルの中にDockerfileとdocker-build.shがあって「docker-build.shを実行すればDockerのマルチステージビルドで、Native Imageがビルドされるよ!」ってドキュメントには書いてある。

んだけど、いまいちかなー。と思ったのでメモを書いとく。

## docker-build.sh

は、こうなってる

#!/bin/sh
docker build . -t hello-graal
echo
echo
echo "To run the docker container execute:"
echo "    $ docker run --network host hello-graal"

つまり、Dockerfileでビルドしてるよってだけね。じゃあDockerfileを見てみよう。

## Dockerfile

こうなってる。マルチステージビルドを使ってる。

FROM oracle/graalvm-ce:1.0.0-rc15 as graalvm
COPY . /home/app/hello-graal
WORKDIR /home/app/hello-graal
RUN native-image --no-server -cp build/libs/hello-graal-*.jar

FROM frolvlad/alpine-glibc
EXPOSE 8080
COPY --from=graalvm /home/app/hello-graal .
ENTRYPOINT ["./hello-graal"]

### まず前半の4行は

  1. graalvmのイメージを使って
  2. カレントディレクトリーを /home/app/hello-graal にコピーして
  3. そこをWORKDIRにして
  4. native-image コマンドを実行してNative Imageをビルドしてる

んー。これだと、GradleによるJARファイルのビルドを事前に実行しておかないといけない・・・よね。もしDockerだけで完結させたいならこうかな?

RUN ./gradlew assemble \
 && native-image --no-server -cp build/libs/hello-graal-*.jar

ただ、これだとビルドのたびにGradleをダウンロードしてきてビルドするから時間がかかるよね。

### 次に後半の4行は

この部分だけ

COPY --from=graalvm /home/app/hello-graal .

これ、前半でビルドしたイメージから、必要なファイルをコピーしてきてるんだけど、ソースコードまで要らんくない?しかもルートディレクトリーにガッと突っ込んでる。そうじゃなくて、

COPY --from=graalvm /home/app/hello-graal/hello-graal .

Native Imageだけ取ってこれたらいいかなと。あと、これは僕の好みだけなんだけど、ルート直下に置くよりは、なんかディレクトリーいっこ切っておきたいな。

FROM frolvlad/alpine-glibc
EXPOSE 8080
WORKDIR /app
COPY --from=graalvm /home/app/hello-graal/hello-graal .
ENTRYPOINT ["/app/hello-graal"]

### ということで

こうなった

FROM oracle/graalvm-ce:1.0.0-rc15 as graalvm
COPY . /home/app/hello-graal
WORKDIR /home/app/hello-graal
RUN ./gradlew assemble \
 && native-image --no-server -cp build/libs/hello-graal-*.jar

FROM frolvlad/alpine-glibc
EXPOSE 8080
WORKDIR /app
COPY --from=graalvm /home/app/hello-graal/hello-graal .
ENTRYPOINT ["/app/hello-graal"]

### 実行するには

host networkモードを使うんだ!ってドキュメントに書いてある。

$ docker run --network=host hello-world

僕のマシンはUbuntuだからいいんだけど、Docker for Macだと現時点ではサポートされてないよね。。。

https://docs.docker.com/network/host/

素直にpublishオプションを使っていいんじゃないかな。

$ docker run -p 8080:8080 hello-world

## そこまでしてDocker使わなくても・・・

いいかなと思う。さっきも書いたけどDocker使うとgradlewの実行遅いし。だから、素直にホストマシンにsdkmanでGraalVM使ってやるといいのかなと。思うのであった。

❯ sdk use java 1.0.0-rc-15-grl

Using java version 1.0.0-rc-15-grl in this shell.

❯ ./gradlew assemble
Starting a Gradle Daemon, 2 incompatible Daemons could not be reused, use --status for details

BUILD SUCCESSFUL in 11s
10 actionable tasks: 7 executed, 3 up-to-date
<-------------> 0% WAITING
> IDLE

❯ native-image --no-server -cp build/libs/hello-graal-*.jar
[hello-graal:31214]    classlist:   6,843.33 ms
[hello-graal:31214]        (cap):   1,346.60 ms
[hello-graal:31214]        setup:   3,216.68 ms
[hello-graal:31214]   (typeflow):  52,258.57 ms
[hello-graal:31214]    (objects):  38,803.47 ms
[hello-graal:31214]   (features):   2,844.33 ms
[hello-graal:31214]     analysis:  97,553.99 ms
[hello-graal:31214]     universe:   2,264.42 ms
[hello-graal:31214]      (parse):   7,780.03 ms
[hello-graal:31214]     (inline):  10,103.81 ms
[hello-graal:31214]    (compile):  85,401.85 ms
[hello-graal:31214]      compile: 106,246.90 ms
[hello-graal:31214]        image:   5,274.68 ms
[hello-graal:31214]        write:   1,565.77 ms
[hello-graal:31214]      [total]: 223,483.88 ms

❯ ./hello-graal
23:31:52.391 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 169ms. Server Running: http://localhost:8080
^C

❯ ./hello-graal
23:32:00.775 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 64ms. Server Running: http://localhost:8080

お。何も設定してないから速いってのもあるけど200msとか100msとかで起動するのは良いなー。

ということで、もうちょっとMicronautとGraalVMのNative Image周りを勉強してみようと思う。

MicronautでStaticコンテンツをハンドリング

今日使ってみたのは最新のバージョン1.1.0

❯ mn --version
| Micronaut Version: 1.1.0
| JVM Version: 12

## リファレンス

Staticコンテンツのハンドリングはドキュメントのこの部分

https://docs.micronaut.io/1.1.0/guide/index.html#staticResources

Javadocはこれだね

https://docs.micronaut.io/1.1.0/api/io/micronaut/web/router/resource/StaticResourceConfiguration.html

## 準備

プロジェクトを作って(spockは趣味)

❯ mn create-app hello-mn --features spock
| Generating Java project...
| Application created at /home/bufferings/workspace/micronaut/hello-mn

IDEAでImportして、Annotation Processorを有効化。

前回はIDEAからはうまく動かなかったけど、今回は普通に動いたや。

## こんな感じにした

f:id:bufferings:20190413120710p:plain:w600

application.yml に設定を入れて

micronaut:
  router:
    static-resources:
      default:
        paths:
          - "classpath:static"

classpath:staticにしたから、resourcesの下にstaticディレクトリーを作って、そこにsample.txtを置いた。

で、ブラウザで見てみたら見えた。

f:id:bufferings:20190413121048p:plain:w300

おわりー。