---
# System prepended metadata

title: SpringBoot3 AOT 開發紀錄
tags: [GraalVM, Spring, Java17, Ahead of Time Compilation, SpringBoot, AOT]

---

---
title: 'SpringBoot3 AOT 開發紀錄'
disqus: hackmd
---
# 1. 準備
* SpringBoot3
* Java17
* **gradle 插件**：
    * AOT：`id "org.graalvm.buildtools.native" version '0.9.28'`
        ```groovy=
        plugins {
          id 'org.graalvm.buildtools.native' version '0.9.28'
        }
        ```
* **編譯環境**
    * Docker
        * docker image：gradle:8.8.0-jdk17-graal-jammy
    * Linux
        * 需要安裝：`build-essential` \ `zlib1g-dev`
        * 指令sudo apt-get install build-essential zlib1g-dev
    * 其他環境：https://www.graalvm.org/latest/reference-manual/native-image/#build-a-native-executable-using-maven-or-gradle
* **編譯指令**：gradle clean nativeCompile
# 2. 需要調整或檢查的項目
* **參考網址**
    * https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-with-GraalVM
    * https://www.graalvm.org/latest/reference-manual/native-image/#build-a-native-executable-using-maven-or-gradle
    * https://www.graalvm.org/latest/reference-manual/native-image/metadata/#reflection
## 2.1 Log包的選擇
* **Logfj2**(`Springboot3不支持AOT`) 在**編譯**(`AOT`)時會出現Class NotFound，目前改用SpringBoot 默認的logback
* pom.xml：引入 logback-db的核心
    *  `implementation 'ch.qos.logback.db:logback-core-db:1.2.11.1'`
    *  github：https://github.com/qos-ch/logback-db
    ```groovy
    configurations {
        compileOnly {
            extendsFrom annotationProcessor
        }
        //排除所有
        configureEach {
            exclude group: 'org.apache.logging.log4j', module: 'log4j-api'
        }
    }
    ```
### 2.1.1 Log-To-DB
* 自定義一個繼承 DBAppenderBase\<E>的類
    * **建議**：重寫DBAppenderBase抽象類的append方法，**避免被默認的PK綁架**
    ```java=
    public class LogDBAppender extends DBAppenderBase<ILoggingEvent> {
        static final int LOG_TIME_INDEX = 1;
        static final int LEVEL_STRING_INDEX = 2;
        static final int THREAD_NAME_INDEX = 3;
        static final int CLASS_NAME_INDEX = 4;
        static final int MESSAGE_INDEX = 5;
        static final int EXCEPTION_MESSAGE_INDEX = 6;
        static final int BACKEND_NAME_INDEX = 7;
        static final String BACKEND_NAME = "API-SERVER";
        private final String insertSQL = "INSERT INTO app_log" +
                "(log_time, \"level\", thread, \"class\", message, \"exception\", backend_name) " +
                "VALUES(?, ?, ?, ?, ?, ?, ?);";

        @Override
        public void start() {
            if (connectionSource == null) {
                throw new IllegalStateException("DBAppender cannot function without a connection source");
            }
            // all nice and dandy on the eastern front
            started = true;
        }

        @Override
        protected Method getGeneratedKeysMethod() {
            return null;
        }

        @Override
        protected String getInsertSQL() {
            return this.insertSQL;
        }

        //重寫DBAppenderBase抽象類的append方法，避免被默認的PK綁架
        @Override
        public void append(ILoggingEvent eventObject) {
            Connection connection = null;
            PreparedStatement insertStatement = null;
            try {
                connection = connectionSource.getConnection();
                connection.setAutoCommit(false);
                insertStatement = connection.prepareStatement(getInsertSQL());
                // inserting an event and getting the result must be exclusive
                synchronized (this) {
                    subAppend(eventObject, connection, insertStatement);
                }

                connection.commit();
            } catch (Throwable sqle) {
                addError("problem appending event", sqle);
            } finally {
                DBHelper.closeStatement(insertStatement);
                DBHelper.closeConnection(connection);
            }
        }

        @Override
        protected void subAppend(ILoggingEvent eventObject, Connection connection, PreparedStatement statement) throws Throwable {
            statement.setTimestamp(LOG_TIME_INDEX, new Timestamp(eventObject.getTimeStamp()));
            statement.setString(LEVEL_STRING_INDEX, eventObject.getLevel().toString());
            statement.setString(THREAD_NAME_INDEX, eventObject.getThreadName());
            statement.setString(CLASS_NAME_INDEX, eventObject.getLoggerName());
            statement.setString(MESSAGE_INDEX, eventObject.getFormattedMessage());
            statement.setString(BACKEND_NAME_INDEX, BACKEND_NAME);

            if (eventObject.getThrowableProxy() != null) {
                IThrowableProxy throwableProxy = eventObject.getThrowableProxy();
                StringBuilder errorMsg = new StringBuilder(1000);
                errorMsg.append(throwableProxy.getClassName()).append(": ").append(throwableProxy.getMessage()).append(
                        "\n");
                Arrays.stream(throwableProxy.getStackTraceElementProxyArray()).collect(Collectors.toList()).forEach(stackTraceElementProxy -> {
                    System.out.println(stackTraceElementProxy.getSTEAsString());
                });
                for (StackTraceElementProxy stackTraceElementProxy : throwableProxy.getStackTraceElementProxyArray()) {
                    errorMsg.append("\t").append(stackTraceElementProxy.getSTEAsString()).append("\n");
                }
                errorMsg.replace(errorMsg.lastIndexOf("\n"), errorMsg.length(), "");
                statement.setString(EXCEPTION_MESSAGE_INDEX, errorMsg.toString());
            } else {
                statement.setString(EXCEPTION_MESSAGE_INDEX, "");

            }

            int updateCount = statement.executeUpdate();
            if (updateCount != 1) {
                addWarn("Failed to insert loggingEvent");
            }
        }

        @Override
        protected void secondarySubAppend(ILoggingEvent eventObject, Connection connection, long eventId) throws Throwable {

        }
    }
    ```
* logback-spring.xml
    ```xml=
    <configuration>

        <property name="LOGS" value="logs"/>



        <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
            <layout class="ch.qos.logback.classic.PatternLayout">
                <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %highlight([%level]) %logger - %msg%n</pattern>
            </layout>
        </appender>

        <appender name="DB" class="com.nicolas.logback_db_test.log.NDBAppender">
            <connectionSource class="ch.qos.logback.core.db.DataSourceConnectionSource">
                <dataSource class="com.zaxxer.hikari.HikariDataSource">
                    <jdbcUrl>jdbc:postgresql://localhost:5432/postgres</jdbcUrl>
                    <driverClassName>org.postgresql.Driver</driverClassName>
                    <poolName>log-pool</poolName>
                </dataSource>
            </connectionSource>
        </appender>


        <root level="INFO">
            <appender-ref ref="STDOUT"/>
            <appender-ref ref="DB"/>
        </root>

    </configuration>
    ```
## 2.2 JPA + DB
### 2.2.1 線程池配置
* DB線程池 需要**設定反射配置**(路徑 `classpath:META-INT/native-image/reflect-config.json`)
    ```json=
    [
      {
        "name": "com.zaxxer.hikari.HikariDataSource",
        "methods": [
          {
            "name": "<init>",
            "parameterTypes": []
          }
        ]
      },
      {
        "name": "org.hibernate.dialect.PostgreSQLDialect",
        "allDeclaredMethods": true,
        "allDeclaredFields": true,
        "allDeclaredConstructors": true
      },
      {
        "name": "com.zaxxer.hikari.HikariConfig",
        "allDeclaredClasses": true,
        "allDeclaredMethods": true,
        "allDeclaredFields": true,
        "allDeclaredConstructors": true,
        "allPublicClasses": true,
        "allPublicMethods": true,
        "allPublicFields": true,
        "allPublicConstructors": true,
        "allRecordComponents": true,
        "allNestMembers": true,
        "allSigners": true,
        "allPermittedSubclasses": true,
        "queryAllDeclaredMethods": true,
        "queryAllDeclaredConstructors": true,
        "queryAllPublicMethods": true,
        "queryAllPublicConstructors": true,
        "unsafeAllocated": true
      },
      {
        "name": "java.sql.Statement[]"
      }
    ]
    ```
### 2.2.2 JAP
* 需要配置 **PersistenceManagedTypes**
    * 未配置所導致的問題：無法找到Entity
        * 訊息：Not a managed type
    * 範例： 
        ```java=
            @Bean("PersistenceManagedTypes")
            @Primary
            PersistenceManagedTypes managedTypes(ResourceLoader resourceLoader) {
                return new PersistenceManagedTypesScanner(resourceLoader)
                        .scan("com.example.aotdemo.apdater.repository");
            }
        ```
    * 參考網址：https://github.com/DigitalMediageek/aot-database-issue/blob/main/src/main/java/com/example/database/hikari/Graal1DbConfig.java
    * 完整配置
        ```java=
        package com.example.aotdemo.config;

        import com.zaxxer.hikari.HikariDataSource;
        import jakarta.persistence.EntityManagerFactory;
        import org.springframework.beans.factory.annotation.Qualifier;
        import org.springframework.beans.factory.annotation.Value;
        import org.springframework.boot.context.properties.ConfigurationProperties;
        import org.springframework.boot.jdbc.DataSourceBuilder;
        import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
        import org.springframework.context.annotation.Bean;
        import org.springframework.context.annotation.Configuration;
        import org.springframework.context.annotation.Primary;
        import org.springframework.core.io.ResourceLoader;
        import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
        import org.springframework.orm.jpa.JpaTransactionManager;
        import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
        import org.springframework.orm.jpa.persistenceunit.PersistenceManagedTypes;
        import org.springframework.orm.jpa.persistenceunit.PersistenceManagedTypesScanner;
        import org.springframework.transaction.PlatformTransactionManager;

        import javax.sql.DataSource;
        import java.util.HashMap;
        import java.util.Map;

        @Configuration
        @EnableJpaRepositories(entityManagerFactoryRef = "CoreEntityManagerFactory",
                transactionManagerRef = "CoreTransactionManager",
                basePackages = {"com.example.aotdemo.adapter.repository"})
        public class CoreDataSourceConfig {

            @Value("${MAXIMUM_POOL_SIZE:2}")
            private int maximumPoolSize;

            @Primary
            @Bean(name = "CoreDatasource")
            @ConfigurationProperties(prefix = "spring.aotdemo.datasource")
            public DataSource dataSource() {
                HikariDataSource dataSource = DataSourceBuilder.create().type(HikariDataSource.class).build();
                dataSource.setMaximumPoolSize(maximumPoolSize);
                dataSource.setPoolName("CorePool");
                return dataSource;
            }

            @Primary
            @Bean(name = "CoreEntityManagerFactory")
            public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean(EntityManagerFactoryBuilder builder,
                                                                                   @Qualifier("CoreDatasource") DataSource dataSource,
                                                                                   @Qualifier("PersistenceManagedTypes") PersistenceManagedTypes persistenceManagedTypes) {
                Map<String, Object> properties = new HashMap<>();
                properties.put("hibernate.hbm2ddl.auto", "none");
                return builder.dataSource(dataSource)
                        .properties(properties)
                        .packages("com.example.aotdemo.adapter.repository")
                        .persistenceUnit("core_db")
                        .managedTypes(persistenceManagedTypes)
                        .build();
            }

            @Primary
            @Bean(name = "CoreTransactionManager")
            public PlatformTransactionManager transactionManager(
                    @Qualifier("CoreEntityManagerFactory") EntityManagerFactory entityManagerFactory) {
                return new JpaTransactionManager(entityManagerFactory);
            }
           
            @Bean("PersistenceManagedTypes")
            @Primary
            PersistenceManagedTypes managedTypes(ResourceLoader resourceLoader) {
                return new PersistenceManagedTypesScanner(resourceLoader)
                        .scan("com.example.aotdemo.adapter.repository");
            }
        }

        ```
        
## 2.3 Spring Validation
* 使用沒有改變，不需要做額外配置
    * **@Vaild**/**@Validated**/**@NotEmpty**/..：不需要額外處理
* 使用 **@RequestMapping**下使用 **@RequestBody**的**參數**會觸發 Spring Boot 的 AOT（Ahead-of-Time）的處理。(`配置該類的信息`)
    * 當使用到**反射**時就可以從`reflect-config.json`中**獲取類的信息**
    * **源碼**：**ControllerMappingReflectiveProcessor**.java 中的 `registerParameterTypeHints()`

## 2.4 GlobalExceptionHandler
* 使用沒有改變，不需要做額外配置
* **@ControllerAdvice** 不需要額外處理
    * 當中有使用到annotation：不需要額外處理
## 2.5 SpringBoot Security
* Spring Boot3下的Security配置不需要額外處理
* **其他**：權限驗證採用 `@PreAuthorize + 自訂權限驗證器`：需要配置該`自訂權限驗證器`的信息(`權限驗證是使用反射的方式來處理`)
    * 另外**Spel表達式**有使用到**Enum**，需要添加該Enum的類信息
    * @PreAuthorize("@**EunoAISecurityExpression**.hasAuthority(T(com.example.aotdemo.entity.PrivilegeActionEnum).CREATE_AND_EDIT_SOURCE)")
        * **EunoAISecurityExpression**：**為自訂權限驗證器的BeanName**
        * **配置**：**自訂權限驗證器**、**Enum**
            ```json=
            {
                "name": "com.example.aotdemo.entity.PrivilegeActionEnum",
                "allDeclaredClasses": true,
                "allDeclaredMethods": true,
                "allDeclaredFields": true,
                "allDeclaredConstructors": true,
                "allPublicClasses": true,
                "allPublicMethods": true,
                "allPublicFields": true,
                "allPublicConstructors": true,
                "allRecordComponents": true,
                "allNestMembers": true,
                "allSigners": true,
                "allPermittedSubclasses": true,
                "queryAllDeclaredMethods": true,
                "queryAllDeclaredConstructors": true,
                "queryAllPublicMethods": true,
                "queryAllPublicConstructors": true,
                "unsafeAllocated": true
              },
              {
                "name": "com.example.aotdemo.service.security.EunoAISecurityExpression",
                "allDeclaredClasses": true,
                "allDeclaredMethods": true,
                "allDeclaredFields": true,
                "allDeclaredConstructors": true,
                "allPublicClasses": true,
                "allPublicMethods": true,
                "allPublicFields": true,
                "allPublicConstructors": true,
                "allRecordComponents": true,
                "allNestMembers": true,
                "allSigners": true,
                "allPermittedSubclasses": true,
                "queryAllDeclaredMethods": true,
                "queryAllDeclaredConstructors": true,
                "queryAllPublicMethods": true,
                "queryAllPublicConstructors": true,
                "unsafeAllocated": true
              }
            ```
            * 目前是添加**該類的所有信息配置**，`後續可以在調整類信息的配置`

## 2.6 OpenAPI
* SpringBoot3下的OpenAPI可以直上

## 2.7 JDBC版本 確認
* Orcale
    * **ojbc參考表**：https://www.oracle.com/database/technologies/appdev/jdbc-downloads.html
    * **ojbc8** 支援 `JDK8\JDK11`
        * 導致Log出現無法解析問題
    * **解**：上升至 ojbc10
* Access
    * **ucanaccess**
        * 使用 hsqldb 當記憶體DB，**SpringBoot3不支援 hsqldb**
    * 解： 直接使用 com.healthmarketscience.jackcess

## 2.8 Bean注入方式：默認禁止方法注入(factory-method)-重點
* **禁止原因**：
    * 為了原生映像（native image）和 AOT 最佳化，**默認關閉代理機制**（proxyBeanMethods = false）
    * 這避免了 CGLIB 動態代理在編譯時產生問題，提升啟動速度並減少原生映像大小 
* 錯誤訊息：UnsatisfiedDependencyException 
    * Unexpected AOP exception
*  **建議手動標注**明確表明不使用代理
    ```java
    @Configuration(proxyBeanMethods = false)
    ```
 
## 2.9 自訂國際化資源失效
* **原因**：**國際化資源為動態載入**
    * 當需要進行國際化處理的時候，才會載入國際化資源
    * **參考**：**ResourceBundleMessageSource類**中的**getResourceBundle()**
* **解法**：參考**MessageSourceAutoConfiguration**中的**MessageSourceRuntimeHints**
    * 確保了資源文件模式在運行時可以加載
    * **另一個方法**：**將資源的路徑**設定到`resource-config.json`
* **代碼**：**添加三個資源**
    ```java=
    @Configuration()
    @ImportRuntimeHints({XxxConfig.XxxMessageSourceRuntimeHints.class})
    public class XxxConfig {
        @Bean("MessageSource")
        public MessageSource messageSource() {
            ReloadableResourceBundleMessageSource source = new ReloadableResourceBundleMessageSource();
            source.setBasenames("classpath:i18n/messages",
                    "classpath:i18n/ValidationMessages",
                    "classpath:i18n/postgresqlErrorCodes");
            source.setDefaultEncoding(StandardCharsets.UTF_8.name());
            return source;
        }

        @Bean("LocalValidatorFactoryBean")
        public LocalValidatorFactoryBean getValidator() {
            LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();
            bean.setValidationMessageSource(messageSource());
            return bean;
        }

        static class XxxMessageSourceRuntimeHints implements RuntimeHintsRegistrar {
            XxxMessageSourceRuntimeHints() {
            }

            @Override
            public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
                hints.resources().registerPattern("i18n/messages.properties")
                        .registerPattern("i18n/ValidationMessages.properties")
                        .registerPattern("i18n/postgresqlErrorCodes.properties");
            }
        }
    }
    ```
    * **registerHints**中的路徑不需要添加 classpath，並以檔案名開頭(`相對路徑`)

## 2.10 使用gson或ObjectMapper轉換
* **狀況**：如果使用**gson**或**ObjectMapper**在`Java 對象`和`JSON字串`之間進行轉換，該對象**未設定AOT配置**，會有如下問題：
    * gson：將Entity轉換一個`{}`字串
    * ObjectMapper：出現無法轉換的異常
* **原因**：**gson**或**ObjectMapper**是使用**反射**(`在運行中動態加載類的信息`)的方式，但**編譯後無法動態加載類的訊息**，所以需要**先定義**該**類**的`字段`、`方法`、`信息`
* **可能使用gson或ObjectMapper轉換的對象的情況**：
    * **Call API**：
        * **建議**：改用SpringBoot3的**HttpInterface**，該參數`如果是一個RequestBody`的話會貼上 **@RequestBody**，SpringBoot自動將此**對象**的**添加**到`META-INF/native-image/reflect-config.json`
            * **上述的觸發機制**：**@RequestBody**觸發SpringBoot的AOT的自動配置
                * RestFulAPI與HttpInterface一至的作法
        * **建議**：需要將轉換的物件設定在`META-INF/native-image/reflect-config.json` 
    * 使用**RabbitMQ**來**發Queue**或**聽Queue**
        * **建議**：需要將轉換的物件設定在`META-INF/native-image/reflect-config.json`
* **配置範例**：**Teacher類**
    * **對象**
        ```java=
        @Data
        public Class Teacher{
            private String id;
            private String name;
            private String sex;    
        }
        ```
    * **reflect-config.json**
        ```json=
        [
          {
            "name": "com.nicolas.mq_test.entity.Teacher",
            "allDeclaredFields": true,
            "allDeclaredConstructors": true,
            "fields": [
              {
                "name": "id"
              },
              {
                "name": "name"
              },
              {
                "name": "sex"
              }
            ],
            "methods": [
              {
                "name": "getId",
                "parameterTypes": []
              },
              {
                "name": "getName",
                "parameterTypes": []
              },
              {
                "name": "getSex",
                "parameterTypes": []
              },
              {
                "name": "setId",
                "parameterTypes": [
                  "java.lang.String"
                ]
              },
              {
                "name": "setName",
                "parameterTypes": [
                  "java.lang.String"
                ]
              },
              {
                "name": "setSex",
                "parameterTypes": [
                  "java.lang.String"
                ]
              }
            ]
          }
        ]
        ```
* **參考網址**：https://www.graalvm.org/latest/reference-manual/native-image/dynamic-features/Reflection/#configuration-with-features
* 使用 SpringBoot註解： **@RegisterReflectionForBinding**(`Spring 6.1.17`) \ **@ReflectiveScan** (`Spring 6.2.3`)
    * **RegisterReflectionForBinding使用方式**：https://docs.spring.io/spring-framework/docs/6.1.20-SNAPSHOT/javadoc-api/org/springframework/aot/hint/annotation/RegisterReflectionForBinding.html
        ```JAVA=
         @Configuration
         @RegisterReflectionForBinding({Foo.class, Bar.class})
         public class MyConfig {
             // ...
         }
        ```
    * **參考**：`@RequestMapping`
    * **網址**：https://docs.spring.io/spring-framework/reference/6.1-SNAPSHOT/core/aot.html#aot.hints.reflective
# 3. 開發經歷
## 3.1 AotInitializerNotFoundException
* **錯誤訊息**
    ```txt=
    org.springframework.boot.AotInitializerNotFoundException: Startup with AOT mode enabled failed: AOT initializer com.eunodata.eunoai.EunoAiBackendApplication__ApplicationContextInitializer could not be found
            at org.springframework.boot.SpringApplication.addAotGeneratedInitializerIfNecessary(SpringApplication.java:443)
            at org.springframework.boot.SpringApplication.prepareContext(SpringApplication.java:400)
            at org.springframework.boot.SpringApplication.run(SpringApplication.java:334)
            at org.springframework.boot.SpringApplication.run(SpringApplication.java:1363)
            at org.springframework.boot.SpringApplication.run(SpringApplication.java:1352)
            at com.eunodata.eunoai.EunoAiBackendApplication.main(EunoAiBackendApplication.java:24)
    ```
    * **ClassLoader**中無法找到`com.eunodata.eunoai.EunoAiBackendApplication`
* **當前原因**：JavaCompile的Task中設定**關閉processAot** (`processAot.enabled(false)`)
    ```gradle=

    tasks.withType(JavaCompile) {
        options.encoding = 'UTF-8'
        options.compilerArgs << '-Xlint:all'
        processAot.enabled(false)
    }
    ```
    * **解決**：**開啟processAot**或**移除此設定**(`默認開啟AOT`)
    * 另外關閉JavaCompile中的AOT，會使**編譯速度加速很多**(`判斷因為該部分沒有被編譯，所以加速`)
* 參考：
    * https://www.xuanyuanli.cn/pages/dd8447/#_1-2-%E9%85%8D%E7%BD%AE-maven-%E6%88%96-gradle
###### tags: `Spring` `SpringBoot` `Java17` `AOT` `GraalVM` `Ahead of Time Compilation`