第三十九讲-Spring Boot启动过程分析(非常重要)
第三十九讲-Spring Boot启动过程分析(非常重要)
接下来我们来了解一下Spring Boot的启动流程。
Spring Boot的启动划分为两个阶段:
- 创建一个SpringApplication对象
- 调用SpringApplication对象的run方法,这其中包括12大步骤和7个事件
接下来我们详细的看一下Spring Boot的启动过程!
我们先回忆一下一个Spring Boot应用程序是如何启动的?我们前面讲过,这里呢我们通过改代码简要的复述一下:
@SpringBootApplication
public class A39_1 {
public static void main(String[] args) {
SpringApplication.run(A39_1.class, args);
}
}
这其中的run
方法到底做了些什么呢?我们跟进去看一下:
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
return new SpringApplication(primarySources).run(args);
}
我们发现调用run方法时创建了一个SpringApplication
对象,并调用了该对象的run
方法。下面我们具体的分析一下SpringApplication构造方法做了哪些事情。
1. SpringApplication构造方法
我们来看一下SpringApplication的构造方法:
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
this.webApplicationType = WebApplicationType.deduceFromClasspath();
this.bootstrapRegistryInitializers = new ArrayList<>(
getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
this.mainApplicationClass = deduceMainApplicationClass();
}
上面的代码可读性不太好,这里呢转为更可读的注释来读一下:
System.out.println("1.演示获取Bean Definition源");
System.oUt.println("2.演示推断应用类型");
System.oUt.println("3.演示ApplicationContext初始化器");
System.oUt.println("4.演示监听器与事件");
System.out.println("5.演示主类推断");
第一步:我们知道,刚开始Spring容器中是空的,它要从各个源头找到一些BeanDefinition,这些源有的来自配置类,有的来自于XML配置文件等等各种各样的源。Spring容器需要首先获取一个主源,这个主源,就是Spring的引导类。
第二步:是推断应用类型,就是Spring Boot程序一共支持三种应用类型:
-
第一种是非WEB应用程序
-
第二种是基于Servlet的WEB应用程序
-
第三种是基于Reactive的WEB应用程序
Spring Boot会根据当前类路径下的jar包的关键类来判断该应用程序到底是哪一种应用程序。且根据不同类型的应用程序创建不同的ApplicationContext。
第三步:添加ApplicationContext的初始化器,当我们把BeanDefinition源和应用程序类型都准备好了,就可以创建出Spring容器了,那么Spring容器创建出来以后可能会对容器做一些扩展工作,这个扩展工作就是由ApplicationContext初始化器来完成的。
第四步:添加监听器和事件,监听器用来监听Spring Boot启动过程中的一些重要事件
第五步:执行主类推断,主类推断就是将来Spring Boot执行main方法所在的类是哪一个?
当然,SpringApplication的构造方法创建出来的对象并不会创建Spring容器,真正创建容器是在SpringApplication对象执行run方法时创建出来的!
接下来我们演示这几步:
1.1 获取BeanDefinition源
@Configuration
public class A39_1 {
public static void main(String[] args) throws Exception {
System.out.println("1. 演示获取 Bean Definition 源");
SpringApplication spring = new SpringApplication(A39_1.class);
// 设置多个源
spring.setSources(Set.of("classpath:b01.xml"));
ConfigurableApplicationContext context = spring.run(args);
// 创建 ApplicationContext
for (String name : context.getBeanDefinitionNames()) {
System.out.println("name: " + name + " 来源:" + context.getBeanFactory().getBeanDefinition(name).getResourceDescription());
}
context.close();
}
static class Bean1 { }
@Bean
public Bean2 bean2() { return new Bean2();}
@Bean
public TomcatServletWebServerFactory servletWebServerFactory() {
return new TomcatServletWebServerFactory();
}
}
name: a39_1 来源:null
name: bean1 来源:class path resource [b01.xml]
name: org.springframework.boot.autoconfigure.internalCachingMetadataReaderFactory 来源:null
name: bean2 来源:com.cherry.a39boot.A39_1
name: servletWebServerFactory 来源:com.cherry.a39boot.A39_1
1.2 推断应用类型
接下来我们演示一下SpringBoot如何推断应用类型的,我们首先看一下SpringApplication的相关源码:
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
// deduceFromClasspath:判断应用程序的类型
this.webApplicationType = WebApplicationType.deduceFromClasspath();
this.bootstrapRegistryInitializers = new ArrayList<>(
getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
this.mainApplicationClass = deduceMainApplicationClass();
}
static WebApplicationType deduceFromClasspath() {
// 判断类路径下存在web.reactive.DispatcherHandler并且不存在web.servlet.DispatcherServlet
if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)
&& !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {
// 则认为该应用程序类型为Reactive类型
return WebApplicationType.REACTIVE;
}
// SERVLET_INDICATOR_CLASSES:"jakarta.servlet.Servlet", "org.springframework.web.context.ConfigurableWebApplicationContext" }
for (String className : SERVLET_INDICATOR_CLASSES) {
// 如果类路径下都不存在jakarta.servlet.Servlet,ConfigurableWebApplicationContext,则认为应用程序类型为非WEB应用程序
if (!ClassUtils.isPresent(className, null)) {
return WebApplicationType.NONE;
}
}
// 认为该应用程序为Servlet WEB应用程序
return WebApplicationType.SERVLET;
}
我们在测试方法中测试调用一下:
package com.cherry.a39boot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.GenericApplicationContext;
import java.lang.reflect.Method;
import java.util.Set;
@Configuration
public class A39_1 {
public static void main(String[] args) throws Exception {
System.out.println("2. 演示推断应用类型");
Method deduceFromClasspath = WebApplicationType.class.getDeclaredMethod("deduceFromClasspath");
deduceFromClasspath.setAccessible(true);
System.out.println("\t应用类型为:"+deduceFromClasspath.invoke(null));
ConfigurableApplicationContext context = spring.run(args);
}
@Bean
public TomcatServletWebServerFactory servletWebServerFactory() {
return new TomcatServletWebServerFactory();
}
}
2. 演示推断应用类型
应用类型为:SERVLET
1.3 ApplicationContext初始化器
接下来我们演示一下ApplicationContext初始化器, 初始化器的作用说白了就是对ApplicationContext的功能做一些扩展。
package com.cherry.a39boot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.GenericApplicationContext;
import java.lang.reflect.Method;
import java.util.Set;
@Configuration
public class A39_1 {
public static void main(String[] args) throws Exception {
System.out.println("1. 演示获取 Bean Definition 源");
SpringApplication spring = new SpringApplication(A39_1.class);
// 设置多个源
spring.setSources(Set.of("classpath:b01.xml"));
System.out.println("2. 演示推断应用类型");
Method deduceFromClasspath = WebApplicationType.class.getDeclaredMethod("deduceFromClasspath");
deduceFromClasspath.setAccessible(true);
System.out.println("\t应用类型为:"+deduceFromClasspath.invoke(null));
System.out.println("3. 演示 ApplicationContext 初始化器");
spring.addInitializers(applicationContext -> {
if (applicationContext instanceof GenericApplicationContext gac) {
gac.registerBean("bean3", Bean3.class);
}
});
ConfigurableApplicationContext context = spring.run(args);
// 创建 ApplicationContext
// 调用初始化器 对 ApplicationContext 做扩展
// ApplicationContext.refresh
for (String name : context.getBeanDefinitionNames()) {
System.out.println("name: " + name + " 来源:" + context.getBeanFactory().getBeanDefinition(name).getResourceDescription());
}
context.close();
}
static class Bean1 {
}
static class Bean2 {
}
static class Bean3 {
}
@Bean
public Bean2 bean2() {
return new Bean2();
}
@Bean
public TomcatServletWebServerFactory servletWebServerFactory() {
return new TomcatServletWebServerFactory();
}
}
name: bean3 来源:null
1.4 监听器与事件
在Spring Boot程序启动和运行的过程中,它会发布一些事件监听器就是用来响应事件并且做出处理。和SpringApplication的初始化器类似,SpringApplication的构造方法里,也有读取配置文件中的一些监听器事件,我们来举个例子,如下面的代码:
@Configuration
public class A39_1 {
public static void main(String[] args) throws Exception {
System.out.println("1. 演示获取 Bean Definition 源");
SpringApplication spring = new SpringApplication(A39_1.class);
// 设置多个源
spring.setSources(Set.of("classpath:b01.xml"));
System.out.println("2. 演示推断应用类型");
Method deduceFromClasspath = WebApplicationType.class.getDeclaredMethod("deduceFromClasspath");
deduceFromClasspath.setAccessible(true);
System.out.println("\t应用类型为:"+deduceFromClasspath.invoke(null));
System.out.println("3. 演示 ApplicationContext 初始化器");
spring.addInitializers(applicationContext -> {
if (applicationContext instanceof GenericApplicationContext gac) {
gac.registerBean("bean3", Bean3.class);
}
});
System.out.println("4. 演示监听器与事件");
// 打印事件的类型
spring.addListeners(event -> System.out.println("\t事件为:" + event.getClass()));
ConfigurableApplicationContext context = spring.run(args);
// 创建 ApplicationContext
// 调用初始化器 对 ApplicationContext 做扩展
// ApplicationContext.refresh
for (String name : context.getBeanDefinitionNames()) {
System.out.println("name: " + name + " 来源:" + context.getBeanFactory().getBeanDefinition(name).getResourceDescription());
}
context.close();
}
static class Bean1 {
}
static class Bean2 {
}
static class Bean3 {
}
@Bean
public Bean2 bean2() {
return new Bean2();
}
@Bean
public TomcatServletWebServerFactory servletWebServerFactory() {
return new TomcatServletWebServerFactory();
}
}
4. 演示监听器与事件
事件为:class org.springframework.boot.context.event.ApplicationContextInitializedEvent
事件为:class org.springframework.boot.context.event.ApplicationPreparedEvent
事件为:class org.springframework.boot.web.servlet.context.ServletWebServerInitializedEvent
事件为:class org.springframework.context.event.ContextRefreshedEvent
事件为:class org.springframework.boot.context.event.ApplicationStartedEvent
事件为:class org.springframework.boot.availability.AvailabilityChangeEvent
事件为:class org.springframework.boot.context.event.ApplicationReadyEvent
事件为:class org.springframework.boot.availability.AvailabilityChangeEvent
事件为:class org.springframework.boot.availability.AvailabilityChangeEvent
事件为:class org.springframework.context.event.ContextClosedEvent
1.5 主类推断
主类推断就是判断将来Spring Boot执行main方法所在的类是哪一个?如下面的测试代码:
@Configuration
public class A39_1 {
public static void main(String[] args) throws Exception {
System.out.println("1. 演示获取 Bean Definition 源");
SpringApplication spring = new SpringApplication(A39_1.class);
// 设置多个源
spring.setSources(Set.of("classpath:b01.xml"));
System.out.println("2. 演示推断应用类型");
Method deduceFromClasspath = WebApplicationType.class.getDeclaredMethod("deduceFromClasspath");
deduceFromClasspath.setAccessible(true);
System.out.println("\t应用类型为:"+deduceFromClasspath.invoke(null));
System.out.println("3. 演示 ApplicationContext 初始化器");
spring.addInitializers(applicationContext -> {
if (applicationContext instanceof GenericApplicationContext gac) {
gac.registerBean("bean3", Bean3.class);
}
});
System.out.println("4. 演示监听器与事件");
spring.addListeners(event -> System.out.println("\t事件为:" + event.getClass()));
System.out.println("5. 演示主类推断");
Method deduceMainApplicationClass = SpringApplication.class.getDeclaredMethod("deduceMainApplicationClass");
deduceMainApplicationClass.setAccessible(true);
System.out.println("\t主类是:"+deduceMainApplicationClass.invoke(spring));
ConfigurableApplicationContext context = spring.run(args);
// 创建 ApplicationContext
// 调用初始化器 对 ApplicationContext 做扩展
// ApplicationContext.refresh
for (String name : context.getBeanDefinitionNames()) {
System.out.println("name: " + name + " 来源:" + context.getBeanFactory().getBeanDefinition(name).getResourceDescription());
}
context.close();
}
static class Bean1 {
}
static class Bean2 {
}
static class Bean3 {
}
@Bean
public Bean2 bean2() {
return new Bean2();
}
@Bean
public TomcatServletWebServerFactory servletWebServerFactory() {
return new TomcatServletWebServerFactory();
}
}
5. 演示主类推断
主类是:class com.cherry.a39boot.A39_1
1.6 总结
SpringApplication 构造方法中所做的操作
- 可以有多种源用来加载 bean 定义
- 应用类型推断
- 容器初始化器
- 启动各阶段事件
- 主类推断
2. SpringApplication对象的run方法
接下来我们来看一下SpringApplication对象的run方法执行流程
下面的示例代码演示了各个阶段的使用示例:
// 运行时请添加运行参数 --server.port=8080 debug
public class A39_3 {
@SuppressWarnings("all")
public static void main(String[] args) throws Exception {
SpringApplication app = new SpringApplication();
app.addInitializers(new ApplicationContextInitializer<ConfigurableApplicationContext>() {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
System.out.println("执行初始化器增强...");
}
});
System.out.println(">>>>>>>>>>>>>>>>>>>>>>>> 2. 封装启动 args");
// 将普通参数封装为ApplicationArguments参数
DefaultApplicationArguments arguments = new DefaultApplicationArguments(args);
System.out.println(">>>>>>>>>>>>>>>>>>>>>>>> 8. 创建容器");
GenericApplicationContext context = createApplicationContext(WebApplicationType.SERVLET);
System.out.println(">>>>>>>>>>>>>>>>>>>>>>>> 9. 准备容器");
for (ApplicationContextInitializer initializer : app.getInitializers()) {
// 获取所有初始化器并进行初始化
initializer.initialize(context);
}
System.out.println(">>>>>>>>>>>>>>>>>>>>>>>> 10. 加载 bean 定义");
DefaultListableBeanFactory beanFactory = context.getDefaultListableBeanFactory();
// 1. 基于注解获取bean
AnnotatedBeanDefinitionReader reader1 = new AnnotatedBeanDefinitionReader(beanFactory);
// 2. 基于xml获取bean
XmlBeanDefinitionReader reader2 = new XmlBeanDefinitionReader(beanFactory);
// 3. 基于类扫描器获取bean
ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(beanFactory);
reader1.register(Config.class);
reader2.loadBeanDefinitions(new ClassPathResource("b03.xml"));
scanner.scan("com.cherry.a39boot.sub");
System.out.println(">>>>>>>>>>>>>>>>>>>>>>>> 11. refresh 容器");
context.refresh();
for (String name : context.getBeanDefinitionNames()) {
System.out.println("name:" + name + " 来源:" + beanFactory.getBeanDefinition(name).getResourceDescription());
}
// 实现CommandLineRunner接口
System.out.println(">>>>>>>>>>>>>>>>>>>>>>>> 12. 执行 runner");
for (CommandLineRunner runner : context.getBeansOfType(CommandLineRunner.class).values()) {
runner.run(args);
}
// 实现ApplicationRunner接口
for (ApplicationRunner runner : context.getBeansOfType(ApplicationRunner.class).values()) {
runner.run(arguments);
}
/*
学到了什么
a. 创建容器、加载 bean 定义、refresh, 对应的步骤
*/
}
// 根据程序的不同类型生成不同的Spring容器
private static GenericApplicationContext createApplicationContext(WebApplicationType type) {
GenericApplicationContext context = null;
switch (type) {
case SERVLET -> context = new AnnotationConfigServletWebServerApplicationContext();
case REACTIVE -> context = new AnnotationConfigReactiveWebServerApplicationContext();
case NONE -> context = new AnnotationConfigApplicationContext();
}
return context;
}
static class Bean4 {
}
static class Bean5 {
}
static class Bean6 {
}
@Configuration
static class Config {
@Bean
public Bean5 bean5() {
return new Bean5();
}
@Bean
public ServletWebServerFactory servletWebServerFactory() {
return new TomcatServletWebServerFactory();
}
@Bean
public CommandLineRunner commandLineRunner() {
return new CommandLineRunner() {
@Override
public void run(String... args) throws Exception {
System.out.println("commandLineRunner()..." + Arrays.toString(args));
}
};
}
@Bean
public ApplicationRunner applicationRunner() {
return new ApplicationRunner() {
@Override
public void run(ApplicationArguments args) throws Exception {
System.out.println("applicationRunner()..." + Arrays.toString(args.getSourceArgs()));
System.out.println(args.getOptionNames());
System.out.println(args.getOptionValues("server.port"));
System.out.println(args.getNonOptionArgs());
}
};
}
}
}
我们根据源码来总结一下SpringApplication对象的run方法:
public ConfigurableApplicationContext run(String... args) {
Startup startup = Startup.create();
if (this.registerShutdownHook) {
SpringApplication.shutdownHook.enableShutdownHookAddition();
}
DefaultBootstrapContext bootstrapContext = createBootstrapContext();
ConfigurableApplicationContext context = null;
configureHeadlessProperty();
// 1. 获取一个事件发布器对象-->读取Spring Factory中的配置,找到事件发布器的实现类
SpringApplicationRunListeners listeners = getRunListeners(args);
// starting事件表示Spring Boot程序开始启动
listeners.starting(bootstrapContext, this.mainApplicationClass);
try {
// 2. 将main方法中普通的参数封装成了一个ApplicationArguments后的参数对象(将参数划分为选项参数和非选项参数)
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
// 进入环境准备阶段
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
// 7. 打印banner信息
Banner printedBanner = printBanner(environment);
// 8. 根据程序类型创建Spring容器
context = createApplicationContext();
context.setApplicationStartup(this.applicationStartup);
// 进入到 prepareContext 方法中
prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
// 11. 调用容器的refresh方法,调用各种Bean Factory的后处理器,准备各种Bean的后处理器,初始化每个单例,并发布一个容器已准备就绪事件
refreshContext(context);
afterRefresh(context, applicationArguments);
startup.started();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), startup);
}
listeners.started(context, startup.timeTakenToStarted());
// 12. 调用所有实现了ApplicationRunner和CommandLineRunner接口的Bean
callRunners(context, applicationArguments);
}
// 如果在启动过程中出现了异常,就会发布一个失败事件!
catch (Throwable ex) {
throw handleRunFailure(context, ex, listeners);
}
try {
// 发布一个ready(running)事件,表示Spring Boot程序启动完成
if (context.isRunning()) {
listeners.ready(context, startup.ready());
}
}
catch (Throwable ex) {
throw handleRunFailure(context, ex, null);
}
return context;
}
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
DefaultBootstrapContext bootstrapContext, ApplicationArguments applicationArguments) {
// Create and configure the environment
// 3. 准备一个环境对象,并将第2步封装好的参数对象加载到环境中(k-v形式)
ConfigurableEnvironment environment = getOrCreateEnvironment();
configureEnvironment(environment, applicationArguments.getSourceArgs());
// 4. 将命名不规范的键值对做一个统一的处理(统一转为'-'形式)
ConfigurationPropertySources.attach(environment);
// 5. 此时事件器发布环境已准备好事件-->表明此时环境已经准备好了
listeners.environmentPrepared(bootstrapContext, environment);
DefaultPropertiesPropertySource.moveToEnd(environment);
Assert.state(!environment.containsProperty("spring.main.environment-prefix"),
"Environment prefix cannot be set via properties.");
// 6. 将环境中以spring.main前缀的key跟Spring application对象做一个绑定
bindToSpringApplication(environment);
if (!this.isCustomEnvironment) {
EnvironmentConverter environmentConverter = new EnvironmentConverter(getClassLoader());
environment = environmentConverter.convertEnvironmentIfNecessary(environment, deduceEnvironmentClass());
}
ConfigurationPropertySources.attach(environment);
return environment;
}
private void prepareContext(DefaultBootstrapContext bootstrapContext, ConfigurableApplicationContext context,
ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments, Banner printedBanner) {
// 9. 容器初始化器,对application Context做一些功能增强
context.setEnvironment(environment);
postProcessApplicationContext(context);
addAotGeneratedInitializerIfNecessary(this.initializers);
applyInitializers(context);
// 发布一个容器已准备好事件
listeners.contextPrepared(context);
bootstrapContext.close(context);
if (this.logStartupInfo) {
logStartupInfo(context.getParent() == null);
logStartupProfileInfo(context);
}
// Add boot specific singleton beans
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
if (printedBanner != null) {
beanFactory.registerSingleton("springBootBanner", printedBanner);
}
if (beanFactory instanceof AbstractAutowireCapableBeanFactory autowireCapableBeanFactory) {
autowireCapableBeanFactory.setAllowCircularReferences(this.allowCircularReferences);
if (beanFactory instanceof DefaultListableBeanFactory listableBeanFactory) {
listableBeanFactory.setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
}
}
if (this.lazyInitialization) {
context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
}
if (this.keepAlive) {
context.addApplicationListener(new KeepAlive());
}
context.addBeanFactoryPostProcessor(new PropertySourceOrderingBeanFactoryPostProcessor(context));
// 10. 得到所有的Bean Definition源,并将这些Bean Definition源加载到Application Context中,并发布一个Bean Definition已加载事件
if (!AotDetector.useGeneratedArtifacts()) {
// Load the sources
Set<Object> sources = getAllSources();
Assert.notEmpty(sources, "Sources must not be empty");
load(context, sources.toArray(new Object[0]));
}
listeners.contextLoaded(context);
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构