Spring6.0新特性详解

Spring6.0新特性

一、Spring的发展阶段

在这里插入图片描述

二、AOT

  AOT是Spring6.0提供的一个新特性,Ahead of Time提前编译

image.png

1.AOT概述

1.1 JIT和AOT的关系

1.1.1 JIT

   JIT(Just-in-time) 动态编译,即时编译,也就是边运行边编译,也就是在程序运行时,动态生成代码,启动比较慢,编译时需要占用运行时的资源。

1.1.2 AOT

  AOTAhead Of Time 指的是运行前预先编译,AOT 编译能直接将源代码转化为机器码,内存占用低,启动速度快,可以无需 runtime 运行,直接将 runtime 静态链接至最终的程序中,但是无运行时性能加成,不能根据程序运行情况做进一步的优化,AOT 缺点就是在程序运行前编译会使程序安装的时间增加。

简而言之:
  JIT即时编译的是在程序的运行过程中,将字节码转换为可在硬件上直接运行的机器码,并部署至托管环境中的过程。而 AOT 编译指的则是,在程序运行之前,便将字节码转换为机器码的过程。

在这里插入图片描述

三、GraalVM

GraalVM即支持AOT也支持JIT。支持多种开发语言。

  Spring6 支持的 AOT 技术,这个 GraalVM 就是底层的支持,Spring 也对 GraalVM 本机映像提供了一流的支持。GraalVM 是一种高性能 JDK,旨在加速用 Java 和其他 JVM 语言编写的应用程序的执行,同时还为 JavaScript、Python 和许多其他流行语言提供运行时。 GraalVM 提供两种运行 Java 应用程序的方法:在 HotSpot JVM 上使用 Graal 即时 (JIT) 编译器或作为提前 (AOT) 编译的本机可执行文件。 GraalVM 的多语言能力使得在单个应用程序中混合多种编程语言成为可能,同时消除了外语调用成本。GraalVM 向 HotSpot Java 虚拟机添加了一个用 Java 编写的高级即时 (JIT) 优化编译器。

GraalVM 具有以下特性:

  1. 一种高级优化编译器,它生成更快、更精简的代码,需要更少的计算资源
  2. AOT 本机图像编译提前将 Java 应用程序编译为本机二进制文件,立即启动,无需预热即可实现最高性能
  3. Polyglot 编程在单个应用程序中利用流行语言的最佳功能和库,无需额外开销
  4. 高级工具在 Java 和多种语言中调试、监视、分析和优化资源消耗

3.1 GraalVM安装

3.1.1 下载GraalVM

点击此处即可进入下载GraalVM安装包页面

在这里插入图片描述

下载社区版本即可,点击此处即可进入选择相关版本页面

在这里插入图片描述

下载好后解压缩出来,得到如下目录文件

在这里插入图片描述

3.1.2 配置环境变量

  1. 编辑系统环境变量

  2. 添加或修改JAVA_HOME,如下图所示:
    在这里插入图片描述

  3. 检查是否配置成功
    在这里插入图片描述

3.1.3 安装native-image插件

由于jdk22社区版本中已经自带了native-image插件,因此不必使用 gu install native-image 命令手动安装了,同时在jdk22版本中已经移除了对gu命令的支持。
在这里插入图片描述

3.1.4 Native Image

  Native image(本地镜像)是一种在Java平台上构建本地应用程序的技术。它将Java应用程序编译成本地机器代码,以便在不需要Java虚拟机(JVM)的情况下运行。这使得应用程序可以更快地启动,更高效地执行,并且占用更少的内存。

  Native image使用GraalVM编译器技术,可以将Java应用程序转换为本地可执行文件,支持Windows、Linux和MacOS等多个操作系统平台。此外,Native image还可以将Java应用程序打包成单个可执行文件,从而方便部署和分发。

  使用Native image,开发人员可以将Java应用程序作为本地应用程序来构建和部署,从而获得更好的性能和更好的用户体验。

3.2 安装C++的编译环境

执行如下命令

native-image Hello

在这里插入图片描述

TIPS:
由于未安装C++编译环境,执行native-image hello 命令进行预编译时报错了

3.2.1 下载社区版本Visual Studio

点击此处进入下载社区版本Visual Studio页面

在这里插入图片描述

3.2.2 安装Visual Studio

在这里插入图片描述

  • 等待在线下载

在这里插入图片描述

在这里插入图片描述

  • 注意安装选项,然后继续等待
    在这里插入图片描述

在这里插入图片描述

  • 创建一个简单的Hello.java文件
public class Hello {
	public static void main(String[] args){
		System.out.println("Hello World ...");
	}
}
  • 执行如下命令进行编译
javac Hello.java

在这里插入图片描述

  • 执行如下命令生成.exe可执行文件
native-image Hello

在这里插入图片描述

  • 通过终端直接执行通过 native-image 命令生成的 hello.exe 文件即可

在这里插入图片描述

四、SpringBoot3.0实战

4.2 springboot3.0 & graalvm22

  我们也可以在SpringBoot项目中通过graalvm的AOT特性来提前编译我们的项目,下面来简单实验一下。

  • 新建一个gradle项目并添加相关依赖
plugins {
    id 'java'
    id 'org.springframework.boot' version '3.3.1'
    id 'io.spring.dependency-management' version '1.1.5'
    id 'com.netflix.dgs.codegen' version '6.2.1'
    id 'org.graalvm.buildtools.native' version '0.10.2'
}

group = 'com.xj.learning'
version = '1.0.0-SNAPSHOT'

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(22)
    }
}

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

ext {
    set('springModulithVersion', "1.2.1")
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-rest'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    developmentOnly 'org.springframework.boot:spring-boot-devtools'
    annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

dependencyManagement {
    imports {
        mavenBom "org.springframework.modulith:spring-modulith-bom:${springModulithVersion}"
    }
}

generateJava {
    schemaPaths = ["${projectDir}/src/main/resources/graphql-client"]
    packageName = 'com.xj.learning.springgraalvm22.codegen'
    generateClient = true
}

tasks.named('test') {
    useJUnitPlatform()
}

  • 编写简单的代码来测试效果
package com.xj.learning.springgraalvm22.controller;

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

@RestController
public class HelloController {
    @GetMapping("/meet")
    public String meet() {
        return "Hello, Nice to meet you!";
    }
}

  • 打开命令行终端 ,切换到工程目录下,执行如下命令进行编译
gradle clean nativeCompile
  • 编译成功后,就会在target目录下生成 .exe 文件

在这里插入图片描述

在这里插入图片描述

  • 编译成功

在这里插入图片描述

  • 双击执行exe文件即可,你将会发启动现速度相比Spring6之前的版本会快很多

在这里插入图片描述

五、RuntimeHints

  与常规 JVM 运行时相比,将应用程序作为本机映像运行需要额外的信息。例如,GraalVM 需要提前知道组件是否使用反射。同样,除非明确指定,否则类路径资源不会在本机映像中提供。因此,如果应用程序需要加载资源,则必须从相应的 GraalVM 原生图像配置文件中引用它。

APIRuntimeHints在运行时收集反射、资源加载、序列化和 JDK 代理的需求。

5.1. 案例分析

  • 创建一个普通的UserService 类
package com.xj.learning.springgraalvm22.service;

public class UserService {
    public String meet(){
        return "Hello, Nice to meet you!";
    }
}
  • 在控制器中通过反射来操作来获取UserService实例
    @GetMapping("/meet")
    public String meet(){
        String res;
        try {
            Class<UserService> userServiceClass = UserService.class;
            Method meetMethod = userServiceClass.getMethod("meet");
            res =  (String)meetMethod.invoke(userServiceClass.newInstance(),null);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return res;
    }
  • 通过命令编译为 .exe 文件

在这里插入图片描述

-运行该.exe文件,并通过浏览器发起请求

在这里插入图片描述

在HelloController中,通过反射的方式获取UserService的无参构造方法。如果就这样调用不做任何处理,那么生成二进制可执行文件后,在执行过程中便会报错。具体的报错信息如上图所示。

有两种解决这个问题方式:

  1. 使用jdk22中推荐的方式来使用反射
    在这里插入图片描述
  2. 通过 Runtime Hints 机制来处理

改写一下HelloController中的meet方法如下:

package com.xj.learning.springgraalvm22.controller;

import com.xj.learning.springgraalvm22.service.UserService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

@RestController
public class HelloController {
    @GetMapping("/meet")
    public String meet(){
        String res;
        try {
            Class<UserService> userServiceClass = UserService.class;
            Method meetMethod = userServiceClass.getMethod("meet");
            res =  (String)meetMethod.invoke(userServiceClass.getDeclaredConstructor().newInstance());
//            res =  (String)meetMethod.invoke(userServiceClass.newInstance(),null);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return res;
    }

}

再次执行 gradle clean nativeCompile 命令,重新生成.exe文件,并执行新生成的.exe文件,重新通过浏览器发起请求
在这里插入图片描述

5.2. RuntimeHintsRegistrar

官网提供的解决方案。我们自定义一个RuntimeHintsRegistrar接口的实现类,并把该实现类注入到Spring容器中

image.png

实现RuntimeHintsRegistrar

@RestController
@ImportRuntimeHints(HelloController.UserServiceRuntimeHints.class)
public class HelloController {

    @GetMapping("/meet")
    public String meet(){
        String res;
        try {
            Class<UserService> userServiceClass = UserService.class;
            Method meetMethod = userServiceClass.getMethod("meet");
            res =  (String)meetMethod.invoke(userServiceClass.newInstance(), null);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return res;
    }

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

六、SpringBoot3.0核心代码

6.1 SpringApplicationAotProcessor

  我们执行 gradle clean nativeCompile 时会执行GraalVM中的相关指令。最终会调用 SpringApplicationAotProcessor 中的main 方法来完成相关提前编译操作。

public static void main(String[] args) throws Exception {
	int requiredArgs = 6; // 调用main方法接收的有6个参数
	Assert.isTrue(args.length >= requiredArgs, () -> "Usage: " + SpringApplicationAotProcessor.class.getName() + " <applicationName> <sourceOutput> <resourceOutput> <classOutput> <groupId> <artifactId> <originalArgs...>");
		// 获取SpringBoot项目的入口class
	Class<?> application = Class.forName(args[0]);
	// 通过传递过来的参数完成相关生成目录的配置
	Settings settings = Settings.builder().sourceOutput(Paths.get(args[1])).resourceOutput(Paths.get(args[2]))
				.classOutput(Paths.get(args[3])).groupId((StringUtils.hasText(args[4])) ? args[4] : "unspecified")
				.artifactId(args[5]).build();
	String[] applicationArgs = (args.length > requiredArgs) ? Arrays.copyOfRange(args, requiredArgs, args.length)
				: new String[0];
	// 执行 process 方法
	new SpringApplicationAotProcessor(application, settings, applicationArgs).process();
}

进入 process() 方法中

public final T process() {
	try {
		// 设置状态
		System.setProperty(AOT_PROCESSING, "true");
		return doProcess(); // 处理的核心方法
	}
	finally {
		System.clearProperty(AOT_PROCESSING);
	}
}

进入 doProcess() 方法中

@Override
protected ClassName doProcess() {
	deleteExistingOutput(); // 删除已经存在的目录
	// 启动SpringBoot服务  但是不会做扫描bean
	GenericApplicationContext applicationContext = prepareApplicationContext(getApplicationClass());
	return performAotProcessing(applicationContext);
}

进入performAotProcessing(applicationContext) 方法

@Override
protected GenericApplicationContext prepareApplicationContext(Class<?> application) {
	return new AotProcessorHook(application).run(() -> {
		Method mainMethod = application.getMethod("main", String[].class);
		return ReflectionUtils.invokeMethod(mainMethod, null, new Object[] { this.applicationArgs });
	});
}

接下来将会执行启动类中的main方法,从而启动SpringBoot

package com.xj.learning.springgraalvm22;

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

@SpringBootApplication
public class SpringGraalvm22Application {
    public static void main(String[] args) {
        SpringApplication.run(SpringGraalvm22Application.class, args);
    }
}

在启动中创建Spring上下文对象时会做如下的处理
在这里插入图片描述

	protected ConfigurableApplicationContext createApplicationContext() {
		return this.applicationContextFactory.create(this.webApplicationType);
	}
	@Override
	public ConfigurableApplicationContext create(WebApplicationType webApplicationType) {
		return (webApplicationType != WebApplicationType.SERVLET) ? null : createContext();
	}

	private ConfigurableApplicationContext createContext() {
		if (!AotDetector.useGeneratedArtifacts()) {
			return new AnnotationConfigServletWebServerApplicationContext();
		}
		return new ServletWebServerApplicationContext();
	}

  如果没有使用AOT,便会实例化 AnnotationConfigServletWebServerApplicationContext,实例化过程中会有条件的注册多种 ConfigurationClassPostProcessor

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

从而会解析配置类,也会执行扫描逻辑,而如果使用了AOT,则会创建 ServletWebServerApplicationContext,它就是一个空容器,它里面没有 ConfigurationClassPostProcessor,所以后续不会触发扫描了

再回到performAotProcessing方法中

	protected ClassName performAotProcessing(GenericApplicationContext applicationContext) {
		FileSystemGeneratedFiles generatedFiles = createFileSystemGeneratedFiles();
		DefaultGenerationContext generationContext = new DefaultGenerationContext(
				createClassNameGenerator(), generatedFiles);
		ApplicationContextAotGenerator generator = new ApplicationContextAotGenerator();
		// 进行相关的扫描操作
		ClassName generatedInitializerClassName = generator.processAheadOfTime(applicationContext, generationContext);
		//如果有反射的注册信息。这里会完成相关信息的生成到reflect-config.json对应的RuntimeHints中去
		registerEntryPointHint(generationContext, generatedInitializerClassName);
		// 生成source目录下的Java文件
		generationContext.writeGeneratedContent();
		//  将RuntimeHints中的内容写入resource目录下的Graalvm的各个配置文件中
		writeHints(generationContext.getRuntimeHints());
		writeNativeImageProperties(getDefaultNativeImageArguments(getApplicationClass().getName()));
		return generatedInitializerClassName;
	}

processAheadOfTime中的逻辑

	public ClassName processAheadOfTime(GenericApplicationContext applicationContext,
			GenerationContext generationContext) {
		return withCglibClassHandler(new CglibClassHandler(generationContext), () -> {
			// 扫描处理
			applicationContext.refreshForAotProcessing(generationContext.getRuntimeHints());
			// 获取bean工厂对象
		DefaultListableBeanFactory beanFactory = applicationContext.getDefaultListableBeanFactory();
			ApplicationContextInitializationCodeGenerator codeGenerator =
					new ApplicationContextInitializationCodeGenerator(generationContext);
			new BeanFactoryInitializationAotContributions(beanFactory).applyTo(generationContext, codeGenerator);
			return codeGenerator.getGeneratedClass().getName();
		});
	}

进入到refreshForAotProcessing方法中

	public void refreshForAotProcessing(RuntimeHints runtimeHints) {
		if (logger.isDebugEnabled()) {
			logger.debug("Preparing bean factory for AOT processing");
		}
		prepareRefresh();
		obtainFreshBeanFactory(); // 获取工厂对象。并完成扫描操作
		prepareBeanFactory(this.beanFactory);
		postProcessBeanFactory(this.beanFactory);
		invokeBeanFactoryPostProcessors(this.beanFactory); // 之后工厂的后置处理器
		this.beanFactory.freezeConfiguration();
		PostProcessorRegistrationDelegate.invokeMergedBeanDefinitionPostProcessors(this.beanFactory);
		preDetermineBeanTypes(runtimeHints);
	}

BeanFactoryInitializationAotContributions构造方法的处理逻辑:

  1. 会读取aot.properties文件的加载器以
  2. 将BeanFactory封装成为一个Loader对象,然后传入父类构造器
	BeanFactoryInitializationAotContributions(DefaultListableBeanFactory beanFactory) {
		this(beanFactory, AotServices.factoriesAndBeans(beanFactory));
	}

image.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值