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";
}
}
在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的源码实现
流程图:
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对象了。