--- 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