SpringBoot3.0新特性嚐鮮,秒啟動的快感!SpringAOT與RuntimeHints來了解一下?

👉 這是一個或許對你有用的社群
🐱 一對一交流/面試小冊/簡歷最佳化/求職解惑,歡迎加入芋道快速開發平臺知識星球。下面是星球提供的部分資料:
👉這是一個或許對你有用的開源專案
國產 Star 破 10w+ 的開源專案,前端包括管理後臺 + 微信小程式,後端支援單體和微服務架構。
功能涵蓋 RBAC 許可權、SaaS 多租戶、資料許可權、商城、支付、工作流、大屏報表、微信公眾號、ERPCRMAI 大模型等等功能:
  • Boot 多模組架構:https://gitee.com/zhijiantianya/ruoyi-vue-pro
  • Cloud 微服務架構:https://gitee.com/zhijiantianya/yudao-cloud
  • 影片教程:https://doc.iocoder.cn
【國內首批】支援 JDK 17/21 + SpringBoot 3.3、JDK 8/11 + Spring Boot 2.7 雙版本 

一、前置知識

1、官網

Spring6.0新特性:https://github.com/spring-projects/spring-framework/wiki/What%27s-New-in-Spring-Framework-6.x
SpringBoot3.0:https://docs.spring.io/spring-boot/docs/current/reference/html/

2、安裝GraalVM

下載地址:https://github.com/graalvm/graalvm-ce-builds/releases
按照jdk版本下載GraalVM。SpringBoot3.0必須要使用jdk17以上了。
安裝過程與普通的jdk安裝一樣。
安裝成功之後,使用java -version,可以看到VM是GraalVM了。

3、GraalVM的限制

GraalVM在編譯成二進位制可執行檔案時,需要確定該應用到底用到了哪些類、哪些方法、哪些屬性,從而把這些程式碼編譯為機器指令(也就是exe檔案)。
但是我們一個應用中某些類可能是動態生成的,也就是應用執行後才生成的,為了解決這個問題,GraalVM提供了配置的方式,比如我們可以在編譯時告訴GraalVM哪些方法會被反射呼叫,比如我們可以透過reflect-config.json來進行配置。

4、安裝maven

略,注意配置阿里雲加速。

5、背景

為了應對Serverless大環境,使Springboot專案快速啟動,所以才會推出AOT與直接編譯為位元組碼的功能。
因為Java程式執行,需要啟動虛擬機器,然後由虛擬機器將class位元組碼檔案編譯為機器指令,所以啟動過程比較慢。
而如果像C語言那樣,直接編譯為機器指令,會大大提高啟動速度,但是會丟失Java反射、動態代理等功能(有解決方案-RuntimeHints)。
而且Springboot3.0-AOT更是將Bean掃描階段提前到了編譯器,而不是啟動期間進行掃描,大大提高了啟動速度。
基於 Spring Boot + MyBatis Plus + Vue & Element 實現的後臺管理系統 + 使用者小程式,支援 RBAC 動態許可權、多租戶、資料許可權、工作流、三方登入、支付、簡訊、商城等功能
  • 專案地址:https://github.com/YunaiV/ruoyi-vue-pro
  • 影片教程:https://doc.iocoder.cn/video/

二、打包SpringBoot3.0

1、專案準備

<parent>
<groupId>

org.springframework.boot

</groupId>
<artifactId>

spring-boot-starter-parent

</artifactId>
<version>

3.2.5

</version>
<relativePath/><!-- lookup parent from repository -->
</parent>

<dependencies>
<dependency>
<groupId>

org.springframework.boot

</groupId>
<artifactId>

spring-boot-starter-web

</artifactId>
</dependency>

<dependency>
<groupId>

org.springframework.boot

</groupId>
<artifactId>

spring-boot-starter-test

</artifactId>
<scope>

test

</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>

org.graalvm.buildtools

</groupId>
<artifactId>

native-maven-plugin

</artifactId>
</plugin>
<plugin>
<groupId>

org.springframework.boot

</groupId>
<artifactId>

spring-boot-maven-plugin

</artifactId>
</plugin>
</plugins>
</build>

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication

public class DemoApplication {
    public static void main(String[] args) {

        SpringApplication.run(DemoApplication.class, args);

    }
}
import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.RestController;
@RestController

public class Controller {
    @GetMapping("/demo1")

    public String demo1() {

        return "hello world";

    }

}

2、打包

> 基於 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 實現的後臺管理系統 + 使用者小程式,支援 RBAC 動態許可權、多租戶、資料許可權、工作流、三方登入、支付、簡訊、商城等功能

>

> * 專案地址:<https://github.com/YunaiV/yudao-cloud>

> * 影片教程:<https://doc.iocoder.cn/video/>

# 注意需要使用GraalVM環境,需要有C語言環境 ,最好使用linux系統

mvn -Pnative native:compile

打包過程會久一些。
會將可執行檔案、jar包都打出來:
執行demo:僅僅幾十毫秒就能啟動!!!
使用jar包執行:很明顯會慢很多!!

3、打包成docker

# 打包成docker

mvn -Pnative spring-boot:build-image
docker run --rm -p 8080:8080 demo

# 如果要傳引數,可以透過-e

docker run --rm -p 8080:8080 -e methodName=

test

 demo

# 不過程式碼中,得透過以下程式碼獲取:

String methodName = System.getenv(

"methodName"

)

#也可以使用Environment獲取,注入Environment 

environment.getProperty(

"methodName"

);

注意,打包的過程會下載java環境映象,比較慢。
僅僅幾十毫秒就可以啟動一個簡單的Springboot專案!

三、認識AOT

1、RuntimeHints

假設以下程式碼:
@Component
publicclassUserService

{

public String test()

{
        String result = 

""

;

try

 {

            Method test = MyService

.class.getMethod("test", null)

;

            result = (String) test.invoke(MyService

.class.newInstance(), null)

;

        } 

catch

 (NoSuchMethodException e) {

thrownew

 RuntimeException(e);

        } 

catch

 (InvocationTargetException e) {

thrownew

 RuntimeException(e);

        } 

catch

 (IllegalAccessException e) {

thrownew

 RuntimeException(e);

        } 

catch

 (InstantiationException e) {

thrownew

 RuntimeException(e);

        }

return

 result;

    }

}

在MyService中,透過反射的方式使用到了MyService的無參構造方法(MyService.class.newInstance()),如果我們不做任何處理,那麼打成二進位制可執行檔案後是執行不了的,可執行檔案中是沒有MyService的無參構造方法的,會報方法找不到的錯誤。
我們可以使用Spring提供的Runtime Hints機制來間接的配置reflect-config.json

2、RuntimeHintsRegistrar

提供一個RuntimeHintsRegistrar介面的實現類,並匯入到Spring容器中就可以了:
@Component
@ImportRuntimeHints

(UserService.MyServiceRuntimeHints

.

class

)

publicclassUserService

{

public String test()

{
        String result = 

""

;

try

 {

            Method test = MyService

.class.getMethod("test", null)

;

            result = (String) test.invoke(MyService

.class.newInstance(), null)

;

        } 

catch

 (NoSuchMethodException e) {

thrownew

 RuntimeException(e);

        } 

catch

 (InvocationTargetException e) {

thrownew

 RuntimeException(e);

        } 

catch

 (IllegalAccessException e) {

thrownew

 RuntimeException(e);

        } 

catch

 (InstantiationException e) {

thrownew

 RuntimeException(e);

        }

return

 result;

    }

staticclassMyServiceRuntimeHintsimplementsRuntimeHintsRegistrar

{

@Override
publicvoidregisterHints(RuntimeHints hints, ClassLoader classLoader)

{

try

 {

                hints.reflection().registerConstructor(MyService

.class.getConstructor(), ExecutableMode.INVOKE)

;

            } 

catch

 (NoSuchMethodException e) {

thrownew

 RuntimeException(e);

            }

        }

    }

}

3、@RegisterReflectionForBinding

// 該類的所有方法都會編譯為機器碼
@RegisterReflectionForBinding

(MyService

.

class

)

publicStringtest

()

{
    String result = 

""

;

try

 {

        Method test = MyService

.class.getMethod("test", null)

;

        result = (String) test.invoke(MyService

.class.newInstance(), null)

;

    } 

catch

 (NoSuchMethodException e) {

thrownew

 RuntimeException(e);

    } 

catch

 (InvocationTargetException e) {

thrownew

 RuntimeException(e);

    } 

catch

 (IllegalAccessException e) {

thrownew

 RuntimeException(e);

    } 

catch

 (InstantiationException e) {

thrownew

 RuntimeException(e);

    }

return

 result;

}

4、@ImportRuntimeHints

注意:如果程式碼中的methodName是透過引數獲取的,那麼GraalVM在編譯時就不能知道到底會使用到哪個方法,那麼test方法也要利用RuntimeHints來進行配置。
@Component
@ImportRuntimeHints

(MyService.MyServiceRuntimeHints

.

class

)

publicclassUserService

{

public String test()

{
        String methodName = System.getProperty(

"methodName"

);
        String result = 

""

;

try

 {

            Method test = MyService

.class.getMethod(methodNamenull)

;

            result = (String) test.invoke(MyService

.class.newInstance(), null)

;

        } 

catch

 (NoSuchMethodException e) {

thrownew

 RuntimeException(e);

        } 

catch

 (InvocationTargetException e) {

thrownew

 RuntimeException(e);

        } 

catch

 (IllegalAccessException e) {

thrownew

 RuntimeException(e);

        } 

catch

 (InstantiationException e) {

thrownew

 RuntimeException(e);

        }

return

 result;

    }

staticclassMyServiceRuntimeHintsimplementsRuntimeHintsRegistrar

{

@Override
publicvoidregisterHints(RuntimeHints hints, ClassLoader classLoader)

{

try

 {

                hints.reflection().registerConstructor(MyService

.class.getConstructor(), ExecutableMode.INVOKE)

;

                hints.reflection().registerMethod(MyService

.class.getMethod("test"), ExecutableMode.INVOKE)

;

            } 

catch

 (NoSuchMethodException e) {

thrownew

 RuntimeException(e);

            }

        }

    }

}

5、使用JDK動態代理也需要配置

public String test()throws ClassNotFoundException 

{
    String className = System.getProperty(

"className"

);

 Class<?> aClass = Class.forName(className);
 Object o = Proxy.newProxyInstance(UserService

.class.getClassLoader(), newClass[]

{aClass}, 

new

 InvocationHandler() {

@Override
public Object invoke(Object proxy, Method method, Object[] args)throws Throwable 

{

return

 method.getName();

     }

 });

return

 o.toString();

}

也可以利用RuntimeHints來進行配置要代理的介面:
publicvoidregisterHints(RuntimeHints hints, ClassLoader classLoader)

{

    hints.proxies().registerJdkProxy(UserInterface

.class)

;

}

6、@Reflective

對於反射用到的地方,我們可以直接加一個@Reflective,前提是MyService得是一個Bean:
@Component
publicclassMyService

{

@Reflective
publicMyService()

{

    }

@Reflective
public String test()

{

return"hello"

;

    }

}

以上Spring6提供的RuntimeHints機制,我們可以使用該機制更方便的告訴GraalVM我們額外用到了哪些類、介面、方法等資訊,最終Spring會生成對應的reflect-config.jsonproxy-config.json中的內容,GraalVM就知道了。

四、AOT的原理

1、外掛執行邏輯

我們執行mvn -Pnative native:compile時,實際上執行的是外掛native-maven-plugin的邏輯。
會先編譯我們自己的java程式碼,然後執行ProcessAotMojo.executeAot()方法(會生成一些Java檔案並編譯成class檔案,以及GraalVM的配置檔案),然後才執行利用GraalVM打包出二進位制可執行檔案。
maven外掛在編譯的時候,就會呼叫到executeAot()這個方法,這個方法會:
  • 先執行org.springframework.boot.SpringApplicationAotProcessor的main方法
  • 從而執行SpringApplicationAotProcessorprocess()
  • 從而執行ContextAotProcessordoProcess(),從而會生成一些Java類並放在spring-aot/main/sources目錄下,詳情看後文
  • 然後把生成在spring-aot/main/sources目錄下的Java類進行編譯,並把對應class檔案放在專案的編譯目錄下target/classes
  • 然後把spring-aot/main/resources目錄下的graalvm配置檔案複製到target/classes
  • 然後把spring-aot/main/classes目錄下生成的class檔案複製到target/classes

2、AOT生成的類

AOT會提前啟動Spring容器,並執行Bean掃描的過程,將這個過程產生的所有BeanDefinition提前生成為Java檔案,如下那樣,所以,可以在編譯期間透過外掛生成BeanDefinition,而不是在啟動期間進行掃描。
我們看一下SpringApplication.run方法:
可以選擇以AOT的方式執行,可以跳過Bean掃描的過程。

3、使用

一般配合GraalVM使用,如果單獨使用的話,需要加上面那個引數開啟AOT,並且透過外掛進行打包。

4、原理圖


歡迎加入我的知識星球,全面提升技術能力。
👉 加入方式,長按”或“掃描”下方二維碼噢
星球的內容包括:專案實戰、面試招聘、原始碼解析、學習路線。
文章有幫助的話,在看,轉發吧。
謝謝支援喲 (*^__^*)

相關文章