https://github.com/spring-projects/spring-framework/wiki/What%27s-New-in-Spring-Framework-6.x
最核心的就是Spring AOT。
GraalVM體驗
下載壓縮包
打開https://github.com/graalvm/graalvm-ce-builds/releases,按JDK版本下載GraalVM對應的壓縮包,請下載Java 17對應的版本,不然後面運行SpringBoot3可能會有問題。
windows的直接給大家:📎graalvm-ce-java17-windows-amd64-22.3.0.zip
下載完後,就解壓,
配置環境變量
新開一個cmd測試:
安裝Visual Studio Build Tools
因爲需要C語言環境,所以需要安裝Visual Studio Build Tools。
打開visualstudio.microsoft.com,下載Visual Studio Installer。
選擇C++桌面開發,和Windows 11 SDK,然後進行下載和安裝,安裝後重啓操作系統。
要使用GraalVM,不能使用普通的windows自帶的命令行窗口,得使用VS提供的 x64 Native Tools Command Prompt for VS 2019,如果沒有可以執行C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools\VC\Auxiliary\Build\vcvars64.bat
腳本來安裝。
安裝完之後其實就可以在 x64 Native Tools Command Prompt for VS 2019中去使用native-image
命令去進行編譯了。
但是,如果後續在編譯過程中編譯失敗了,出現以下錯誤:
那麼可以執行cl.exe,如果是中文,那就得修改爲英文。
通過Visual Studio Installer來修改,比如:
可能一開始只選擇了中文,手動選擇英文,去掉中文,然後安裝即可。
再次檢查
這樣就可以正常的編譯了。
Hello World實戰
新建一個簡單的Java工程:
我們可以直接把graalvm當作普通的jdk的使用
我們也可以利用native-image命令來將字節碼編譯爲二進制可執行文件。
打開x64 Native Tools Command Prompt for VS 2019,進入工程目錄下,並利用javac將java文件編譯爲class文件:javac -d . src/com/zhouyu/App.java
此時的class文件因爲有main方法,所以用java命令可以運行
我們也可以利用native-image來編譯:
編譯需要一些些。。。。。。。時間。
編譯完了之後就會在當前目錄生成一個exe文件:
我們可以直接運行這個exe文件:
並且運行這個exe文件是不需要操作系統上安裝了JDK環境的。
我們可以使用-o參數來指定exe文件的名字:
native-image com.zhouyu.App -o app
GraalVM的限制
GraalVM在編譯成二進制可執行文件時,需要確定該應用到底用到了哪些類、哪些方法、哪些屬性,從而把這些代碼編譯爲機器指令(也就是exe文件)。但是我們一個應用中某些類可能是動態生成的,也就是應用運行後才生成的,爲了解決這個問題,GraalVM提供了配置的方式,比如我們可以在編譯時告訴GraalVM哪些方法會被反射調用,比如我們可以通過reflect-config.json來進行配置。
SpringBoot 3.0實戰
然後新建一個Maven工程,添加SpringBoot依賴
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.0.0</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>
以及SpringBoot的插件
<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>
以及一些代碼
@RestController public class ZhouyuController { @Autowired private UserService userService; @GetMapping("/demo") public String test() { return userService.test(); } }
package com.zhouyu; import org.springframework.stereotype.Component; @Component public class UserService { public String test(){ return "hello zhouyu"; } }
package com.zhouyu; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class MyApplication { public static void main(String[] args) { SpringApplication.run(MyApplication.class, args); } }
這本身就是一個普通的SpringBoot工程,所以可以使用我們之前的方式使用,同時也支持利用native-image命令把整個SpringBoot工程編譯成爲一個exe文件。
同樣在 x64 Native Tools Command Prompt for VS 2019中,進入到工程目錄下,執行mvn -Pnative native:compile
進行編譯就可以了,就能在target下生成對應的exe文件,後續只要運行exe文件就能啓動應用了。
在執行命令之前,請確保環境變量中設置的時graalvm的路徑。
編譯完成截圖:
這樣,我們就能夠直接運行這個exe來啓動我們的SpringBoot項目了。
Docker SpringBoot3.0 實戰
我們可以直接把SpringBoot應用對應的本地可執行文件構建爲一個Docker鏡像,這樣就能跨操作系統運行了。
Buildpacks,類似Dockerfile的鏡像構建技術
注意要安裝docker,並啓動docker
注意這種方式並不要求你機器上安裝了GraalVM,會由SpringBoot插件利用/paketo-buildpacks/native-image來生成本地可執行文件,然後打入到容器中
Docker鏡像名字中不能有大寫字母,我們可以配置鏡像的名字:
<properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <spring-boot.build-image.imageName>springboot3demo</spring-boot.build-image.imageName> </properties>
然後執行:
mvn -Pnative spring-boot:build-image
來生成Docker鏡像,成功截圖:
執行完之後,就能看到docker鏡像了:
然後就可以運行容器了:
docker run --rm -p 8080:8080 springboot3demo
如果要傳參數,可以通過-e
docker run --rm -p 8080:8080 -e methodName=test springboot3demo
不過代碼中,得通過以下代碼獲取:
String methodName = System.getenv("methodName")
建議工作中直接使用Environment來獲取參數:
RuntimeHints
假如應用中有如下代碼:
public class ZhouyuService { public String test(){ return "zhouyu"; } }
@Component public class UserService { public String test(){ String result = ""; try { Method test = ZhouyuService.class.getMethod("test", null); result = (String) test.invoke(ZhouyuService.class.newInstance(), null); } catch (NoSuchMethodException e) { throw new RuntimeException(e); } catch (InvocationTargetException e) { throw new RuntimeException(e); } catch (IllegalAccessException e) { throw new RuntimeException(e); } catch (InstantiationException e) { throw new RuntimeException(e); } return result; } }
在UserService中,通過反射的方式使用到了ZhouyuService的無參構造方法(ZhouyuService.class.newInstance()),如果我們不做任何處理,那麼打成二進制可執行文件後是運行不了的,可執行文件中是沒有ZhouyuService的無參構造方法的,會報如下錯誤:
我們可以通過Spring提供的Runtime Hints機制來間接的配置reflect-config.json。
方式一:RuntimeHintsRegistrar
提供一個RuntimeHintsRegistrar接口的實現類,並導入到Spring容器中就可以了:
@Component @ImportRuntimeHints(UserService.ZhouyuServiceRuntimeHints.class) public class UserService { public String test(){ String result = ""; try { Method test = ZhouyuService.class.getMethod("test", null); result = (String) test.invoke(ZhouyuService.class.newInstance(), null); } catch (NoSuchMethodException e) { throw new RuntimeException(e); } catch (InvocationTargetException e) { throw new RuntimeException(e); } catch (IllegalAccessException e) { throw new RuntimeException(e); } catch (InstantiationException e) { throw new RuntimeException(e); } return result; } static class ZhouyuServiceRuntimeHints implements RuntimeHintsRegistrar { @Override public void registerHints(RuntimeHints hints, ClassLoader classLoader) { try { hints.reflection().registerConstructor(ZhouyuService.class.getConstructor(), ExecutableMode.INVOKE); } catch (NoSuchMethodException e) { throw new RuntimeException(e); } } } }
方式二:@RegisterReflectionForBinding
@RegisterReflectionForBinding(ZhouyuService.class) public String test(){ String result = ""; try { Method test = ZhouyuService.class.getMethod("test", null); result = (String) test.invoke(ZhouyuService.class.newInstance(), null); } catch (NoSuchMethodException e) { throw new RuntimeException(e); } catch (InvocationTargetException e) { throw new RuntimeException(e); } catch (IllegalAccessException e) { throw new RuntimeException(e); } catch (InstantiationException e) { throw new RuntimeException(e); } return result; }
注意
如果代碼中的methodName是通過參數獲取的,那麼GraalVM在編譯時就不能知道到底會使用到哪個方法,那麼test方法也要利用RuntimeHints來進行配置。
@Component @ImportRuntimeHints(UserService.ZhouyuServiceRuntimeHints.class) public class UserService { public String test(){ String methodName = System.getProperty("methodName"); String result = ""; try { Method test = ZhouyuService.class.getMethod(methodName, null); result = (String) test.invoke(ZhouyuService.class.newInstance(), null); } catch (NoSuchMethodException e) { throw new RuntimeException(e); } catch (InvocationTargetException e) { throw new RuntimeException(e); } catch (IllegalAccessException e) { throw new RuntimeException(e); } catch (InstantiationException e) { throw new RuntimeException(e); } return result; } static class ZhouyuServiceRuntimeHints implements RuntimeHintsRegistrar { @Override public void registerHints(RuntimeHints hints, ClassLoader classLoader) { try { hints.reflection().registerConstructor(ZhouyuService.class.getConstructor(), ExecutableMode.INVOKE); hints.reflection().registerMethod(ZhouyuService.class.getMethod("test"), ExecutableMode.INVOKE); } catch (NoSuchMethodException e) { throw new RuntimeException(e); } } } }
或者使用了JDK動態代理:
public String test() throws ClassNotFoundException { String className = System.getProperty("className"); Class<?> aClass = Class.forName(className); Object o = Proxy.newProxyInstance(UserService.class.getClassLoader(), new Class[]{aClass}, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { return method.getName(); } }); return o.toString(); }
那麼也可以利用RuntimeHints來進行配置要代理的接口:
public void registerHints(RuntimeHints hints, ClassLoader classLoader) { hints.proxies().registerJdkProxy(UserInterface.class); }
方式三:@Reflective
對於反射用到的地方,我們可以直接加一個@Reflective,前提是ZhouyuService得是一個Bean:
@Component public class ZhouyuService { @Reflective public ZhouyuService() { } @Reflective public String test(){ return "zhouyu"; } }
以上Spring6提供的RuntimeHints機制,我們可以使用該機制更方便的告訴GraalVM我們額外用到了哪些類、接口、方法等信息,最終Spring會生成對應的reflect-config.json、proxy-config.json中的內容,GraalVM就知道了。
Spring AOT的源碼實現
流程圖:https://www.processon.com/view/link/63edeea8440e433d3d6a88b2
SpringBoot 3.0插件實現原理
上面的SpringBoot3.0實戰過程中,我們在利用image-native編譯的時候,target目錄下會生成一個spring-aot文件夾:
這個spring-aot文件夾是編譯的時候spring boot3.0的插件生成的,resources/META-INF/native-image文件夾中的存放的就是graalvm的配置文件。
當我們執行mvn -Pnative native:compile
時,實際上執行的是插件native-maven-plugin的邏輯。
我們可以執行mvn help:describe -Dplugin=org.graalvm.buildtools:native-maven-plugin -Ddetail
來查看這個插件的詳細信息。
發現native:compile命令對應的實現類爲NativeCompileMojo,並且會先執行package這個命令,從而會執行process-aot命令,因爲spring-boot-maven-plugin插件中有如下配置:
我們可以執行mvn help:describe -Dplugin=org.springframework.boot:spring-boot-maven-plugin -Ddetail
發現對應的phase爲:prepare-package,所以會在打包之前執行ProcessAotMojo。
所以,我們在運行mvn -Pnative native:compile
時,會先編譯我們自己的java代碼,然後執行executeAot()方法(會生成一些Java文件並編譯成class文件,以及GraalVM的配置文件),然後才執行利用GraalVM打包出二進制可執行文件。
對應的源碼實現:
maven插件在編譯的時候,就會調用到executeAot()這個方法,這個方法會:
- 先執行org.springframework.boot.SpringApplicationAotProcessor的main方法
- 從而執行SpringApplicationAotProcessor的process()
- 從而執行ContextAotProcessor的doProcess(),從而會生成一些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
Spring AOT核心原理
以下只是一些關鍵源碼
prepareApplicationContext會直接啓動我們的SpringBoot,並在觸發contextLoaded事件後,返回所創建的Spring對象,注意此時還沒有掃描Bean。
protected ClassName performAotProcessing(GenericApplicationContext applicationContext) { FileSystemGeneratedFiles generatedFiles = createFileSystemGeneratedFiles(); DefaultGenerationContext generationContext = new DefaultGenerationContext(createClassNameGenerator(), generatedFiles); ApplicationContextAotGenerator generator = new ApplicationContextAotGenerator(); // 會進行掃描,並且根據掃描得到的BeanDefinition生成對應的Xx_BeanDefinitions.java文件 // 並返回com.zhouyu.MyApplication__ApplicationContextInitializer ClassName generatedInitializerClassName = generator.processAheadOfTime(applicationContext, generationContext); // 因爲後續要通過反射調用com.zhouyu.MyApplication__ApplicationContextInitializer的構造方法 // 所以將相關信息添加到reflect-config.json對應的RuntimeHints中去 registerEntryPointHint(generationContext, generatedInitializerClassName); // 生成source目錄下的Java文件 generationContext.writeGeneratedContent(); // 將RuntimeHints中的內容寫入resource目錄下的Graalvm的各個配置文件中 writeHints(generationContext.getRuntimeHints()); writeNativeImageProperties(getDefaultNativeImageArguments(getApplicationClass().getName())); return generatedInitializerClassName; }
public ClassName processAheadOfTime(GenericApplicationContext applicationContext, GenerationContext generationContext) { return withCglibClassHandler(new CglibClassHandler(generationContext), () -> { // 會進行掃描,並找到beanType是代理類的請求,把代理類信息設置到RuntimeHints中 applicationContext.refreshForAotProcessing(generationContext.getRuntimeHints()); // 拿出Bean工廠,掃描得到的BeanDefinition對象在裏面 DefaultListableBeanFactory beanFactory = applicationContext.getDefaultListableBeanFactory(); ApplicationContextInitializationCodeGenerator codeGenerator = new ApplicationContextInitializationCodeGenerator(generationContext); // 核心 new BeanFactoryInitializationAotContributions(beanFactory).applyTo(generationContext, codeGenerator); return codeGenerator.getGeneratedClass().getName(); }); }
BeanFactoryInitializationAotContributions(DefaultListableBeanFactory beanFactory) { // 把aot.factories文件的加載器以及BeanFactory,封裝成爲一個Loader對象,然後傳入 this(beanFactory, AotServices.factoriesAndBeans(beanFactory)); }
BeanFactoryInitializationAotContributions(DefaultListableBeanFactory beanFactory, AotServices.Loader loader) { // getProcessors()中會從aot.factories以及beanfactory中拿出BeanFactoryInitializationAotProcessor類型的Bean對象 // 同時還會添加一個RuntimeHintsBeanFactoryInitializationAotProcessor this.contributions = getContributions(beanFactory, getProcessors(loader)); }
private List<BeanFactoryInitializationAotContribution> getContributions( DefaultListableBeanFactory beanFactory, List<BeanFactoryInitializationAotProcessor> processors) { List<BeanFactoryInitializationAotContribution> contributions = new ArrayList<>(); // 逐個調用BeanFactoryInitializationAotProcessor的processAheadOfTime()開始處理 for (BeanFactoryInitializationAotProcessor processor : processors) { BeanFactoryInitializationAotContribution contribution = processor.processAheadOfTime(beanFactory); if (contribution != null) { contributions.add(contribution); } } return Collections.unmodifiableList(contributions); }
總結一下,在SpringBoot項目編譯時,最終會通過BeanFactoryInitializationAotProcessor來生成Java文件,或者設置RuntimeHints,後續會把寫入Java文件到磁盤,將RuntimeHints中的內容寫入GraalVM的配置文件,再後面會編譯Java文件,再後面就會基於生成出來的GraalVM配置文件打包出二進制可執行文件了。
所以我們要看Java文件怎麼生成的,RuntimeHints如何收集的就看具體的BeanFactoryInitializationAotProcessor就行了。
比如:
- 有一個BeanRegistrationsAotProcessor,它就會負責生成Xx_BeanDefinition.java以及Xx__ApplicationContextInitializer.java、Xx__BeanFactoryRegistrations.java中的內容
- 還有一個RuntimeHintsBeanFactoryInitializationAotProcessor,它負責從aot.factories文件以及BeanFactory中獲取RuntimeHintsRegistrar類型的對象,以及會找到@ImportRuntimeHints所導入的RuntimeHintsRegistrar對象,最終就是從這些RuntimeHintsRegistrar中設置RuntimeHints。
Spring Boot3.0啓動流程
在run()方法中,SpringBoot會創建一個Spring容器,但是SpringBoot3.0中創建容器邏輯爲:
private ConfigurableApplicationContext createContext() { if (!AotDetector.useGeneratedArtifacts()) { return new AnnotationConfigServletWebServerApplicationContext(); } return new ServletWebServerApplicationContext(); }
如果沒有使用AOT,那麼就會創建AnnotationConfigServletWebServerApplicationContext,它裏面會添加ConfigurationClassPostProcessor,從而會解析配置類,從而會掃描。
而如果使用了AOT,則會創建ServletWebServerApplicationContext,它就是一個空容器,它裏面沒有ConfigurationClassPostProcessor,所以後續不會觸發掃描了。
創建完容器後,就會找到MyApplication__ApplicationContextInitializer,開始向容器中註冊BeanDefinition。
後續就是創建Bean對象了。
本系列文章來自圖靈學院周瑜老師分享,本博客整理學習並搬運