---
# System prepended metadata

title: SpringBoot3 AOT Developer Record
tags: [JDK17, SpringBoot, Java]

---

---
title: 'SpringBoot3 AOT Developer Record'
disqus: hackmd
---

# 1. Prepare
* SpringBoot3
* Java17
* gradle plugin
    * AOT：`id "org.graalvm.buildtools.native" version '0.9.28'`
* Compilation environment
    * Docker：
        * docker image：gradle:8.8.0-jdk17-graal-jammy
    * Linux： 
        * installed：`build-essential` \ `zlib1g-dev`
        * sudo apt-get install build-essential zlib1g-dev
    * Other：https://www.graalvm.org/latest/reference-manual/native-image/#build-a-native-executable-using-maven-or-gradle
* Compilation command：`gradle clean nativeCompile`

# 2. Items to adjust or review
* Reference URL
    * 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 Selection of log packages
* **Log4j2** is not supported
    * ClassNotFound occurs during compilation
* **Logback** is supported

## 2.2 JAP + DB
### 2.2.1 DB
* **Reflection setup needed**：**DB** & **Hibernate**
    * **Configuration path**：`classpath:META-INT/native-image/reflect-config.json`
    * **Configuration**：
        ```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 JPA
* Configuration of **PersistenceManagedTypes** is needed
    * Issue caused by missing configuration: Unable to find Entity
        * Message: Not a managed type
    * **Example**：
        ```java=
            @Bean("PersistenceManagedTypes")
            @Primary
            PersistenceManagedTypes managedTypes(ResourceLoader resourceLoader) {
                return new PersistenceManagedTypesScanner(resourceLoader)
                        .scan("com.example.aotdemo.apdater.repository");
            }
        ```
    * Reference URL：https://github.com/DigitalMediageek/aot-database-issue/blob/main/src/main/java/com/example/database/hikari/Graal1DbConfig.java

## 2.3 Spring Validation
* No changes have been made, no additional configuration is required.
* Using **@RequestBody parameters** triggers Spring Boot's AOT (Ahead-of-Time) processing. (`Configure information for the class`)
    * When reflection is used, class information can be retrieved from `reflect-config.json`

## 2.4 GlobalExceptionHandler
* No changes have been made, no additional configuration is required.

## 2.5 SpringBoot Security
* No changes have been made, no additional configuration is required.
* **Permission verification** uses **@PreAuthorize** + **custom permission verifier**: Information for the **custom permission verifier** needs to be configured (`Permission verification is handled using reflection`).
    * If the **SpEL expression** uses an **Enum**, the class information for the Enum needs to be added.
    * @PreAuthorize("@**EunoAISecurityExpression**.hasAuthority(T(com.example.aotdemo.entity.**PrivilegeActionEnum**).CREATE_AND_EDIT_SOURCE)"
        * **EunoAISecurityExpression**: The **BeanName** for the custom permission verifier.
        * **Configuration**: **Custom permission verifier**、**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
* No changes have been made, no additional configuration is required.

## 2.7 JDBC Version Confirm
* Orcale：
    * **OJBC reference table**：https://www.oracle.com/database/technologies/appdev/jdbc-downloads.html
    * **ojbc8** Support `JDK8\JDK11`
        * JDK17：Causes the log to show unresolvable issues
* Access：
    * **ucanaccess**
        * **Issue**: Using **HSQLDB** as an in-memory database, **Spring Boot 3 does not support HSQLDB**.

## 2.8 Bean injection method: Method injection failed
* **Failure reason**: After AOT processing, dynamic proxies cannot be handled, and the proxy part will be defined before AOT (`/build/generated/aotXXX`).
    * Error message: **UnsatisfiedDependencyException**
        * Unexpected AOP exception
* **Solution**:
    1. `@Configuration(value = "CoreServiceConfig", proxyBeanMethods = false)` or `switch to @Component` >> **Bean Lite Mode**
        *  **Set proxyBeanMethods to false**: Disables method injection, preventing proxies from being created.
        * **Problem**: This approach results in multiple identical instances, and the additional instances are not managed by Spring.

## 2.9 Custom Internationalization Resource Failure
* **Cause**: Internationalization resources are **dynamically loaded**
    * Internationalization resources are only loaded when needed for internationalization processing.
    * **Reference**: g**etResourceBundle() in the ResourceBundleMessageSource class.**
* **Solution**: Refer to **MessageSourceAutoConfiguration** and **MessageSourceRuntimeHints**
    * Ensure that the resource files can be loaded at runtime.
    * **Alternative solution**: Set the **resource paths** in `resource-config.json`.
* **Example**: Add three resources
    ```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");
            }
        }
    }
    ```
    * In **registerHints**, the path does not need to include classpath, and it should start with the file name (`relative path`)

## 2.10 Use Gson or ObjectMapper for conversion.
* **Situation**: When using **Gson** or **ObjectMapper** to convert between Java objects and JSON strings, if the object **is not configured for AOT**, the following issues occur:
    * **Gson**: Converts the Entity to an empty **{}** string.
    * **ObjectMapper**: Throws an exception
* **Cause**: **Gson** or **ObjectMapper** uses reflection (`dynamically loading class information at runtime`). However, after compilation, **class information cannot be dynamically loaded**. Therefore, it is necessary to define the `fields`, `methods`, and `information` of the **class** in advance.
* Configuration Example: Teacher.class
    * Class
        ```java=
        @Data
        public Class Teacher{
            private String id;
            private String name;
            private String sex;    
        }
        ```
    * **Configuration**：`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"
                ]
              }
            ]
          }
        ]
        ```
* **Reference URL**：https://www.graalvm.org/latest/reference-manual/native-image/dynamic-features/Reflection/#configuration-with-features