Skip to content

5、Spring AOT提前优化

AOT之什么是AOT

一种编译方式的变革!旨在提升启动速度!

JIT,即Just-in-time,动态(即时)编译,运行时编译;

AOT,Ahead Of Time,指运行前编译,是两种程序的编译方式

20251206233401da8e482e5.png

AOT之什么是GraalVM

202512062334014c2c9cab3.png

GraalVM旨在加速Java应用程序的性能,同时消耗更少的资源。GraalVM提供了两种运行Java应用程序的方式:在HotSpot JVM上使用Graal即时编译器或作为预先编译的本地可执行文件(AOT)。除了Java,它还提供了JavaScript、Ruby、Python和许多其他流行语言的运行时。GraalVM的多语言能力使得可以在单个应用程序中混合编程语言,同时消除不同语言之间调用的成本。

GraalVM文章推荐:https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzI3MDI5MjI1Nw==&action=getalbum&album_id=2761361634840969217&scene=173&from_msgid=2247484273&from_itemidx=1&count=3&nolastread=1#wechat_redirect

GraalVM体验

下载压缩包

打开https://github.com/graalvm/graalvm-ce-builds/releases,按JDK版本下载GraalVM对应的压缩包,请下载Java 17对应的版本,不然后面运行SpringBoot3可能会有问题。

202512062334011f6e0aac9.png

windows的同学直接给大家:附件: graalvm-ce-java17-windows-amd64-22.3.0.zip

下载完后,就解压,

2025120623340167c2380da.png

配置环境变量

202512062334013a38f2bb6.png

新开一个cmd测试:

20251206233401bdffd2e5a.png

安装Visual Studio Build Tools

202512062334019e774b5b4.png

打开visualstudio.microsoft.com,下载Visual Studio Installer。

选择C++桌面开发,和Windows 11 SDK,然后进行下载和安装,安装后重启操作系统。

20251206233401c8bd06c0d.png

要使用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脚本来安装。

安装完之后其实就可以在 x****64 Native Tools Command Prompt for VS 2019中去使用native-image命令去进行编译了。

但是,如果后续在编译过程中编译失败了,出现以下错误:

2025120623340115db967f1.png

那么可以执行cl.exe,如果是中文,那就得修改为英文。20251206233401f9735c993.png

通过Visual Studio Installer来修改,比如:20251206233401ac965a283.png

可能一开始只选择了中文,手动选择英文,去掉中文,然后安装即可。

再次检查

202512062334018e0fc7566.png

这样就可以正常的编译了。

Hello World实战

新建一个简单的Java工程:20251206233401dd4a5d089.png

我们可以直接把graalvm当作普通的jdk的使用20251206233401dbf22bf17.png

我们也可以利用native-image命令来将字节码编译为二进制可执行文件。

打开x64 Native Tools Command Prompt for VS 2019,进入工程目录下,并利用javac将java文件编译为class文件:javac -d . src/com/xs/App.java20251206233401cbd0e6c46.png

此时的class文件因为有main方法,所以用java命令可以运行202512062334019051b7dc9.png

我们也可以利用native-image来编译:20251206233401eb5f0137a.png

编译完了之后就会在当前目录生成一个exe文件:

202512062334016cc5c9632.png

我们可以直接运行这个exe文件:

20251206233401717c5536c.png

并且运行这个exe文件是不需要操作系统上安装了JDK环境的。


我们可以使用-o参数来指定exe文件的名字:

java
native-image com.xs.App -o app

SpringBoot 3.0实战

202512062334015d261f703.png

然后新建一个Maven工程,添加SpringBoot依赖

xml
<parent>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-parent</artifactId>
	<version>3.0.0</version>
</parent>

以及SpringBoot的插件

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

以及一些代码

java
package com.xs;

import org.springframework.stereotype.Component;

@Component
public class UserService {

    public String test(){
        return "hello xushu";
    }
}
java
package com.xs;

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的路径。

编译完成截图:

20251206233401c3410be8f.png

20251206233401ce09ec13a.png

这样,我们就能够直接运行这个exe来启动我们的SpringBoot项目了。

GraalVM的限制

GraalVM在编译成二进制可执行文件时,需要确定该应用到底用到了哪些类、哪些方法、哪些属性,从而把这些代码编译为机器指令(也就是exe文件)。但是我们一个应用中某些类可能是动态生成的,也就是应用运行后才生成的,为了解决这个问题,GraalVM提供了配置的方式,比如我们可以在编译时告诉GraalVM哪些方法会被反射调用,比如我们可以通过reflect-config.json来进行配置。

通过RuntimeHints解决

假如应用中有如下代码:

java
/**
* 作者:徐庶
*/
public class XushuService {

    public String test(){
        return "xushu";
    }
}
java
@Component
public class UserService {

    public String test(){

        String result = "";
        try {
            Method test = XushuService.class.getMethod("test", null);
            result = (String) test.invoke(XushuService.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中,通过反射的方式使用到了XushuService的无参构造方法(XushuService.class.newInstance()),如果我们不做任何处理,那么打成二进制可执行文件后是运行不了的,可执行文件中是没有XushuService的无参构造方法的,会报如下错误:

20251206233401b3df8968b.png

我们可以通过Spring提供的Runtime Hints机制来间接的配置reflect-config.json。

方式一:RuntimeHintsRegistrar

提供一个RuntimeHintsRegistrar接口的实现类,并导入到Spring容器中就可以了:

java
@Component
@ImportRuntimeHints(UserService.XushuServiceRuntimeHints.class)
public class UserService {

    public String test(){

        String result = "";
        try {
            Method test = XushuService.class.getMethod("test", null);
            result = (String) test.invoke(XushuService.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 XushuServiceRuntimeHints implements RuntimeHintsRegistrar {

        @Override
        public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
            try {
                hints.reflection().registerConstructor(XushuService.class.getConstructor(), ExecutableMode.INVOKE);
            } catch (NoSuchMethodException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

方式二:@RegisterReflectionForBinding

java
@RegisterReflectionForBinding(XushuService.class)
public String test(){

    String result = "";
    try {
        Method test = XushuService.class.getMethod("test", null);
        result = (String) test.invoke(XushuService.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来进行配置。

java
@Component
@ImportRuntimeHints(UserService.XushuServiceRuntimeHints.class)
public class UserService {

    public String test(){

        String methodName = System.getProperty("methodName");

        String result = "";
        try {
            Method test = XushuService.class.getMethod(methodName, null);
            result = (String) test.invoke(XushuService.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 XushuServiceRuntimeHints implements RuntimeHintsRegistrar {

        @Override
        public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
            try {
                hints.reflection().registerConstructor(XushuService.class.getConstructor(), ExecutableMode.INVOKE);
                hints.reflection().registerMethod(XushuService.class.getMethod("test"), ExecutableMode.INVOKE);
            } catch (NoSuchMethodException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

或者使用了JDK动态代理:

java
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来进行配置要代理的接口:

java
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
    hints.proxies().registerJdkProxy(UserInterface.class);
}

方式三:@Reflective

对于反射用到的地方,我们可以直接加一个@Reflective,前提是XushuService得是一个Bean:

java
@Component
public class XushuService {

    @Reflective
    public XushuService() {
    }

    @Reflective
    public String test(){
        return "Xushu";
    }
}

以上Spring6提供的RuntimeHints机制,我们可以使用该机制更方便的告诉GraalVM我们额外用到了哪些类、接口、方法等信息,最终Spring会生成对应的reflect-config.json、proxy-config.json中的内容,GraalVM就知道了。

Docker SpringBoot3.0 实战

我们可以直接把SpringBoot应用对应的本地可执行文件构建为一个Docker镜像,这样就能跨操作系统运行了。

Buildpacks,类似Dockerfile的镜像构建技术

注意要安装docker,并启动docker

注意这种方式并不要求你机器上安装了GraalVM,会由SpringBoot插件利用/paketo-buildpacks/native-image来生成本地可执行文件,然后打入到容器中

Docker镜像名字中不能有大写字母,我们可以配置镜像的名字:

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

然后执行:

xml
mvn -Pnative spring-boot:build-image

来生成Docker镜像,成功截图:

20251206233401bb9f23bba.png

执行完之后,就能看到docker镜像了:

20251206233401db9edacc1.png

然后就可以运行容器了:

xml
docker run --rm -p 8080:8080 springboot3demo

如果要传参数,可以通过-e

java
docker run --rm -p 8080:8080 -e methodName=test springboot3demo

不过代码中,得通过以下代码获取:

java
String methodName = System.getenv("methodName")

建议工作中直接使用Environment来获取参数:

202512062334016c4662b53.png

更新: 2024-12-23 16:37:13
原文: https://www.yuque.com/tulingzhouyu/sfx8p0/kyo9smam2fqvnryc