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