14
8

GraalVM で Spring Boot アプリケーションをネイティブイメージ化してみた

Last updated at Posted at 2024-05-29

要点

バッチアプリケーションの初期化処理削減のために、GraalVM を利用してバッチアプリケーションをネイティブイメージ化できないかを調査・検討しました。その中で実施した内容を、以下の通り説明します。

  • GraalVM を利用して Spring Boot アプリケーションをネイティブイメージ化した
  • GraalVM で対応していない OSS ライブラリを利用していることによるエラーを調査した
  • ネイティブイメージのパフォーマンスを計測し実行時間やメモリ消費量の削減を確認した
  • GraalVM がサポートしていない OSS ライブラリが多いため、実際のシステム開発での利用はまだ難しいと考察した

背景

BIPROGYでは、Spring Boot ベースのサンプルアプリケーションDresscaを作成し、様々な案件で雛形として使用しています。

とある案件で、Spring Boot アプリケーションのバッチ処理において、アプリケーション起動時に Bean の生成や初期化処理に時間がかかってしまうことが問題として挙げられていました。そこで、この時間を低減するための一つの手段として GraalVM を利用することができないかという案が出ました。

本投稿は、Dresscaで GraalVM 利用してネイティブイメージを実装してみた際の記録をまとめ、実際のシステム開発で利用できるかを考察していきます。

GraalVMとは

GraalVMは、 Java アプリケーションをネイティブなバイナリ実行ファイル(ネイティブイメージ)にコンパイルする機能を持った JDK です。
GraalVM を利用することで、 JVM を介さず直接実行できるネイティブイメージを作成することが可能となり、実行速度の高速化やメモリ使用量の低減を期待できます。

Dressca のフォルダー構成

Dressca のフォルダー構成は以下の通りです。
なお、本記事に関係するフォルダー・ファイルのみを記載しています。

Dressca のフォルダー構成
Dresscaのフォルダー構成
dressca ------------------------------------------------- ルートプロジェクト
├ build.gradle ------------------------------------------ プロジェクト全体で利用する依存関係を設定するファイル
├ dependencies.gradle ----------------------------------- プロジェクトで利用している依存関係のバージョンを管理するファイル
├ application-core -------------------------------------- ビジネスロジックを実装するサブプロジェクト
|  └ src/main/java/com/dressca/applictioncore
|      ├ build.gradle ----------------------------------- application-core層で利用する依存関係を設定するファイル
|      ├ ...
|      └ order
|          ├ ...
|          └ Address.java   ----------------------------- 日本の住所を表現する値オブジェクト(エラー対応で利用)
├ batch ------------------------------------------------- バッチ処理を実装するサブプロジェクト
|  └ src/main/
|      ├ java/com/dressca/batch
|      |   ├ build.gradle ------------------------------- batch層で利用する依存関係を設定するファイル
|      |   ├ BatchApplication.java ---------------------- バッチ処理アプリケーションの起動メインクラス
|      |   └ job ---------------------------------------- バッチジョブを格納するフォルダー   
|      └ resources
|          └ META-INF/native-image
|              └ reflect-config.json -------------------- 動的コンパイルするライブラリを設定するjsonファイル
|
├ infrastructure ---------------------------------------- データベースアクセス関連を実装するサブプロジェクト
|  └ src/main/
|      ├ java/com/dressca/infrastructure
|      |   ├ repository/mybatis
|      |   |   ├ config 
|      |   |   |   ├ MybatisConfig.java ----------------- MyBatis 用の設定クラス
|      |   |   |   └ MybatisNativeConfigration.java ----- MyBatis Spring Native 用の設定クラス(新規作成)
|      |   |   ├ generated ------------------------------ MyBatis Generator で自動生成したクラス
|      |   |   └ mapper
|      |   |       ├ Mapper.java ------------------------ 手動で作成したテーブルアクセス用のクラス
|      |   |     └ Mapper.xml ------------------------- 手動で作成したテーブルアクセス用の SQL が記載されたファイル
|      └ resources
├ web --------------------------------------------------- Web APIを配置するサブプロジェクト
|  └ src/main/java/com/dressca/web
|     ├ build.gradle ------------------------------------ web 層で利用する依存関係を設定するファイル
|     ├ controller -------------------------------------- Web API のコントローラークラスを配置するフォルダー
|     └ controlleradvice
|         └ ExceptionHandlerControllerAdvice.java ------- エラーのハンドリングを行うクラス(ロガーを利用しているクラス)
|
└ system-common --- 共通設定を行うサブプロジェクト
      ├ build.gradle ----------------------------------- system-common 層で利用する依存関係を設定するファイル
      └ src/main/resources
          ├ log4j2.xml --------------------------------- log4j2のログの表示形式を設定するxmlファイル(削除)
          └ logback.xml -------------------------------- logbackのログの表示形式を設定するxmlファイル(新規作成)

上記フォルダー構成からわかる通り、Dressca は Gradle のマルチプロジェクトです。Web 、バッチ、コア機能、共通機能などのサブプロジェクトに分けられることで、それぞれ独立性・開発効率が高いプロジェクト構成となります。マルチプロジェクトを実現するにあたり、ルートプロジェクト、サブプロジェクトの雛形をそれぞれ作成し、組み合わせることでマルチプロジェクトの雛形としています。
また、各プロジェクトは、Spring Initializr の雛形で作成されています。プロジェクトの作成方法についてはこちらを確認してください。

ツール・ライブラリのバージョン

本アプリケーションは Windows 10 環境下で実装しました。

開発環境に関係するツールのバージョン

利用ツールのバージョンは以下の通りです。
なお、既存のDressca がJava 17 を利用していることから、GraalVM もバージョン対応するように設定しました。

項目 バージョン
Visual Studio Code 3.2.4
Visual Studio Build Tools 17.9.6
GraalVM 17.0.9
Gradle 8.2.1

OSS ライブラリバージョン

OSS ライブラリのバージョンは以下の通りです。

項目 バージョン
GraalVM Build Tools 0.9.28
Spring Boot 3.2.4
Spring Batch 5.1.1
MyBatis 3.5.14
MyBatis Spring Native 0.1.0-SNAPSHOT
h2 Database 2.2.224

GraalVM の導入手順

Spring Boot が提供するGraalVM ネイティブイメージ導入手順を参考にしながら、導入を進めていきます。

GraalVM のインストール

GraalVM のダウンロードページから GraalVM をインストールします。その後、JAVA_HOMEPathにインストールした GraalVM を設定します。

JAVA_HOME
C:\Program Files\GraalVM\graalvm-jdk-17.0.9+11.1
Path
C:\Program Files\GraalVM\graalvm-jdk-17.0.9+11.1\bin

以下のコマンドで、正しく設定できていることを確認してください。

cmd
C:\> java --version
java 17.0.9 2023-10-17 LTS
Java(TM) SE Runtime Environment Oracle GraalVM 17.0.9+11.1 (build 17.0.9+11-LTS-jvmci-23.0-b21)
Java HotSpot(TM) 64-Bit Server VM Oracle GraalVM 17.0.9+11.1 (build 17.0.9+11-LTS-jvmci-23.0-b21, mixed mode, sharing)

Visual Studio Toolsのダウンロード

Windows 環境でGraalVMのネイティブイメージを作成するために、Visual Studio のダウンロードページからVisual Studio Tools をダウンロードし、起動します。
「C++ によるデスクトップ開発」のチェックボックスをオンにし、「インストールの詳細」で「Windows 10 SDK」のチェックボックスをオンにしてインストールします。

image.png

build.gradleの設定

まず、サブプロジェクト batchbuild.gradle に以下の依存関係を追加します。
(GraalVMのために新規で追加する依存関係のみを記載しています)

build.gradle
plugins {
  	id 'org.graalvm.buildtools.native' version "${graalvmVersion}"
}

repositories {
	maven { url "https://oss.sonatype.org/content/repositories/snapshots" }
}

dependencies {
	implementation supportDependencies.mybatis_spring_native
}

graalvmNative {
  binaries {
    main {
      javaLauncher = javaToolchains.launcherFor {
        languageVersion = JavaLanguageVersion.of(17)
        vendor = JvmVendorSpec.matching("Oracle Corporation")
      }
      mainClass = 'com.dressca.batch.BatchApplication'
    }
  }
}

次に、ルートプロジェクトの dependencies.gradle にバージョン番号と利用するライブラリの名称を設定します。なお、この dependencies.gradle は、 Gradle のマルチプロジェクト構成をとる Dressca において各プロジェクトのライブラリバージョンを統括的に管理するために作成した独自の設定ファイルです。一般的には、dependencies.gradleの内容は build.gradle に直接設定します。

dependencies.gradle
ext {
    // -- PLUGINS
    graalvmVersion = "0.9.28"

    // -- DEPENDENCIES
    mybatisNativeVersion = '0.1.0-SNAPSHOT'

    supportDependencies = [
        mybatis_spring_native : "org.mybatis.spring.native:mybatis-spring-native-core:$mybatisNativeVersion"
    ]
}

ネイティブコンパイルとexeファイルの実行

以下のコマンドでネイティブイメージを作成します。

nativeCompile
./gradlew clean batch:nativeCompile

コンパイルが完了したら ./batch/build/native/nativeCompileフォルダーにbatch.exeファイルが生成されます。これが実行できれば、作業は完了です。

エラー対応

しかしながら、実行時にいくつかエラーが発生してしまいました...。
エラー内容について、以下のように対応していきます。

Log4j2 が対応していない

既存のDresscaのロギングライブラリは、log4j2を利用していました。
しかし、GraalVM のサポートライブラリ一覧を見ると、ロギングライブラリでlog4j2は対応しておらず、対応しているのは logback + slf4j の形式でした。

このことから、logback + slf4j のロギングライブラリを利用する方針としました。
そこで、ルートプロジェクトおよびサブプロジェクトのbuild.gradleにある以下の記載を削除しました。

build.gradle
// log4j2 を利用するための設定であるため、以下の記載を削除します。
- dependencies {
-     testImplementation supportDependencies.spring_boot_starter_log4j2
- }
- 
- configurations {
- 	all {
- 		exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging'
- 	}
- }

また、ログの出力形式をlogback + slf4jに対応するため、共通設定を行うサブプロジェクトsystem-commonresourcesフォルダーにある、以下の xml ファイルを新規で作成しました。

logback.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>

  <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %logger %-5level %thread %msg%n</pattern>
    </encoder>
  </appender>

  <appender name="application.log.appender" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %thread %msg%n</pattern>
    </encoder>
  </appender>

  <logger name="application.log" level="debug" additivity="false">
    <appender-ref ref="application.log.appender" />
  </logger>

  <root level="info">
    <appender-ref ref="console" />
  </root>

</configuration>

MyBatis Spring Native が正しく動作しない

log4j2のエラーを解消すれば正しく動作するかと思いましたが、そううまくはいきません。
生成されたbatch.exeを実行すると、以下のエラーが発生しました。

batch.exeのエラー内容
実行結果
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.2.4)

2024-04-26 10:50:22.658 com.dressca.batch.BatchApplication INFO  main Starting AOT-processed BatchApplication using Java 17.0.9 with PID 17300 (C:\Maia\maia\samples\web-csr\dressca-backend\batch\build\native\nativeCompile\batch.exe started by user in C:\Maia\maia\samples\web-csr\dressca-backend)
2024-04-26 10:50:22.658 com.dressca.batch.BatchApplication INFO  main No active profile set, falling back to 3 default profiles: "local", "common", "dev"
2024-04-26 10:50:22.660 org.springframework.context.support.GenericApplicationContext WARN  main Exception encountered during context initialization - cancelling refresh attempt: java.lang.ExceptionInInitializerError      
2024-04-26 10:50:22.662 org.springframework.boot.SpringApplication ERROR main Application run failed
java.lang.ExceptionInInitializerError: null
        at org.mybatis.spring.mapper.MapperScannerConfigurer.postProcessBeanDefinitionRegistry(MapperScannerConfigurer.java:363)
        at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanDefinitionRegistryPostProcessors(PostProcessorRegistrationDelegate.java:349)
        at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:148)
        at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:788)
        at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:606)
        at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:754)
        at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:456)
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:334)
        at com.dressca.batch.BatchApplication.main(BatchApplication.java:19)
Caused by: org.apache.ibatis.logging.LogException: Error creating logger for logger org.mybatis.spring.mapper.ClassPathMapperScanner.  Cause: java.lang.NullPointerException
        at org.apache.ibatis.logging.LogFactory.getLog(LogFactory.java:54)
        at org.apache.ibatis.logging.LogFactory.getLog(LogFactory.java:47)
        at org.mybatis.logging.LoggerFactory.getLogger(LoggerFactory.java:32)
        at org.mybatis.spring.mapper.ClassPathMapperScanner.<clinit>(ClassPathMapperScanner.java:62)
        ... 9 common frames omitted
Caused by: java.lang.NullPointerException: null
        at org.apache.ibatis.logging.LogFactory.getLog(LogFactory.java:52)
        ... 12 common frames omitted
Exception in thread "main" java.lang.IllegalStateException: java.lang.ExceptionInInitializerError
        at org.springframework.boot.SpringApplication.handleRunFailure(SpringApplication.java:825)
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:344)
        at com.dressca.batch.BatchApplication.main(BatchApplication.java:19)
Caused by: java.lang.ExceptionInInitializerError
        at org.mybatis.spring.mapper.MapperScannerConfigurer.postProcessBeanDefinitionRegistry(MapperScannerConfigurer.java:363)
        at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanDefinitionRegistryPostProcessors(PostProcessorRegistrationDelegate.java:349)
        at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:148)
        at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:788)
        at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:606)
        at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:754)
        at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:456)
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:334)
        ... 1 more
Caused by: org.apache.ibatis.logging.LogException: Error creating logger for logger org.mybatis.spring.mapper.ClassPathMapperScanner.  Cause: java.lang.NullPointerException
        at org.apache.ibatis.logging.LogFactory.getLog(LogFactory.java:54)
        at org.apache.ibatis.logging.LogFactory.getLog(LogFactory.java:47)
        at org.mybatis.logging.LoggerFactory.getLogger(LoggerFactory.java:32)
        at org.mybatis.spring.mapper.ClassPathMapperScanner.<clinit>(ClassPathMapperScanner.java:62)
        ... 9 more
Caused by: java.lang.NullPointerException
        at org.apache.ibatis.logging.LogFactory.getLog(LogFactory.java:52)
        ... 12 more

この原因について調査すると、 MyBatis で定義されている独自のロガーをlogback + slf4j に変換しようとする際にエラーが発生しているようでした。

さらに調べてみると、MyBatis Spring Native において、上記のエラーに対応するためのクイックスタートが提供されていました。将来的には、この対応は MyBatis Spring Native のバージョンアップに伴い統合されるようですが、現時点では設定の必要があります。

これに従って、サブプロジェクトinfrastructureconfigフォルダーにMyBatisNativeConfiguration.javaをコピーしました。
なお、Spring Boot 3.2.x のバージョンにおいて、 Bean が早期に初期化されることで、Bean が正常に読み込まれないエラーが発生していることが報告されていました。このことから、MybatisNativeConfiguration.javaresolveMapperFactoryBeanTypeIfNecessaryメソッドを以下のように修正しました。

MyBatisNativeConfigration.java(一部抜粋)
private void resolveMapperFactoryBeanTypeIfNecessary(RootBeanDefinition beanDefinition) {
  if (!beanDefinition.hasBeanClass()
      || !MapperFactoryBean.class.isAssignableFrom(beanDefinition.getBeanClass())) {
    return;
  }
  if (beanDefinition.getResolvableType().hasUnresolvableGenerics()) {
    Class<?> mapperInterface = getMapperInterface(beanDefinition);
    if (mapperInterface != null) {
      // Bean の早期初期化を防ぐために Generics Type を適用
+     ConstructorArgumentValues constructorArgumentValues = new ConstructorArgumentValues();
+     constructorArgumentValues.addGenericArgumentValue(mapperInterface);
+     beanDefinition.setConstructorArgumentValues(constructorArgumentValues);
+     beanDefinition.setConstructorArgumentValues(constructorArgumentValues);
+     beanDefinition.setTargetType(ResolvableType.forClassWithGenerics(beanDefinition.getBeanClass(), mapperInterface));
    }
  }
}

その後、MybatisのMapperのxmlファイルが読み込めないエラーを解消するために、以下のようにMyBatisConfig.javaを作成しました。

MyBatisConfig.java
@Configuration
@EnableTransactionManagement
@MapperScan(basePackages = "com.dressca.infrastructure.repository.mybatis", sqlSessionTemplateRef = "sqlSessionTemplate")
public class MyBatisConfig {
  @Bean
  public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
    SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
    sessionFactory.setDataSource(dataSource);
    // ファイルの読み込みを行うために SpringBootVFS を利用する
    sessionFactory.setVfs(SpringBootVFS.class);

    org.apache.ibatis.session.Configuration config = new org.apache.ibatis.session.Configuration();
    config.setMapUnderscoreToCamelCase(true);
    config.setAutoMappingBehavior(AutoMappingBehavior.FULL);
    VFS.addImplClass(SpringBootVFS.class);
    config.getTypeAliasRegistry().registerAliases("package.to.entities");
    sessionFactory.setConfiguration(config);

    PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
    // xml ファイルをリソースとして設定する
    sessionFactory.setMapperLocations(resolver.getResources("classpath:/**/*.xml"));

    return sessionFactory.getObject();
  }
}

しかし、この設定を行うと「SpringBootVFSに関係するメソッドが見つからない」というエラーが発生しました。

batch.exeのエラー内容
実行結果

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

2024-04-26 11:55:42.842 com.dressca.batch.BatchApplication INFO  main Starting AOT-processed BatchApplication using Java 17.0.9 with PID 9076 (C:\Maia\maia\samples\web-csr\dressca-backend\batch\build\native\nativeCompile\batch.exe started by user in C:\Maia\maia\samples\web-csr\dressca-backend)
2024-04-26 11:55:42.842 com.dressca.batch.BatchApplication INFO  main No active profile set, falling back to 3 default profiles: "local", "common", "dev"
2024-04-26 11:55:42.842 org.springframework.context.support.PostProcessorRegistrationDelegate$BeanPostProcessorChecker WARN  main Bean 'jobRegistry' of type [org.springframework.batch.core.configuration.support.MapJobRegistry] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying). Is this bean getting eagerly injected into a currently created BeanPostProcessor [jobRegistryBeanPostProcessor]? Check the corresponding BeanPostProcessor declaration and its dependencies.
2024-04-26 11:55:42.921 org.apache.ibatis.io.VFS ERROR main Failed to instantiate class org.mybatis.spring.boot.autoconfigure.SpringBootVFS
java.lang.NoSuchMethodException: org.mybatis.spring.boot.autoconfigure.SpringBootVFS.<init>()
        at java.base@17.0.9/java.lang.Class.checkMethod(DynamicHub.java:1038)
        at java.base@17.0.9/java.lang.Class.getConstructor0(DynamicHub.java:1204)
        at java.base@17.0.9/java.lang.Class.getDeclaredConstructor(DynamicHub.java:2754)
        at org.apache.ibatis.io.VFS$VFSHolder.createVFS(VFS.java:61)
        at org.apache.ibatis.io.VFS$VFSHolder.<clinit>(VFS.java:48)
        at org.apache.ibatis.io.VFS.getInstance(VFS.java:90)
        at org.apache.ibatis.io.ResolverUtil.find(ResolverUtil.java:250)
        at org.apache.ibatis.type.TypeAliasRegistry.registerAliases(TypeAliasRegistry.java:138)
        at org.apache.ibatis.type.TypeAliasRegistry.registerAliases(TypeAliasRegistry.java:133)
        at com.dressca.infrastructure.repository.mybatis.config.MyBatisConfig.sqlSessionFactory(MyBatisConfig.java:72)
        at com.dressca.infrastructure.repository.mybatis.config.MyBatisConfig$$SpringCGLIB$$0.CGLIB$sqlSessionFactory$0(<generated>)
        at com.dressca.infrastructure.repository.mybatis.config.MyBatisConfig$$SpringCGLIB$$FastClass$$1.invoke(<generated>)
        at org.springframework.cglib.proxy.MethodProxy.invokeSuper(MethodProxy.java:258)
        at org.springframework.context.annotation.ConfigurationClassEnhancer$BeanMethodInterceptor.intercept(ConfigurationClassEnhancer.java:331)
        at com.dressca.infrastructure.repository.mybatis.config.MyBatisConfig$$SpringCGLIB$$0.sqlSessionFactory(<generated>)
        at com.dressca.infrastructure.repository.mybatis.config.MyBatisConfig__BeanDefinitions.lambda$getSqlSessionFactoryInstanceSupplier$2(MyBatisConfig__BeanDefinitions.java:70)
        at org.springframework.util.function.ThrowingBiFunction.apply(ThrowingBiFunction.java:68)
        at org.springframework.util.function.ThrowingBiFunction.apply(ThrowingBiFunction.java:54)
        at org.springframework.beans.factory.aot.BeanInstanceSupplier.lambda$get$2(BeanInstanceSupplier.java:206)
        at org.springframework.util.function.ThrowingSupplier.get(ThrowingSupplier.java:58)
        at org.springframework.util.function.ThrowingSupplier.get(ThrowingSupplier.java:46)
        at org.springframework.beans.factory.aot.BeanInstanceSupplier.invokeBeanSupplier(BeanInstanceSupplier.java:218)
        at org.springframework.beans.factory.aot.BeanInstanceSupplier.get(BeanInstanceSupplier.java:206)
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.obtainInstanceFromSupplier(DefaultListableBeanFactory.java:949)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.obtainFromSupplier(AbstractAutowireCapableBeanFactory.java:1217)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1161)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:562)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:522)
        at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:326)
        at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
        at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:324)
        at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:200)
        at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:254)
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1443)
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1353)
        at org.springframework.beans.factory.aot.AutowiredFieldValueResolver.resolveValue(AutowiredFieldValueResolver.java:188)
        at org.springframework.beans.factory.aot.AutowiredFieldValueResolver.resolveObject(AutowiredFieldValueResolver.java:154)
        at org.springframework.beans.factory.aot.AutowiredFieldValueResolver.resolve(AutowiredFieldValueResolver.java:143)
        at com.dressca.batch.job.catalog.CatalogItemReaderConf__Autowiring.apply(CatalogItemReaderConf__Autowiring.java:17)
        at org.springframework.beans.factory.support.InstanceSupplier$1.get(InstanceSupplier.java:83)
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.obtainInstanceFromSupplier(DefaultListableBeanFactory.java:949)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.obtainFromSupplier(AbstractAutowireCapableBeanFactory.java:1217)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1161)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:562)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:522)
        at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:326)
        at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
        at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:324)
        at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:200)
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:975)
        at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:962)
        at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:624)
        at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:754)
        at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:456)
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:334)
        at com.dressca.batch.BatchApplication.main(BatchApplication.java:19)
2024-04-26 11:55:42.928 org.springframework.context.support.GenericApplicationContext WARN  main Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'catalogItemReaderConf': Unsatisfied dependency expressed through field 'sqlSessionFactory': Error creating bean with name 'sqlSessionFactory': Instantiation of supplied bean failed
2024-04-26 11:55:42.929 org.springframework.boot.SpringApplication ERROR main Application run failed
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'catalogItemReaderConf': Unsatisfied dependency expressed through field 'sqlSessionFactory': Error creating bean with name 'sqlSessionFactory': Instantiation of supplied bean failed
        at org.springframework.beans.factory.aot.AutowiredFieldValueResolver.resolveValue(AutowiredFieldValueResolver.java:194)
        at org.springframework.beans.factory.aot.AutowiredFieldValueResolver.resolveObject(AutowiredFieldValueResolver.java:154)
        at org.springframework.beans.factory.aot.AutowiredFieldValueResolver.resolve(AutowiredFieldValueResolver.java:143)
        at com.dressca.batch.job.catalog.CatalogItemReaderConf__Autowiring.apply(CatalogItemReaderConf__Autowiring.java:17)
        at org.springframework.beans.factory.support.InstanceSupplier$1.get(InstanceSupplier.java:83)
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.obtainInstanceFromSupplier(DefaultListableBeanFactory.java:949)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.obtainFromSupplier(AbstractAutowireCapableBeanFactory.java:1217)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1161)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:562)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:522)
        at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:326)
        at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
        at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:324)
        at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:200)
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:975)
        at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:962)
        at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:624)
        at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:754)
        at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:456)
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:334)
        at com.dressca.batch.BatchApplication.main(BatchApplication.java:19)
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'sqlSessionFactory': Instantiation of supplied bean failed       
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.obtainFromSupplier(AbstractAutowireCapableBeanFactory.java:1223)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1161)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:562)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:522)
        at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:326)
        at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
        at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:324)
        at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:200)
        at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:254)
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1443)
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1353)
        at org.springframework.beans.factory.aot.AutowiredFieldValueResolver.resolveValue(AutowiredFieldValueResolver.java:188)
        ... 20 common frames omitted
Caused by: java.lang.NullPointerException: null
        at org.apache.ibatis.io.ResolverUtil.find(ResolverUtil.java:250)
        at org.apache.ibatis.type.TypeAliasRegistry.registerAliases(TypeAliasRegistry.java:138)
        at org.apache.ibatis.type.TypeAliasRegistry.registerAliases(TypeAliasRegistry.java:133)
        at com.dressca.infrastructure.repository.mybatis.config.MyBatisConfig.sqlSessionFactory(MyBatisConfig.java:72)
        at com.dressca.infrastructure.repository.mybatis.config.MyBatisConfig$$SpringCGLIB$$0.CGLIB$sqlSessionFactory$0(<generated>)
        at com.dressca.infrastructure.repository.mybatis.config.MyBatisConfig$$SpringCGLIB$$FastClass$$1.invoke(<generated>)
        at org.springframework.cglib.proxy.MethodProxy.invokeSuper(MethodProxy.java:258)
        at org.springframework.context.annotation.ConfigurationClassEnhancer$BeanMethodInterceptor.intercept(ConfigurationClassEnhancer.java:331)
        at com.dressca.infrastructure.repository.mybatis.config.MyBatisConfig$$SpringCGLIB$$0.sqlSessionFactory(<generated>)
        at com.dressca.infrastructure.repository.mybatis.config.MyBatisConfig__BeanDefinitions.lambda$getSqlSessionFactoryInstanceSupplier$2(MyBatisConfig__BeanDefinitions.java:70)
        at org.springframework.util.function.ThrowingBiFunction.apply(ThrowingBiFunction.java:68)
        at org.springframework.util.function.ThrowingBiFunction.apply(ThrowingBiFunction.java:54)
        at org.springframework.beans.factory.aot.BeanInstanceSupplier.lambda$get$2(BeanInstanceSupplier.java:206)
        at org.springframework.util.function.ThrowingSupplier.get(ThrowingSupplier.java:58)
        at org.springframework.util.function.ThrowingSupplier.get(ThrowingSupplier.java:46)
        at org.springframework.beans.factory.aot.BeanInstanceSupplier.invokeBeanSupplier(BeanInstanceSupplier.java:218)
        at org.springframework.beans.factory.aot.BeanInstanceSupplier.get(BeanInstanceSupplier.java:206)
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.obtainInstanceFromSupplier(DefaultListableBeanFactory.java:949)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.obtainFromSupplier(AbstractAutowireCapableBeanFactory.java:1217)
        ... 31 common frames omitted

GraalVMでは、SpringBootVFS のライブラリは未対応であるようでした。
そこで、未対応のライブラリを利用するために動的コンパイルするよう設定しました。

同様に動的コンパイルが必要なライブラリとして、lombok@Valueアノテーションが静的コンパイル時に読み込まれないエラーが報告されていました。
そのため、@Valueアノテーションを用いているメソッドに関しても動的コンパイルするよう設定しました。具体的には、batch/src/main/resource/META-INF/native-image/reflect-config.jsonを作成し、以下の記載を行いました。

reflect-config.json
[
    {
        "name" : "org.mybatis.spring.boot.autoconfigure.SpringBootVFS",
        "allDeclaredConstructors" : true,
        "allPublicConstructors" : true,
        "allDeclaredMethods" : true,
        "allPublicMethods" : true,
        "methods" : [
            { "name" : "<init>", "parameterTypes" : [] }
        ]
    },
    {
        "name" : "com.dressca.applicationcore.order.Address",
        "allDeclaredConstructors" : true,
        "allPublicConstructors" : true,
        "allDeclaredMethods" : true,
        "allPublicMethods" : true
    }
]

以上の対応で、正しくバッチアプリケーションが実行されました!

実行結果

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

2024-04-26 12:39:51.318 com.dressca.batch.BatchApplication INFO  main Starting AOT-processed BatchApplication using Java 17.0.9 with PID 6088 (C:\Maia\maia\samples\web-csr\dressca-backend\batch\build\native\nativeCompile\batch.exe started by user in C:\Maia\maia\samples\web-csr\dressca-backend)
2024-04-26 12:39:51.318 com.dressca.batch.BatchApplication INFO  main No active profile set, falling back to 3 default profiles: "local", "common", "dev"
2024-04-26 12:39:51.338 org.springframework.context.support.PostProcessorRegistrationDelegate$BeanPostProcessorChecker WARN  main Bean 'jobRegistry' of type [org.springframework.batch.core.configuration.support.MapJobRegistry] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying). Is this bean getting eagerly injected into a currently created BeanPostProcessor [jobRegistryBeanPostProcessor]? Check the corresponding BeanPostProcessor declaration and its dependencies.
2024-04-26 12:39:51.450 com.zaxxer.hikari.HikariDataSource INFO  main HikariPool-1 - Starting...
2024-04-26 12:39:51.453 com.zaxxer.hikari.pool.HikariPool INFO  main HikariPool-1 - Added connection conn0: url=jdbc:h2:mem:./data user=
2024-04-26 12:39:51.453 com.zaxxer.hikari.HikariDataSource INFO  main HikariPool-1 - Start completed.
2024-04-26 12:39:51.453 org.springframework.batch.core.repository.support.JobRepositoryFactoryBean INFO  main No database type set, using meta data indicating: H2 
2024-04-26 12:39:51.465 org.springframework.batch.core.configuration.annotation.BatchObservabilityBeanPostProcessor INFO  main No Micrometer observation registry found, defaulting to ObservationRegistry.NOOP
2024-04-26 12:39:51.466 org.springframework.batch.core.configuration.annotation.BatchObservabilityBeanPostProcessor INFO  main No Micrometer observation registry found, defaulting to ObservationRegistry.NOOP
2024-04-26 12:39:51.468 org.springframework.batch.core.configuration.annotation.BatchObservabilityBeanPostProcessor INFO  main No Micrometer observation registry found, defaulting to ObservationRegistry.NOOP
2024-04-26 12:39:51.469 org.springframework.batch.core.configuration.annotation.BatchObservabilityBeanPostProcessor INFO  main No Micrometer observation registry found, defaulting to ObservationRegistry.NOOP
2024-04-26 12:39:51.470 org.springframework.batch.core.launch.support.SimpleJobLauncher INFO  main No TaskExecutor has been set, defaulting to synchronous executor.
2024-04-26 12:39:51.472 org.hibernate.validator.internal.util.Version INFO  main HV000001: Hibernate Validator 8.0.1.Final
2024-04-26 12:39:51.479 com.dressca.batch.BatchApplication INFO  main Started BatchApplication in 0.189 seconds (process running for 0.189)
2024-04-26 12:39:51.481 com.zaxxer.hikari.HikariDataSource INFO  main HikariPool-1 - Shutdown initiated...
2024-04-26 12:39:51.481 com.zaxxer.hikari.HikariDataSource INFO  main HikariPool-1 - Shutdown completed.

パフォーマンス測定

実際にネイティブイメージ化することで、どの程度パフォーマンスが向上するのかを確認してみました。

なお、時間計測には Windows Powershell の Measure-Command コマンドを、メモリ計測には、Linux 環境の /user/bin/time コマンドをそれぞれ使用しました。
また、各データは 5 回実行した際の平均と分散を求め、測定結果としています。

Measure-command をはじめとする Windows PowerShell のコマンドは、.NET Framework で構成されています。 .NET Framework の場合、アセンブリが IL と呼ばれる中間言語で記載されています。初回起動では、 JIT コンパイラを用いて中間言語を機械語に翻訳する時間がかかります。
また、初回起動時はNgen.exe と呼ばれるネイティブイメージを生成するツールが実行されます。これにより、2回目以降はアセンブリが機械語に翻訳された形で直接起動できるようになり、起動が高速化します。
なるべく アプリケーションの起動・実行にかかる時間のみを比較するため、Measure-commandを利用した計測では、2回目以降の結果を用いています。

測定結果

nativeCompile によるコンパイル時間

実行コマンド 説明 単位 最小 最大 平均
./gradlew batch:nativeCompile ネイティブイメージの
コンパイルの実行時間
4.689 4.782 4.722

サンプルアプリケーションのような実装でも、コンパイル時間はおよそ 5 分程度かかるようです。
Spring Initializr によって作成したデフォルトのアプリケーションをそのままネイティブイメージ化した場合は平均 1.5 分ほどのコンパイル時間でした。このことから、アプリケーションの構成が複雑になりプログラム量が多くなるほど、コンパイル時間が増大することになります。
実際の開発環境において、ネイティブイメージにコンパイルしてテストすることは多くの時間を費やす可能性があります。この時間が発生することを考慮し、慎重に判断することが必要でしょう。

実行時間の比較

実行コマンド 説明 単位 最小 最大 平均
batch.exe ネイティブイメージ
アプリの実行時間
0.209 0.232 0.216
./gradlew batch:bootRun gradleによる
アプリの実行時間
3.592 3.986 3.781

ネイティブイメージによる実行と通常の Gradle による実行で実行時間を比較しました。
ここで、実行時間とは「実行コマンドを受け付けてから処理が完了するまでの時間」としています。

結果を比較すると、ネイティブイメージ化を実施したことで Bean の生成・初期化処理などがコンパイル時に行われ、実行時間が短縮できたことを確認しました。また、実行時間の最大・最小に大きな差はなく安定しており、実行時間が大幅に増加するといった問題は発生しませんでした。

メモリ使用量の比較

実行コマンド 説明 単位 最小 最大 平均
batch.exe ネイティブイメージ
によるアプリのメモリ消費量
MB 1.604 1.604 1.604
./gradlew
batch:bootRun
gradleによる
アプリのメモリ消費量
MB 109.120 127.484 116.598

ネイティブイメージによる実行と通常の Gradle による実行でメモリ消費量を比較しました。

その結果、ネイティブイメージ化したほうが大幅にメモリ使用量を削減できていることがわかります。また、最大・最小の値を見るとわかるように、静的にコンパイルされていることからメモリの使用量が変動せず安定していることもわかります。

考察(実際のシステム開発での利用について)

本検証で実施したネイティブイメージ化は、アプリケーションの実行時間短縮やメモリ使用量の削減といったシステムの負荷を低減する効果があることを確認しました。基本的にはソースコードを変更することなく、ネイティブイメージ化することができる画期的な技術だといえます。

しかしながら、本検証においてサンプルアプリケーションをネイティブイメージ化させるために、log4j2からlogback + slf4j のライブラリに変更させたり、MyBatis 用のファイル読み込み設定のソースコードを追記したりすることは私にとって大きな負荷となりました...。

このように GraalVM がサポートしていないライブラリが多く、利用のためにライブラリの変更したりソースコードを別途作成したりしなくてはならないことは大きな問題です。
実際のシステム開発において、そのようなライブラリを利用できるよう複雑な設定が余儀なくされることは望ましくありません。(サポートしていないライブラリをすべてreflect-config.jsonに記載して動的にコンパイルすることも現実的でないですし...)

実案件では、バッチ処理のプロセスを常時起動するようにしておくなどで対応することが無難と考えます。

よって、現時点では GraalVM を利用したネイティブイメージ化の技術を実際のシステム開発で採用することは難しいと考察します。

アプリケーションの実行時間短縮のための選択肢としてネイティブイメージ化を取り入れるためには、ライブラリの変更やソースコードの追加をすることなくライブラリを使用できるようになる必要があります。

これに対しGraalVM では、サポートするライブラリを増加させるために作業状況が共有されています。このような作業が完了し、サポートするライブラリが増えていくことで、実際のシステム開発で利用できるようになるでしょう。

まとめ

本投稿では、GraalVM 利用してネイティブイメージを実装してみた際の記録をまとめ、実際のシステム開発で利用できるかを考察していきました。

サポートされるライブラリが増えることを見守りながら、ぜひ皆さんもネイティブイメージ化を一つの方法として取り入れてみてください!

We Are Hiring!

14
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
8