SpringBoot3.0でサポートされた ネイティブイメージ起動してみる

エキサイト株式会社エンジニア佐々木です。

Advent Calendarの季節が今年もやってきました。
昨年同様、エキサイトホールディングス Advent Calendarで毎日投稿される予定です。 様々な話題が投稿されるのでぜひ閲覧してみてください!

qiita.com

前回、

ネイティブJavaに向けてウォーミングアップする - エキサイト TechBlog. で、GraalVMを使ったネイティブアプリケーションの実行方法について書かせていただきました。 今回は、SpringBoot3.0でネイティブイメージがサポートされたので、こちら動かしてみます。

前提

Java

openjdk 17.0.5 2022-10-18 LTS
OpenJDK Runtime Environment GraalVM 22.3.0 (build 17.0.5+8-LTS)
OpenJDK 64-Bit Server VM GraalVM 22.3.0 (build 17.0.5+8-LTS, mixed mode, sharing)

Gradle

------------------------------------------------------------
Gradle 7.5.1
------------------------------------------------------------

Build time:   2022-08-05 21:17:56 UTC
Revision:     d1daa0cbf1a0103000b71484e1dbfe096e095918

Kotlin:       1.6.21
Groovy:       3.0.10
Ant:          Apache Ant(TM) version 1.10.11 compiled on July 10 2021
JVM:          17.0.5 (BellSoft 17.0.5+8-LTS)
OS:           Mac OS X 12.5 aarch64

SpringBoot

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.0.0)

コード

build.gradleは下記のようなコードになります。

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.0.0'
    id 'io.spring.dependency-management' version '1.1.0'
    id 'org.graalvm.buildtools.native' version '0.9.18'
}

group = 'jp.co.excite'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    developmentOnly 'org.springframework.boot:spring-boot-devtools'
    annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
    useJUnitPlatform()
}

今回は、決まった値を返すだけのRestAPIを作るだけなので、依存関係は少なめになります。

Javaのコードは下記のようになります。

SpringBootの起動クラスは下記のようなコードになります。

package jp.co.excite.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication(scanBasePackages = "jp.co.excite.example")
public class ExampleApplication {

    public static void main(String[] args) {
        SpringApplication.run(ExampleApplication.class, args);
    }

}

続いて、リクエストを処理するControllerクラスのコードは下記となります。

package jp.co.excite.example.controller;

import lombok.Data;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class SampleController {

    @GetMapping("sample")
    public Item index(){
        Item item = new Item();
        item.setId(1);
        item.setMessage("Hello,World");
        return item;
    }

    @Data
    private static class Item {
        private int id;
        private String message;
    }
}

ただ、文字列を返すのも味気ないので、オブジェクトを作ってJSONを返します。

ビルド

では、ネイティブコンパイルをしてみます。

$ ./gradlew nativeCompile

 time ./gradlew nativeCompile

> Task :processAot

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.0.0)

2022-12-12T23:13:58.464+09:00  INFO 92193 --- [           main] j.co.excite.example.ExampleApplication   : Starting ExampleApplication using Java 17.0.5 with PID 92193 

.....
.....

real    0m58.269s
user    0m0.853s
sys     0m0.124s

1エンドポイントのみですが、コンパイル時間は58秒になります。一度SpringBootが起動しているのがログから確認できます。SpringBootは起動時にDIコンテナにインスタンスを登録するので、事前にそれを行っているようです。

起動

では、起動してみます。

$ ./build/native/nativeCompile/example 

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.0.0)

2022-12-12T23:30:09.716+09:00  INFO 93508 --- [           main] j.co.excite.example.ExampleApplication   : Starting AOT-processed ExampleApplication using Java 17.0.5 with PID 93508 )
2022-12-12T23:30:09.716+09:00  INFO 93508 --- [           main] j.co.excite.example.ExampleApplication   : No active profile set, falling back to 1 default profile: "default"
2022-12-12T23:30:09.725+09:00  INFO 93508 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2022-12-12T23:30:09.725+09:00  INFO 93508 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2022-12-12T23:30:09.726+09:00  INFO 93508 --- [           main] o.apache.catalina.core.StandardEngine    : Starting Servlet engine: [Apache Tomcat/10.1.1]
2022-12-12T23:30:09.731+09:00  INFO 93508 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2022-12-12T23:30:09.731+09:00  INFO 93508 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 15 ms
2022-12-12T23:30:09.741+09:00  INFO 93508 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2022-12-12T23:30:09.742+09:00  INFO 93508 --- [           main] j.co.excite.example.ExampleApplication   : Started ExampleApplication in 0.03 seconds (process running for 0.038)


// リクエストしてみます
$ curl http://localhost:8080/sample
{"id":1,"message":"Hello,World"}

起動時間は、0.038秒(38ミリ秒)になります。通常のJavaアプリケーションとして実行すると0.785秒(785ミリ秒)となり、20倍近く起動が速くなります。Javaの方だとクラス数が多くなると起動時にリクエストが受け付けられるようになるまでの時間が長くなっていきます。クラウドネイティブ環境としては起動速度は大切です。Javaが持っているJITコンパイラ等のパフォーマンス最適化の機能などはネイティブコンパイルでは動作しないので、使い分けが大切です。

ハマりポイント

NativeCompileではコンパイル時に確定していないといけないようで、下記のようなコードだとうまく動きませんでした。

    @GetMapping("sample")
    public Object index(){
        Item item = new Item();
        item.setId(1);
        item.setMessage("Hello,World");
        return item;
    }

// 実行時
$ curl http://localhost:8080/sample
{"timestamp":"2022-12-12T23:54:32.344+00:00","status":406,"error":"Not Acceptable","path":"/sample"}

========

    @GetMapping("sample")
    public Item index(){
        Item item = new Item();
        item.setId(1);
        item.setMessage("Hello,World");
        return item;
    }

// 実行時
$ curl http://localhost:8080/sample
{"id":1,"message":"Hello,World"}

レスポンス型がObject型であった場合には、うまく解釈できずにエラーが返りました。型が確定している場合は、Item型の場合は正常にレスポンスできています。

まとめ

SpringBoot3.0だとおもったより簡単にネイティブイメージを起動することができました。まだ、いくつかのライブラリ等は対応していなかったりするようなので、本番環境投入は先になるかとおもいます。また、コンパイルに時間がかかるのでローカルでの開発時は、通常のJavaアプリケーションで開発しサーバではネイティブコンパイルするといった形になるかと思います。

最後に

エキサイトではフロントエンジニア、バックエンドエンジニア、アプリエンジニアを随時募集しております。長期インターンも歓迎していますので、興味があれば連絡いただければと思います。

カジュアル面談はこちらになります! meety.net

募集職種一覧はこちらになります!(カジュアルからもOK) www.wantedly.com