【SpringBoot合集-02】Spring Boot 核心流程学习

目录

[一、手写模拟 Spring Boot 启动过程](#一、手写模拟 Spring Boot 启动过程)

[1. 核心思想铺垫](#1. 核心思想铺垫)

[2. 手写实现:从注解到容器启动](#2. 手写实现:从注解到容器启动)

第一步:自定义启动注解

第二步:包扫描注解定义

[第三步:自定义 Bean 标记注解](#第三步:自定义 Bean 标记注解)

[第四步:手写简化版 Spring 容器](#第四步:手写简化版 Spring 容器)

第五步:手写启动入口类

[3. 原理与踩坑说明](#3. 原理与踩坑说明)

[二、手写模拟 Spring Boot 条件注解功能](#二、手写模拟 Spring Boot 条件注解功能)

[1. 条件注解的核心作用](#1. 条件注解的核心作用)

[2. 核心执行时机](#2. 核心执行时机)

[3. 手写实现 @MyConditionalOnClass](#3. 手写实现 @MyConditionalOnClass)

第一步:定义条件元注解

第二步:定义条件匹配接口

第三步:实现类存在条件处理器

第四步:定义业务条件注解

[4. 集成到包扫描流程](#4. 集成到包扫描流程)

[5. 扩展:@ConditionalOnMissingBean 的实现逻辑](#5. 扩展:@ConditionalOnMissingBean 的实现逻辑)

[三、自动配置功能与 spring.factories 文件解析](#三、自动配置功能与 spring.factories 文件解析)

[1. 自动配置的本质](#1. 自动配置的本质)

[2. spring.factories 文件解析原理](#2. spring.factories 文件解析原理)

手写配置文件

手写文件加载工具类

[3. 手写自动配置导入逻辑](#3. 手写自动配置导入逻辑)

[第一步:定义 @Import 与 ImportSelector](#第一步:定义 @Import 与 ImportSelector)

第二步:实现自动配置选择器

第三步:定义自动配置开启注解

第四步:增强启动注解

[4. 自动配置类示例](#4. 自动配置类示例)

[5. 常见踩坑](#5. 常见踩坑)

[四、Spring Boot 整合 Tomcat 底层源码分析](#四、Spring Boot 整合 Tomcat 底层源码分析)

[1. 内嵌 Tomcat 的核心思想](#1. 内嵌 Tomcat 的核心思想)

[2. 手写内嵌 Tomcat 启动](#2. 手写内嵌 Tomcat 启动)

第一步:引入依赖

[第二步:封装 TomcatWebServer](#第二步:封装 TomcatWebServer)

[3. 集成到启动流程](#3. 集成到启动流程)

[4. 源码级细节说明](#4. 源码级细节说明)

五、自动配置类加载全流程源码梳理

[1. 完整执行链路图](#1. 完整执行链路图)

[2. 关键扩展机制](#2. 关键扩展机制)

[3. 用户配置优先的实现](#3. 用户配置优先的实现)

六、面试速记总结


一、手写模拟 Spring Boot 启动过程

1. 核心思想铺垫

Spring Boot 不是一套全新的框架,它本质是对 Spring 容器的 "封装增强",核心解决 Spring 原生开发中配置繁琐、部署依赖外部容器的痛点。它的所有能力,最终都落地到「Bean 的扫描、注册、实例化」这套 Spring 核心流程上。

启动流程的本质可以浓缩为三步:

  1. 创建 Spring 容器上下文
  2. 解析启动类,完成包扫描与 Bean 定义注册
  3. 刷新容器,实例化 Bean,若是 Web 环境则启动内嵌服务器

2. 手写实现:从注解到容器启动

第一步:自定义启动注解

模拟原生 @SpringBootApplication,先实现最基础的包扫描能力(后续再叠加自动配置)。

java 复制代码
/**
 * 自定义启动注解,对标@SpringBootApplication
 * 组合包扫描注解,后续会叠加自动配置能力
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@MyComponentScan // 引入自定义包扫描注解
public @interface MySpringBootApplication {
}
第二步:包扫描注解定义

对应 Spring 的 @ComponentScan,用于指定扫描路径,默认取启动类所在包。

java 复制代码
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyComponentScan {
    // 指定扫描包路径,为空则默认扫描启动类所在包
    String value() default "";
}
第三步:自定义 Bean 标记注解

对标 @Component,标记哪些类需要被 Spring 容器管理。

java 复制代码
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyComponent {
}
第四步:手写简化版 Spring 容器

核心实现「注册配置类 → 包扫描 → Bean 定义注册 → 实例化」的完整流程。

java 复制代码
/**
 * 简化版Spring注解容器,对标AnnotationConfigApplicationContext
 */
public class MyAnnotationConfigApplicationContext {

    // Bean定义池:存储所有Bean的Class信息,对应Spring的BeanDefinitionMap
    private final Map<String, Class<?>> beanDefinitionMap = new ConcurrentHashMap<>();
    // 单例Bean缓存:存储实例化后的单例对象,对应Spring的一级缓存
    private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>();

    /**
     * 注册配置类(启动类),作为扫描的入口
     */
    public void register(Class<?> configClass) {
        String beanName = Introspector.decapitalize(configClass.getSimpleName());
        beanDefinitionMap.put(beanName, configClass);
    }

    /**
     * 刷新容器:Spring最核心的方法,完成所有Bean的初始化
     */
    public void refresh() {
        // 1. 执行包扫描,把所有@MyComponent的类注册为Bean定义
        scanComponentPackages();

        // 2. 实例化所有单例Bean(简化版,暂不处理依赖注入)
        instantiateSingletonBeans();
    }

    /**
     * 包扫描核心逻辑
     * 读取启动类的包路径,遍历类文件,识别注解,注册Bean定义
     */
    private void scanComponentPackages() {
        // 获取启动配置类
        Class<?> startupClass = beanDefinitionMap.values().iterator().next();
        MyComponentScan scanAnnotation = startupClass.getAnnotation(MyComponentScan.class);

        // 没指定路径就用启动类所在包,这就是Spring Boot的"约定"
        String scanPackage = scanAnnotation.value();
        if (scanPackage.isEmpty()) {
            scanPackage = startupClass.getPackage().getName();
        }

        // 把包名转为类路径,读取所有class文件
        String classPath = scanPackage.replace(".", "/");
        URL resource = startupClass.getClassLoader().getResource(classPath);
        if (resource == null) return;

        File packageDir = new File(resource.getFile());
        // 遍历包下所有class文件
        File[] classFiles = packageDir.listFiles(f -> f.getName().endsWith(".class"));
        if (classFiles == null) return;

        for (File file : classFiles) {
            String className = scanPackage + "." + file.getName().replace(".class", "");
            try {
                Class<?> clazz = Class.forName(className);
                // 只有加了@MyComponent的类,才注册进容器
                if (clazz.isAnnotationPresent(MyComponent.class)) {
                    String beanName = Introspector.decapitalize(clazz.getSimpleName());
                    beanDefinitionMap.put(beanName, clazz);
                }
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 实例化所有单例Bean
     * 真实Spring中这一步会处理依赖注入、循环依赖、BeanPostProcessor等扩展
     */
    private void instantiateSingletonBeans() {
        for (Map.Entry<String, Class<?>> entry : beanDefinitionMap.entrySet()) {
            String beanName = entry.getKey();
            Class<?> beanClass = entry.getValue();
            try {
                Object bean = beanClass.getDeclaredConstructor().newInstance();
                singletonObjects.put(beanName, bean);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 从容器获取Bean
     */
    public Object getBean(String beanName) {
        return singletonObjects.get(beanName);
    }

    public boolean containsBean(String beanName) {
        return singletonObjects.containsKey(beanName);
    }
}
第五步:手写启动入口类

对标 SpringApplication.run(),封装启动流程

java 复制代码
public class MySpringApplication {

    public static MyAnnotationConfigApplicationContext run(Class<?> primarySource) {
        // 1. 创建容器上下文
        MyAnnotationConfigApplicationContext context = new MyAnnotationConfigApplicationContext();
        // 2. 注册启动类
        context.register(primarySource);
        // 3. 刷新容器,完成所有Bean初始化
        context.refresh();

        System.out.println("MySpringBoot 容器启动完成");
        return context;
    }
}

启动测试

java 复制代码
// 启动类
@MySpringBootApplication
public class BootDemoApplication {
    public static void main(String[] args) {
        MyAnnotationConfigApplicationContext context = MySpringApplication.run(BootDemoApplication.class);
        // 测试从容器获取Bean
        UserService userService = (UserService) context.getBean("userService");
        userService.sayHello();
    }
}

// 业务Bean
@MyComponent
public class UserService {
    public void sayHello() {
        System.out.println("hello, my spring boot!");
    }
}

3. 原理与踩坑说明

  • 为什么要先存 BeanDefinition 再实例化? 因为 Spring 需要先收集全所有 Bean 的元信息,才能处理依赖注入、Bean 的先后顺序、后置处理器扩展。如果边扫描边实例化,会出现依赖的 Bean 还没扫描到的问题。
  • 手写版的局限性 :只支持目录形式的类路径,不支持 Jar 包内的扫描;真实 Spring 用ResourcePatternResolver统一处理各种资源形式。
  • 约定大于配置的体现:默认扫描启动类所在包,不用我们手动写包路径,这就是 Spring Boot 最基础的 "约定"。

二、手写模拟 Spring Boot 条件注解功能

1. 条件注解的核心作用

条件注解是自动配置的底层基石:只有满足预设条件时,对应的配置类或 Bean 才会被注册进容器

  • Spring 原生提供 @Conditional 元注解 + Condition 匹配接口
  • Spring Boot 在此基础上扩展了大量常用条件:@ConditionalOnClass(类存在生效)、@ConditionalOnMissingBean(容器无该 Bean 生效)等

2. 核心执行时机

条件判断发生在 BeanDefinition 注册阶段,不满足条件的类连 Bean 定义都不会进入容器,更不会被实例化,不会占用资源。

3. 手写实现 @MyConditionalOnClass

第一步:定义条件元注解

对标 Spring 的 @Conditional,用于指定条件处理器。

java 复制代码
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyConditional {
    // 指定条件处理器的Class
    Class<? extends MyCondition> value();
}
第二步:定义条件匹配接口
java 复制代码
public interface MyCondition {
    /**
     * 条件匹配方法
     * @return true表示满足条件,Bean可以注册
     */
    boolean matches(Class<?> targetClass);
}
第三步:实现类存在条件处理器

判断类路径下是否存在指定的类,存在则满足条件。

java 复制代码
public class OnClassCondition implements MyCondition {
    @Override
    public boolean matches(Class<?> targetClass) {
        MyConditionalOnClass annotation = targetClass.getAnnotation(MyConditionalOnClass.class);
        if (annotation == null) {
            return true; // 没加条件注解,默认放行
        }

        String targetClassName = annotation.value();
        try {
            // 反射加载类,加载成功说明类路径下存在
            Class.forName(targetClassName);
            return true;
        } catch (ClassNotFoundException e) {
            // 类不存在,不满足条件
            return false;
        }
    }
}
第四步:定义业务条件注解
java 复制代码
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@MyConditional(OnClassCondition.class) // 绑定条件处理器
public @interface MyConditionalOnClass {
    // 用字符串存类名,而不是Class类型
    // 原因:如果类不存在,写Class类型编译就会报错,用字符串可以安全反射判断
    String value();
}

4. 集成到包扫描流程

在容器的 scanComponentPackages 方法中,注册 Bean 前增加条件校验:

java 复制代码
// 识别@MyComponent注解
if (clazz.isAnnotationPresent(MyComponent.class)) {
    // 执行条件判断,满足条件才注册
    if (matchCondition(clazz)) {
        String beanName = Introspector.decapitalize(clazz.getSimpleName());
        beanDefinitionMap.put(beanName, clazz);
    }
}

补充条件判断工具方法:

java 复制代码
private boolean matchCondition(Class<?> clazz) {
    MyConditional conditional = clazz.getAnnotation(MyConditional.class);
    if (conditional == null) {
        return true;
    }
    try {
        // 实例化条件处理器,执行匹配逻辑
        MyCondition condition = conditional.value().getDeclaredConstructor().newInstance();
        return condition.matches(clazz);
    } catch (Exception e) {
        e.printStackTrace();
        return false;
    }
}

5. 扩展:@ConditionalOnMissingBean 的实现逻辑

这个注解是「用户配置优先」的核心:容器中不存在指定 Bean 时,自动配置的 Bean 才会生效。

  • 执行时机:必须在所有 Bean 定义都注册完成后、实例化前执行
  • 常见坑点 :如果配置类加载顺序不对,会出现判断时用户 Bean 还没注册,导致自动配置错误生效。Spring Boot 通过@AutoConfigureOrder强制排序来规避这个问题。

三、自动配置功能与 spring.factories 文件解析

1. 自动配置的本质

自动配置不是黑魔法,核心逻辑非常清晰:

  1. 提前写好一堆配置类(比如 Tomcat 配置、数据源配置)
  2. 启动时通过配置文件批量发现这些配置类
  3. 配合条件注解,筛选出适配当前环境的配置类
  4. 把这些配置类自动注册进容器,创建默认的 Bean

2. spring.factories 文件解析原理

这是 Spring Boot 扩展机制的核心,属于 SPI(服务发现接口)思想的实现。

  • 所有 Jar 包都可以在 META-INF/spring.factories 中配置自己的自动配置类
  • Spring Boot 启动时会扫描所有 Jar 包下的这个文件,聚合所有自动配置类
  • 第三方 Starter 就是靠这个文件实现 "引入依赖就自动生效"
手写配置文件

在 resources 目录下创建 META-INF/my-spring.factories

XML 复制代码
# 格式:key=自动配置入口的全限定名,value=配置类列表,逗号分隔
com.tuling.myboot.autoconfigure.MyEnableAutoConfiguration=\
com.tuling.myboot.autoconfigure.TomcatAutoConfiguration,\
com.tuling.myboot.autoconfigure.UserAutoConfiguration
手写文件加载工具类

对标 Spring 的 SpringFactoriesLoader

java 复制代码
public class MySpringFactoriesLoader {
    // 配置文件固定路径
    private static final String FACTORIES_PATH = "META-INF/my-spring.factories";

    /**
     * 加载所有指定类型的配置类全限定名
     * @param factoryType 配置key的全限定名
     * @param classLoader 类加载器
     */
    public static List<String> loadFactoryNames(String factoryType, ClassLoader classLoader) {
        List<String> result = new ArrayList<>();
        try {
            // 1. 扫描类路径下所有的factories文件(包括所有Jar包里的)
            Enumeration<URL> urls = classLoader.getResources(FACTORIES_PATH);
            while (urls.hasMoreElements()) {
                URL url = urls.nextElement();
                // 2. 读取properties内容
                Properties props = new Properties();
                try (InputStream is = url.openStream()) {
                    props.load(is);
                }
                // 3. 解析对应key的value,拆分多个类名
                String classNamesStr = props.getProperty(factoryType);
                if (classNamesStr != null && !classNamesStr.isEmpty()) {
                    String[] classNames = classNamesStr.split(",");
                    for (String className : classNames) {
                        result.add(className.trim());
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return result;
    }
}

3. 手写自动配置导入逻辑

Spring 通过 @Import + ImportSelector 实现批量导入配置类,我们复刻这套逻辑。

第一步:定义 @Import 与 ImportSelector
java 复制代码
// 导入注解,对标@Import
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyImport {
    Class<?> value();
}

// 配置类选择器接口
public interface MyImportSelector {
    /**
     * 返回需要导入的配置类全限定名数组
     */
    String[] selectImports(Class<?> annotationClass);
}
第二步:实现自动配置选择器
java 复制代码
public class MyAutoConfigurationImportSelector implements MyImportSelector {
    @Override
    public String[] selectImports(Class<?> annotationClass) {
        // 1. 从factories文件加载所有自动配置类
        List<String> autoConfigClasses = MySpringFactoriesLoader.loadFactoryNames(
                MyEnableAutoConfiguration.class.getName(),
                this.getClass().getClassLoader()
        );
        // 2. 返回给容器,后续会被注册为配置类
        return autoConfigClasses.toArray(new String[0]);
    }
}
第三步:定义自动配置开启注解
java 复制代码
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@MyImport(MyAutoConfigurationImportSelector.class) // 导入选择器
public @interface MyEnableAutoConfiguration {
}
第四步:增强启动注解

把自动配置能力组合到启动注解上,和原生 @SpringBootApplication 完全一致。

java 复制代码
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@MyComponentScan
@MyEnableAutoConfiguration // 叠加自动配置能力
public @interface MySpringBootApplication {
}

4. 自动配置类示例

配合条件注解,实现 "满足条件才生效"。

java 复制代码
/**
 * Tomcat自动配置类
 * 只有类路径下存在Tomcat相关类时,才会注册进容器
 */
@MyConfiguration
@MyConditionalOnClass("org.apache.catalina.startup.Tomcat")
public class TomcatAutoConfiguration {

    @MyBean
    public TomcatWebServer tomcatWebServer() {
        return new TomcatWebServer();
    }
}

5. 常见踩坑

  • 文件格式错误:类名写错、多空格、逗号用了中文格式,都会导致类加载失败,自动配置不生效。
  • 条件顺序错误@ConditionalOnClass 是类加载级别的判断,要在 Bean 定义注册前执行;@ConditionalOnMissingBean 是容器级别的判断,要在所有 Bean 定义注册后执行,顺序不能乱。
  • 包扫描不到:自动配置类不在启动类扫描范围内,所以必须靠 spring.factories 机制发现,不能靠包扫描。

四、Spring Boot 整合 Tomcat 底层源码分析

1. 内嵌 Tomcat 的核心思想

传统 SSM 项目需要外部 Tomcat,打 War 包部署;Spring Boot 则是把 Tomcat 封装成一个普通的 Bean,放在 Spring 容器里管理,容器启动时顺便启动 Tomcat,直接打 Jar 包就能运行。

2. 手写内嵌 Tomcat 启动

第一步:引入依赖
XML 复制代码
<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-core</artifactId>
    <version>9.0.65</version>
</dependency>
第二步:封装 TomcatWebServer

对标 Spring Boot 的 TomcatWebServer,封装启动停止逻辑。

java 复制代码
public class TomcatWebServer implements MyWebServer {

    private Tomcat tomcat;
    private final int port = 8080;

    @Override
    public void start() {
        try {
            // 1. 创建Tomcat核心实例
            tomcat = new Tomcat();
            // 2. 设置服务端口
            tomcat.setPort(port);
            // 3. 设置临时工作目录
            tomcat.setBaseDir(System.getProperty("java.io.tmpdir"));

            // 4. 创建Web应用上下文,对应一个项目
            Context context = tomcat.addContext("/", System.getProperty("java.io.tmpdir"));

            // 5. 注册Servlet(真实Spring MVC会注册DispatcherServlet)
            Tomcat.addServlet(context, "demoServlet", new HttpServlet() {
                @Override
                protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
                    resp.setContentType("text/html;charset=utf-8");
                    resp.getWriter().write("<h1>MySpringBoot 内嵌Tomcat运行成功</h1>");
                }
            }).setLoadOnStartup(1);

            // 6. 配置Servlet映射路径
            context.addServletMappingDecoded("/", "demoServlet");

            // 7. 启动Tomcat
            tomcat.start();
            System.out.println("内嵌Tomcat启动完成,端口:" + port);

            // 8. 阻塞等待请求,防止主线程退出
            tomcat.getServer().await();
        } catch (LifecycleException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void stop() {
        try {
            tomcat.stop();
        } catch (LifecycleException e) {
            e.printStackTrace();
        }
    }
}

3. 集成到启动流程

MySpringApplication.run 中,容器刷新后启动 Web 服务器:

java 复制代码
public static MyAnnotationConfigApplicationContext run(Class<?> primarySource) {
    MyAnnotationConfigApplicationContext context = new MyAnnotationConfigApplicationContext();
    context.register(primarySource);
    context.refresh();

    // 如果容器中有WebServer Bean,就启动它
    if (context.containsBean("tomcatWebServer")) {
        MyWebServer webServer = (MyWebServer) context.getBean("tomcatWebServer");
        // 异步启动,避免阻塞主线程
        new Thread(webServer::start).start();
    }

    System.out.println("MySpringBoot 启动完成");
    return context;
}

4. 源码级细节说明

  1. WebServer 抽象 :Spring Boot 定义了 WebServer 接口,有 Tomcat、Jetty、Undertow 三个实现,通过条件注解自动切换,这就是能无缝更换 Web 容器的原因。
  2. 启动时机 :真实源码中,Tomcat 启动在 ServletWebServerApplicationContextonRefresh() 方法里,属于容器刷新流程的一环,不是刷新完才启动。
  3. 零配置原理 :Spring MVC 的 DispatcherServlet 也是靠自动配置类自动创建并注册到 Tomcat 的,不用我们写 web.xml。

五、自动配置类加载全流程源码梳理

1. 完整执行链路图

2. 关键扩展机制

  • 排除机制@SpringBootApplication(exclude = ...) 可以手动排除不需要的自动配置类,本质是在过滤阶段移除。
  • 排序机制@AutoConfigureBefore@AutoConfigureAfter 控制配置类加载顺序,比如数据源配置必须在连接池配置之后。
  • 属性绑定@ConfigurationProperties 把 yml 配置绑定到 JavaBean,自动配置类通过它读取用户自定义配置,覆盖默认值。

3. 用户配置优先的实现

核心就是 @ConditionalOnMissingBean

  • 所有自动配置的默认 Bean 都加了这个注解
  • 用户如果自己注册了同类型的 Bean,自动配置的 Bean 就不会创建
  • 保证用户自定义的优先级最高,自动配置只做兜底

六、面试速记总结

  1. Spring Boot 启动核心三步:创建容器 → 包扫描注册 Bean 定义 → 刷新容器实例化 Bean,Web 环境额外启动内嵌 Tomcat。
  2. 条件注解核心@Conditional 是元注解,Spring Boot 扩展了类存在、Bean 缺失等常用条件;判断发生在 BeanDefinition 注册阶段。
  3. 自动配置本质spring.factories SPI 机制批量发现配置类 + 条件注解按需加载,实现第三方 Starter 开箱即用。
  4. 内嵌 Tomcat 原理:把 Tomcat 封装成容器中的 Bean,随 Spring 容器刷新而启动,替代外部 Web 容器。
  5. 手写的意义:剥掉 Spring Boot 的层层封装,核心还是注解解析、反射、类加载、SPI 这些 Java 基础技术;理解了底层,排查自动配置失效、启动失败等问题会有所帮助。
相关推荐
青山是哪个青山8 小时前
LangChain 学习笔记(四):Message 与提示词模板
笔记·学习·langchain
hsjiasb9 小时前
FreeRTOS学习(二十六)——动态内存管理heap_1到heap_5
stm32·单片机·学习·学习笔记·freertos
崇子嵘9 小时前
基于zynqMP15eg的linux驱动学习
学习
东风破_9 小时前
ESLint 是什么?为什么你的项目需要它?
前端·后端·代码规范
嘻哈∠※9 小时前
0061基于 SpringBoot 的投稿与稿件处理系统设计与实现
java·spring boot·后端
卷无止境10 小时前
在 awesome-fastapi 里,哪些库值得一看?
后端·python
知识分享小能手11 小时前
线性代数学习教程,从入门到精通,向量组的线性相关性 — 完整知识点梳理(7)
学习·线性代数·机器学习
卷无止境11 小时前
FastAPI 的Admin面板生态
后端·python
Shell运维手记11 小时前
Linux 常用基础命令学习笔记
linux·运维·笔记·学习·算法·github
动词ing12 小时前
【学习笔记】C语言(数组指针与指针数组+字符数组+函数+参数传递+字符串作为形参+递归函数+指针函数+回调函数+结构体嵌套+内存动态分配函数)
c语言·笔记·学习