当前位置: 首页 > article >正文

SpringBoot源码解析(一):SpringApplication构造方法

SpringBoot源码系列文章

SpringBoot源码解析(一):SpringApplication构造方法


目录

  • 前言
  • SpringApplication的静态run方法
  • 一、推断Web应用类型
  • 二、spring.factories文件
    • 1、spring.factories介绍
    • 2、读取spring.factories文件
      • 2.1、加载factories文件原理
      • 2.2、三个spring.factories文件路径
      • 2.3、引导注册组件初始化器BootstrapRegistryInitializer
      • 2.4、上下文初始化器ApplicationContextInitializer
      • 2.5、应用监听器ApplicationListener
  • 三、推断启动类Class
  • 总结

前言

  在之前的文章中,我们深入研究了Tomcat、Spring、以及SpringMVC的源码。这次,我们终于来到SpringBoot的源码分析。接下来的几篇文章将重点关注SpringBoot的启动原理自动配置原理。本篇文章将聚焦于SpringApplication的构造方法。基于 2.7.18版本,这也是SpringBoot3发布前的最后一个版本。

SpringApplication.run()方法是启动SpringBoot应用的核心入口。我们从这个方法开始,逐步深入。

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

SpringApplication的静态run方法

  • 点进SpringApplication的静态run方法
public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
	return run(new Class<?>[] { primarySource }, args);
}
  • 接下来进入SpringApplication的构造方法,run方法后面介绍
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
	return new SpringApplication(primarySources).run(args);
}
  1. 根据类路径来推断Web应用程序的类型
  2. 读取spring.factories文件
    • 查找引导注册组件初始化器BootstrapRegistryInitializer
    • 查询上下文初始化器ApplicationContextInitializer
    • 查询监听器ApplicationListener
  3. 推断启动类Class
private Set<Class<?>> primarySources;
// web应用类型
private WebApplicationType webApplicationType;
// 引导注册组件初始化器
private List<BootstrapRegistryInitializer> bootstrapRegistryInitializers;
// 上下文初始化器
private List<ApplicationContextInitializer<?>> initializers;
// 监听器
private List<ApplicationListener<?>> listeners;
// 启动类Class
private Class<?> mainApplicationClass;
	
public SpringApplication(Class<?>... primarySources) {
	this(null, primarySources);
}

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
	this.resourceLoader = resourceLoader;
	// primarySources也就是SpringApplication.run(Application.class, args)的Application不能为空
	Assert.notNull(primarySources, "PrimarySources must not be null");
	// 将Application的Class对象添加进来
	this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
	// 1.根据类路径来推断 Web 应用程序的类型
	this.webApplicationType = WebApplicationType.deduceFromClasspath();
	// 2. 读取spring.factories文件
	// 2.1 查找并实例化BootstrapRegistryInitializer类型的工厂类
	this.bootstrapRegistryInitializers = new ArrayList<>(
			getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
	// 2.2 查找上下文初始化器
	setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
	// 2.3 查找监听器
	setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
	// 3.推断启动类Class
	this.mainApplicationClass = deduceMainApplicationClass();
}

一、推断Web应用类型

// 1.根据类路径来推断 Web 应用程序的类型
this.webApplicationType = WebApplicationType.deduceFromClasspath();

SpringBoot应用程序的三种Web应用类型

  • NONE:
    • 表示该应用程序不是Web应用,不会启动嵌入式Web服务器
  • SERVLET:
    • 表示一个传统的基于Servlet的Web应用程序,将启动嵌入式ServletWeb服务器(如Tomcat)
  • REACTIVE:
    • 表示一个响应式风格的Web应用程序,将启动嵌入式响应式Web服务器(如Netty)
// WebApplicationType枚举类
public enum WebApplicationType {

	// 表示该应用程序不是 Web 应用,不会启动嵌入式 Web 服务器
	NONE,

	// 表示一个传统的基于 Servlet 的 Web 应用程序,将启动嵌入式 Servlet Web 服务器(如 Tomcat)
	SERVLET,

	// 表示一个响应式风格的 Web 应用程序,将启动嵌入式响应式 Web 服务器(如 Netty)
	REACTIVE;
	
	// 适合运行在基于 Servlet 的环境中
	private static final String[] SERVLET_INDICATOR_CLASSES = { "javax.servlet.Servlet",
			"org.springframework.web.context.ConfigurableWebApplicationContext" };
	// 是 Spring MVC 应用程序的主要调度器
	private static final String WEBMVC_INDICATOR_CLASS = "org.springframework.web.servlet.DispatcherServlet";
	// 用于 Spring WebFlux。这表明是一个响应式 Web 应用程序
	private static final String WEBFLUX_INDICATOR_CLASS = "org.springframework.web.reactive.DispatcherHandler";
	// 用于基于 Jersey 的应用程序
	private static final String JERSEY_INDICATOR_CLASS = "org.glassfish.jersey.servlet.ServletContainer";
	
	// 推断方法
	static WebApplicationType deduceFromClasspath() {
		if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)
				&& !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {
			return WebApplicationType.REACTIVE;
		}
		for (String className : SERVLET_INDICATOR_CLASSES) {
			if (!ClassUtils.isPresent(className, null)) {
				return WebApplicationType.NONE;
			}
		}
		return WebApplicationType.SERVLET;
	}

}

  先介绍下ClassUtils.isPresent方法,用它来检查类路径中是否有特定类。使用Class.forName('类路径')方法,成功返回true,表示存在此类,报错返回fals,表示没有此类。

public static boolean isPresent(String className, @Nullable ClassLoader classLoader) {
	try {
		forName(className, classLoader);
		return true;
	}
	catch (IllegalAccessError err) {
		throw new IllegalStateException("Readability mismatch in inheritance hierarchy of class [" +
				className + "]: " + err.getMessage(), err);
	}
	catch (Throwable ex) {
		// Typically ClassNotFoundException or NoClassDefFoundError...
		return false;
	}
}
  • 回到WebApplicationType.deduceFromClasspath()推断方法
    • 存在reactive.DispatcherHandler类,当不存在servlet.DispatcherServlet类表示响应式Web应用
    • 如果不是响应式应用,ServletConfigurableWebApplicationContext都存在表示传统Web应用

二、spring.factories文件

1、spring.factories介绍

  spring.factories 是 Spring 框架中的一个关键配置文件,通常位于类路径下的META-INF目录中。它的主要功能是提供一种自动装配机制,用于在应用启动时自动加载指定的类。通过spring.factories文件,开发者可以将特定的配置类、监听器、过滤器等组件注册到Spring上下文中。

文件格式

  • 通常以键值对的形式表示
    • 键为接口或抽象类的全限定名
    • 值为实现类的全限定名,多个类可以用逗号分隔

示例

  • 使用\表示续行符,用来将长行分成多行写
# 自动配置
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.config.MyAutoConfiguration1,\
com.example.config.MyAutoConfiguration2

# 自定义监听器
org.springframework.context.ApplicationListener=\
com.example.listener.MyApplicationListener

2、读取spring.factories文件

// 2. 读取spring.factories文件
// 2.1 查找并实例化BootstrapRegistryInitializer类型的工厂类
this.bootstrapRegistryInitializers = new ArrayList<>(
		getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
// 2.2 查找上下文初始化器
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
// 2.3 查找应用监听器
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
  • 用于从所有依赖的JAR文件的META-INF/spring.factories文件中加载指定类型的多个工厂类名称
  • 通过Class.forName反射获取实例化多个对象,并根据对象上@Order注解或Ordered接口排序
// SpringApplication类方法
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type) {
	// 第二个参数为构造器的参数类型集合
	return getSpringFactoriesInstances(type, new Class<?>[] {});
}

private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
	ClassLoader classLoader = getClassLoader();
	// 用于从所有依赖的 JAR 文件的 META-INF/spring.factories 文件中加载指定类型的工厂类名称
	Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
	// 根据权限定类名字符串,Class.forName反射实例化对象
	List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
	// 它能够根据@Order注解和Ordered接口的优先级来对对象进行排序,从而决定对象的执行顺序
	AnnotationAwareOrderComparator.sort(instances);
	return instances;
}

2.1、加载factories文件原理

// SpringFactoriesLoader类方法
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
	ClassLoader classLoaderToUse = classLoader;
	if (classLoaderToUse == null) {
		classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
	}
	String factoryTypeName = factoryType.getName();
	// 加载spring.factories文件,获取Map,通过key获取多个实现
	return loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
}

// 读取spring.factories文件的缓存结果
static final Map<ClassLoader, Map<String, List<String>>> cache = new ConcurrentReferenceHashMap<>();

// 从 `META-INF/spring.factories` 资源中加载并缓存 Spring 工厂。
private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
    // 首先检查缓存中是否已经存在该 classLoader 对应的工厂列表
    Map<String, List<String>> result = cache.get(classLoader);
    if (result != null) {
        return result; // 如果缓存中存在,则直接返回
    }

    // 初始化一个空的 Map 用于存储结果
    result = new HashMap<>();
    try {
        // 获取资源位置(META-INF/spring.factories)的所有 URL
        Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
        while (urls.hasMoreElements()) {
            // 遍历每个 URL 并从资源文件加载属性
            URL url = urls.nextElement();
            UrlResource resource = new UrlResource(url);

            // 从当前资源文件加载属性
            Properties properties = PropertiesLoaderUtils.loadProperties(resource);

            // 遍历每个属性条目(工厂类型及其实现)
            for (Map.Entry<?, ?> entry : properties.entrySet()) {
                // 键为工厂类型名称
                String factoryTypeName = ((String) entry.getKey()).trim();

                // 值为逗号分隔的工厂实现名称字符串
                String[] factoryImplementationNames =
                        StringUtils.commaDelimitedListToStringArray((String) entry.getValue());

                // 将每个工厂实现添加到结果 Map 中对应的工厂类型下
                for (String factoryImplementationName : factoryImplementationNames) {
                    // 去除空格并添加每个工厂实现名称到该工厂类型的列表中
                    result.computeIfAbsent(factoryTypeName, key -> new ArrayList<>())
                            .add(factoryImplementationName.trim());
                }
            }
        }

        // 确保所有列表都是不可修改的且包含唯一的元素
        result.replaceAll((factoryType, implementations) -> implementations.stream().distinct()
                .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)));

        // 将结果缓存到缓存中以便下次复用
        cache.put(classLoader, result);
    }
    catch (IOException ex) {
        // 如果加载资源时遇到 IOException,抛出 IllegalArgumentException 异常
		throw new IllegalArgumentException("Unable to load factories from location [" +
				FACTORIES_RESOURCE_LOCATION + "]", ex);
    }
    return result; // 返回加载的结果
}

PropertiesLoaderUtils.loadProperties(resource)资源加载属性原理

在这里插入图片描述

  • 从LineReader中加载键值对,并存储在当前对象中
// 从 LineReader 中加载键值对,并存储在当前对象中
private void load0(LineReader lr) throws IOException {
    // 用于字符转换的缓冲区
    char[] convtBuf = new char[1024];
    int limit;
    int keyLen;
    int valueStart;
    char c;
    boolean hasSep;
    boolean precedingBackslash;

    // 逐行读取,直到没有更多行
    while ((limit = lr.readLine()) >= 0) {
        c = 0;
        keyLen = 0;
        valueStart = limit;
        hasSep = false;

        // precedingBackslash 表示当前字符前是否有反斜杠
        precedingBackslash = false;

        // 遍历当前行的每个字符,识别键的长度(keyLen)和值的起始位置(valueStart)
        while (keyLen < limit) {
            c = lr.lineBuf[keyLen];
            // 检查字符是否为键值分隔符(= 或 :),并且没有被转义
            if ((c == '=' || c == ':') && !precedingBackslash) {
                valueStart = keyLen + 1;
                hasSep = true; // 标记有分隔符
                break;
            } 
            // 如果字符为空格、制表符或换页符,并且没有被转义,则认为是值的开始
            else if ((c == ' ' || c == '\t' || c == '\f') && !precedingBackslash) {
                valueStart = keyLen + 1;
                break;
            }
            // 检查是否是反斜杠,并处理转义状态
            if (c == '\\') {
                precedingBackslash = !precedingBackslash;
            } else {
                precedingBackslash = false;
            }
            keyLen++;
        }

        // 跳过值前面的空格、制表符和换页符
        while (valueStart < limit) {
            c = lr.lineBuf[valueStart];
            if (c != ' ' && c != '\t' && c != '\f') {
                // 如果没有分隔符,并且遇到 '=' 或 ':',则标记为有分隔符
                if (!hasSep && (c == '=' || c == ':')) {
                    hasSep = true;
                } else {
                    break;
                }
            }
            valueStart++;
        }

        // 使用 loadConvert 方法将键和值部分的字符转换为字符串
        String key = loadConvert(lr.lineBuf, 0, keyLen, convtBuf);
        String value = loadConvert(lr.lineBuf, valueStart, limit - valueStart, convtBuf);

        // 将键值对存入当前对象的属性中
        put(key, value);
    }
}

2.2、三个spring.factories文件路径

  • spring-boot-2.7.18.jar

在这里插入图片描述

  • spring-boot-autoconfigure-2.7.18.jar

在这里插入图片描述

  • spring-beans-5.3.31.jar

在这里插入图片描述

  查询引导注册组件初始化器、上下文初始化器、应用监听器就是从以上三个spring.factories文件中获取BootstrapRegistryInitializerApplicationContextInitializerApplicationListener这三个接口的实现类。

2.3、引导注册组件初始化器BootstrapRegistryInitializer

  BootstrapRegistryInitializer在ApplicationContext创建之前对注册表进行配置,并注册一些启动时的关键组件。它主要应用于SpringCloud的场景中,用来初始化那些在应用上下文加载之前需要配置的组件,比如配置中心服务注册和发现等。

@FunctionalInterface
public interface BootstrapRegistryInitializer {
	void initialize(BootstrapRegistry registry);
}

  从以上三个jar的spring.factories文件没有获取到初始化器,表示这里也没用到它,等以后解析SpringCloud源码时候再做深究。

2.4、上下文初始化器ApplicationContextInitializer

  ApplicationContextInitializer主要作用是在Spring应用上下文 (ApplicationContext) 刷新之前进行自定义的初始化操作。它允许开发者在应用上下文完全启动和加载所有Bean定义之前进行特定的配置和设置。

@FunctionalInterface
public interface ApplicationContextInitializer<C extends ConfigurableApplicationContext> {
	void initialize(C applicationContext);
}

  从以上三个jar的spring.factories文件获取到7个上下文初始化器,前5个来自spring-boot-2.7.18.jar,最后2个来自spring-boot-autoconfigure-2.7.18.jar。

  1. org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer
    • 作用:检查配置中的常见错误或不推荐的配置项,并在检测到这些问题时发出警告,帮助开发者尽早发现潜在的配置问题,确保配置的正确性
  2. org.springframework.boot.context.ContextIdApplicationContextInitializer
    • 作用:为ApplicationContext设置一个唯一的上下文ID,尤其在多上下文应用程序中有助于区分和管理不同的上下文实例。默认情况下,ID 会基于应用的环境和属性生成
  3. org.springframework.boot.context.config.DelegatingApplicationContextInitializer
    • 作用:代理一组ApplicationContextInitializer 的初始化任务,实现灵活组合多个初始化任务。此组件将任务委派给自定义的ApplicationContextInitializer列表,使初始化更灵活和可定制
  4. org.springframework.boot.rsocket.context.RSocketPortInfoApplicationContextInitializer
    • 作用:将 RSocket 服务器的端口信息暴露在应用上下文环境中,使应用程序的其他组件能够访问该端口信息。这对于需要动态访问 RSocket 服务的端口信息的场景非常有用
  5. org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer
    • 作用:将 Web 服务器的端口信息暴露在应用上下文环境中,使其他组件可以动态访问该端口信息。适用于需要动态确定服务器端口的情况(例如在随机端口上启动时)
  6. org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer
    • 作用:在应用上下文中共享一个MetadataReaderFactory实例,以便于Spring扫描类路径和读取类元数据,减少I/O操作和开销,提高性能
  7. org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener
    • 作用:记录条件评估报告,将自动配置条件的匹配或不匹配详情输出到日志中,帮助开发者理解SpringBoot自动配置的过程和条件匹配的结果,便于调试和优化

  后续篇章会单独解析每一个初始化器。

2.5、应用监听器ApplicationListener

  ApplicationListener作用是监听Spring框架中内置的各种事件(如上下文刷新事件、上下文关闭事件等),也可以监听自定义的事件。基于事件触发执行逻辑,常用于对生命周期事件的监听以及自定义事件的处理,帮助实现松耦合的事件驱动架构。

@FunctionalInterface
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
	// 处理应用程序事件
	void onApplicationEvent(E event);
	
	static <T> ApplicationListener<PayloadApplicationEvent<T>> forPayload(Consumer<T> consumer) {
		return event -> consumer.accept(event.getPayload());
	}
}

  从以上三个jar的spring.factories文件获取到8个监听器,前7个来自spring-boot-2.7.18.jar,最后一个来自spring-boot-autoconfigure-2.7.18.jar。

  1. org.springframework.boot.ClearCachesApplicationListener
    • 作用:用于清除SpringBoot内部的缓存,通常在应用程序重新加载或重新初始化时触发,以确保数据一致性
    • 触发时机:当应用上下文刷新或重新启动时
  2. org.springframework.boot.builder.ParentContextCloserApplicationListener
    • 作用:监听子上下文的关闭事件,并在子上下文关闭时关闭其父上下文。这通常用于父子上下文组合应用场景中,以确保上下文的关闭顺序正确
    • 触发时机:当子上下文关闭时
  3. org.springframework.boot.context.FileEncodingApplicationListener
    • 作用:确保应用程序以指定的文件编码运行。如果系统的文件编码与SpringBoot配置中的编码不匹配,它会强制设置为指定编码,确保编码一致性
    • 触发时机:应用上下文刷新时
  4. org.springframework.boot.context.config.AnsiOutputApplicationListener
    • 作用:控制 ANSI 输出的设置,允许在控制台中使用 ANSI 彩色输出(如日志输出中的彩色显示)
    • 触发时机:应用上下文刷新时,根据配置启用或禁用 ANSI 彩色输出
  5. org.springframework.boot.context.config.DelegatingApplicationListener
    • 作用:充当其他 ApplicationListener 的代理,将事件转发给多个监听器。这使得可以集中管理多个监听器
    • 触发时机:在监听器列表中注册的事件触发时
  6. org.springframework.boot.context.logging.LoggingApplicationListener
    • 作用:用于初始化日志系统,根据 application.properties 或环境配置设置日志级别和格式。Spring Boot 的日志系统初始化通常是由该监听器负责
    • 触发时机:应用启动时,最早被触发的监听器之一
  7. org.springframework.boot.env.EnvironmentPostProcessorApplicationListener
    • 作用:在 Environment 准备阶段后调用 EnvironmentPostProcessor,允许对环境变量进行进一步处理,例如动态配置属性值
    • 触发时机:在应用上下文刷新之前
  8. org.springframework.boot.autoconfigure.BackgroundPreinitializer
    • 作用:在后台线程中异步初始化一些资源或任务,减少主线程的阻塞时间。此操作通常是提前加载一些可能需要时间初始化的资源,以优化启动时间
    • 触发时机:在应用启动阶段,通过后台线程异步执行

  后续篇章会单独解析每一个监听器器。

三、推断启动类Class

// 3.推断启动类Class
this.mainApplicationClass = deduceMainApplicationClass();

通过创建一个异常栈追踪来找到调用 main 方法的类。以下是对它的逐步分析

  1. 获取栈追踪信息:new RuntimeException().getStackTrace() 获取当前执行线程的堆栈追踪信息
    • 堆栈追踪中包含了方法调用的顺序,每个元素都是一个StackTraceElement对象
    • 记录了方法名、类名、文件名和代码行号等信息
  2. 遍历栈追踪:通过for循环遍历每个StackTraceElement,查找方法名为main的元素
    • 如果找到了方法名为main的元素,则可以通过stackTraceElement.getClassName()获取该类的全限定名,然后使用Class.forName()加载该类
    • 如果类加载失败(可能是类不存在),会抛出ClassNotFoundException,但这里捕获了异常并选择继续执行
  3. 最终,如果未找到主类或类加载失败,则返回null
// SpringApplication类方法
private Class<?> mainApplicationClass;
...
this.mainApplicationClass = deduceMainApplicationClass();
...
private Class<?> deduceMainApplicationClass() {
	try {
		// 获取当前执行线程的堆栈追踪信息
		StackTraceElement[] stackTrace = new RuntimeException().getStackTrace();
		// 遍历每个元素
		for (StackTraceElement stackTraceElement : stackTrace) {
			// 获取方法名为main的元素
			if ("main".equals(stackTraceElement.getMethodName())) {
				// 通过元素类权限定名的Class.forName()获取Class对象
				return Class.forName(stackTraceElement.getClassName());
			}
		}
	}
	catch (ClassNotFoundException ex) {
		// Swallow and continue
	}
	return null;
}

总结

  1. SpringApplication.run():作为SpringBoot应用的启动入口,它负责创建SpringApplication对象,并调用其run方法进行启动
  2. 推断Web应用类型:SpringBoot应用分为三种类型:NONESERVLETREACTIVE,根据类路径中的特定类进行推断
  3. 读取spring.factories文件:在SpringBoot启动过程中,从META-INF/spring.factories文件加载初始化器和监听器(都必须无参构造),以便实现自动配置和事件处理
  4. 推断启动类:通过堆栈追踪找到调用main方法的类,即应用的主启动类

http://www.kler.cn/a/381893.html

相关文章:

  • C语言心型代码解析
  • 基于梯度的快速准确头部运动补偿方法在锥束CT中的应用|文献速递-基于深度学习的病灶分割与数据超分辨率
  • 求助帖【如何学习核磁共振的原理】
  • 算法: 链表题目练习
  • conda迁移虚拟环境路径
  • 爬虫学习4
  • npm入门教程5:package.json
  • 静态库、动态库、framework、xcframework、use_frameworks!的作用、关联核心SDK工程和测试(主)工程、设备CPU架构
  • 分布式光伏发电的投融资计算
  • OTFS基带通信系统(脉冲导频,信道估计,MP解调算法)
  • 零基础快速入门MATLAB
  • Nat Med病理AI系列|哈佛大学团队发表研究,探讨深度学习在病理诊断中的公平性问题及解决方案|顶刊精析·24-11-02
  • Webserver(3.2)锁
  • 基于CentOS 7.9上安装WebLogic
  • 【STL_list 模拟】——打造属于自己的高效链表容器
  • EasyExcel 学习之 导出 “文件编码问题”
  • 苍穹外卖 商家取消、派送、完成订单
  • HTB:PermX[WriteUP]
  • 附件商户,用户签到,uv统计功能(geo,bitmap,hyperloglog结构的使用)
  • 如何使用RabbitMQ和Python实现广播消息
  • 深度学习基础知识-Batch Normalization(BN)超详细解析
  • 第二节 管道符、重定向与环境变量
  • 手写一个axios方法
  • python爬取旅游攻略(1)
  • SparkSql读取数据的方式
  • 多模态PaliGemma——Google推出的基于SigLIP和Gemma的视觉语言模型