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