• Spring Boot 2.x源码系列【4】启动流程深入解析之启动监听器


    有道无术,术尚可求,有术无道,止于术。

    本系列Spring Boot版本2.7.0

    前言

    紧接上文,接下来分析下面这几步:

    		ConfigurableApplicationContext context = null;
    		// 配置Headless
    		configureHeadlessProperty();
    		// 获取监听器集合
    		SpringApplicationRunListeners listeners = getRunListeners(args);
    		// 调用监听器启动方法
    		listeners.starting(bootstrapContext, this.mainApplicationClass);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    核心类及接口

    ConfigurableApplicationContext

    ConfigurableApplicationContext 是Spring 提供的接口,翻译过来就是可配置应用上下文的意思,可以看到它继承了很多接口,上一级继承的接口有:

    • ApplicationContext:引用上下文接口,也就是IOC容器接口
    • Lifecycle:容器生命周期接口
    • Closeable:JDK释放资源接口
      在这里插入图片描述

    该接口的源码如下:

    public interface ConfigurableApplicationContext extends ApplicationContext, Lifecycle, Closeable {
    	// 配置文件路径分隔符,主要用来分割各个配置文件路径
        String CONFIG_LOCATION_DELIMITERS = ",; \t\n";
        // 转换服务的bean 名称,通过这个服务,我们则可以实现类型转换操作
        String CONVERSION_SERVICE_BEAN_NAME = "conversionService";
        // 工厂中LoadTimeWeaver bean的名称
        String LOAD_TIME_WEAVER_BEAN_NAME = "loadTimeWeaver";
        // 环境{@link-Environment}bean的名称。
        String ENVIRONMENT_BEAN_NAME = "environment";
        // 系统属性的bean 名称
        String SYSTEM_PROPERTIES_BEAN_NAME = "systemProperties";
        // 系统环境的bean 名称
        String SYSTEM_ENVIRONMENT_BEAN_NAME = "systemEnvironment";
        //  {@link ApplicationStartup} 的bean 名称
        String APPLICATION_STARTUP_BEAN_NAME = "applicationStartup";
        // 关闭钩子函数线程名称
        String SHUTDOWN_HOOK_THREAD_NAME = "SpringContextShutdownHook";
    	// 设置应用容器的唯一id
        void setId(String id);
    	// 设置父级容器(上下文)
        void setParent(@Nullable ApplicationContext parent);
    	// 设置当前容器环境
        void setEnvironment(ConfigurableEnvironment environment);
    	// 返回此应用程序上下文的环境,允许进一步自定义
        ConfigurableEnvironment getEnvironment();
    	// 设置ApplicationStartup
        void setApplicationStartup(ApplicationStartup applicationStartup);
    	// 获取 ApplicationStartup
        ApplicationStartup getApplicationStartup();
    	// 添加BeanFactoryPostProcessor
        void addBeanFactoryPostProcessor(BeanFactoryPostProcessor postProcessor);
    	// 添加容器监听器,主要是指继承了ApplicationListener的监听器
        void addApplicationListener(ApplicationListener<?> listener);
    	// 设置类加载器
        void setClassLoader(ClassLoader classLoader);
    	// 注册协议解析器。协议解析器的作用就是根据指定的地址和资源加载期,解析资源并将资源返回
        void addProtocolResolver(ProtocolResolver resolver);
    	// 核心方法:加载|刷新 整个容器
        void refresh() throws BeansException, IllegalStateException;
    	// 注册关闭钩子
        void registerShutdownHook();
    	// 关闭容器
        void close();
    	// 获取容器是否活跃
        boolean isActive();
    	// 获取bean factory
        ConfigurableListableBeanFactory getBeanFactory() throws IllegalStateException;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    由上可知,ConfigurableApplicationContext 就是Spring Boot中的应用上下文对象,也就是Spring 容器。

    SpringApplicationRunListener

    SpringApplicationRunListenerSpring Boot提供的接口,从字面上看,它是一个Spring 应用启动监听器。

    学过Spring 的应该知道,有个事件监听器机制,是在观察者模式基础上的进一步抽象和改进,定义了一个ApplicationListener接口作为事件监听器的抽象,当监听的事件在容器中被发布,onApplicationEvent方法将被调用。

    public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
    
       /**
        * Handle an application event.
        * @param event the event to respond to
        */
       void onApplicationEvent(E event);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    SpringApplicationRunListener就是用来在整个启动流程中接收不同执行点事件通知的监听者,可以在接口定义中看到,一旦启动阶段发布了事件,该监听器就会观察到事件被做出相应操作。

    public interface SpringApplicationRunListener {
    	// 首次启动run方法时立即调用。可用于非常早期初始化。
        default void starting(ConfigurableBootstrapContext bootstrapContext) {
        }
    	// 环境准备就绪,但在{@link ApplicationContext}创建之前。
        default void environmentPrepared(ConfigurableBootstrapContext bootstrapContext, ConfigurableEnvironment environment) {
        }
    	// 在创建和准备{@link ApplicationContext}后,但在加载源之前调用。
        default void contextPrepared(ConfigurableApplicationContext context) {
        }
    	// 在加载应用程序上下文之后但在加载之前调用已刷新。
        default void contextLoaded(ConfigurableApplicationContext context) {
        }
    	// 上下文已刷新,应用程序已启动,但{@link CommandLineRunner CommandLineRunner}和{@link ApplicationRunner}未调用。
        default void started(ConfigurableApplicationContext context, Duration timeTaken) {
            this.started(context);
        }
    	
        /** @deprecated */
        @Deprecated
        default void started(ConfigurableApplicationContext context) {
        }
    	// 当应用程序上下文已刷新且所有{@link CommandLineRunner CommandLineRunner}和{@link ApplicationRunner ApplicationRunners}已调用。
        default void ready(ConfigurableApplicationContext context, Duration timeTaken) {
            this.running(context);
        }
    	
        /** @deprecated */
        @Deprecated
        default void running(ConfigurableApplicationContext context) {
        }
    	// 运行应用程序时发生故障时调用。
        default void failed(ConfigurableApplicationContext context, Throwable exception) {
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    ApplicationListener

    ApplicationListener是Spring 中的监听器接口,可以看到在spring.factories中定义了7种监听器。
    在这里插入图片描述
    SpringApplication对象在创建的时候,会去获取这些监听器。

        public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
        // ..........
            this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));
        }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5

    ApplicationStartup

    ApplicationStartup 字面上看是应用启动的意思,它的作用是标记应用程序启动期间的步骤,并收集有关执行上下文或其处理时间的数据。

    该接口只有一个start方法,传入一个步骤名称参数,描述当前操作或阶段,然后创建一个步骤对象并标记它的开始,然后返回当前StartupStep (启动步骤)对象。

    public interface ApplicationStartup {
        ApplicationStartup DEFAULT = new DefaultApplicationStartup();
    
        StartupStep start(String name);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    可理解为当前接口的功能就是记录每一个启动步骤的相关检测数据。

    1. 设置Headless

    private void configureHeadlessProperty() {
    		// java.awt.headless
    		System.setProperty(SYSTEM_PROPERTY_JAVA_AWT_HEADLESS,
    				System.getProperty(SYSTEM_PROPERTY_JAVA_AWT_HEADLESS, Boolean.toString(this.headless)));
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5

    configureHeadlessProperty方法很简单,就是设置了一个java.awt.headless系统参数,默认为true,表示开启Headless(中文无头、无外设的意思)模式。

    程序在运行时,可能需要使用外设(显示屏、键盘、鼠标)或者获取外设的信息,但一般部署的服务器是没有这些设备的,这个时候就可以开启Headless模式,告诉程序当前环境没有外设,需要依靠系统的计算能力模拟出这些外设特性。

    2. 获取监听器

    getRunListeners方法获取监听器,返回一个SpringApplicationRunListeners 对象。

    	private SpringApplicationRunListeners getRunListeners(String[] args) {
    		// 1. 创建一个数组,存放SpringApplication.class、String[].class
    		Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
    		return new SpringApplicationRunListeners(logger,
    				// SPI机制,从spring.factories 中获取SpringApplicationRunListener实例
    				getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args),
    				this.applicationStartup);
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    这里可以看到又是使用SPI 机制从META-INF/spring.factories中获取SpringApplicationRunListener类型的实例,可以在源码spring-boot模块中看到只配置了一个该类型的监听器EventPublishingRunListener
    在这里插入图片描述
    最后将获取到监听器,放入到SpringApplicationRunListeners 对象的成员属性中:
    在这里插入图片描述

    3. 开启步骤

    接着进入到SpringApplicationRunListeners 对象的starting方法,可以看到在该方法中,直接转到了doWithListeners方法,参数为一个字符串,两个为Consumer函数式接口的匿名内部类。

        void starting(ConfigurableBootstrapContext bootstrapContext, Class<?> mainApplicationClass) {
            this.doWithListeners("spring.boot.application.starting", (listener) -> {
                listener.starting(bootstrapContext);
            }, (step) -> {
                if (mainApplicationClass != null) {
                    step.tag("mainApplicationClass", mainApplicationClass.getName());
                }
    
            });
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    接着进入到其doWithListeners方法,可以看到在该方法中,会调用所有监听器的starting 方法,并记录一个名称为spring.boot.application.starting 的步骤,所有监听器的starting 方法执行完成后,该步骤结束。

        private void doWithListeners(String stepName, Consumer<SpringApplicationRunListener> listenerAction, Consumer<StartupStep> stepAction) {
        	// 1. 步骤名为spring.boot.application.starting 
            StartupStep step = this.applicationStartup.start(stepName);
            // 2. 循环所有监听器,调用其starting 方法
            this.listeners.forEach(listenerAction);
            // 3. 记录步骤标记 
            if (stepAction != null) {
                stepAction.accept(step);
            }
    		// 4. 步骤结束。
            step.end();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    4. 广播事件

    因为默认只有一个EventPublishingRunListener监听器,所以这里会进入到该监听器中,会调用SimpleApplicationEventMulticaster对象的multicastEvent方法,参数为ApplicationStartingEvent,该方法从名字上看是进行事件广播,它的作用是会将启动事件,发布给所有的监听器。

        public void starting(ConfigurableBootstrapContext bootstrapContext) {
        	// 广播事件
            this.initialMulticaster.multicastEvent(new ApplicationStartingEvent(bootstrapContext, this.application, this.args));
        }
    
    • 1
    • 2
    • 3
    • 4

    ApplicationStartingEvent对象比较简单,就是一个应用启动事件对象,封装了引导上下文和SpringApplication对象。
    在这里插入图片描述

    接着进入到SimpleApplicationEventMulticaster对象的multicastEvent方法:

        public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
        	// 1. 解析事件的类型=》ApplicationStartingEvent
            ResolvableType type = eventType != null ? eventType : this.resolveDefaultEventType(event);
            // 2. 获取线程池,初次为NULL 
            Executor executor = this.getTaskExecutor();        
            Iterator var5 = this.getApplicationListeners(event, type).iterator();
    		// 3. 循环获取到的监听器,并调用监听器的监听方法
            while(var5.hasNext()) {
                ApplicationListener<?> listener = (ApplicationListener)var5.next();
                if (executor != null) {
                    executor.execute(() -> {
                        this.invokeListener(listener, event);
                    });
                } else {
                	// 线程池不存在,所以调用这一步
                    this.invokeListener(listener, event);
                }
            }
    
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    getApplicationListeners方法会获取所有支持当前事件的的监听器,不匹配的提前被排除在外。

    	protected Collection<ApplicationListener<?>> getApplicationListeners(
    			ApplicationEvent event, ResolvableType eventType) {
    		// 1. 当前事件的来源,SpringApplication
    		Object source = event.getSource();
    		Class<?> sourceType = (source != null ? source.getClass() : null);
    		// 2. 创建缓存的 KEY 
    		ListenerCacheKey cacheKey = new ListenerCacheKey(eventType, sourceType);
    		// Potential new retriever to populate
    		CachedListenerRetriever newRetriever = null;
    		
    		// 3. 缓存中查询是有
    		CachedListenerRetriever existingRetriever = this.retrieverCache.get(cacheKey);
    		if (existingRetriever == null) {
    			// Caching a new ListenerRetriever if possible
    			if (this.beanClassLoader == null ||
    					(ClassUtils.isCacheSafe(event.getClass(), this.beanClassLoader) &&
    							(sourceType == null || ClassUtils.isCacheSafe(sourceType, this.beanClassLoader)))) {
    				newRetriever = new CachedListenerRetriever();
    				existingRetriever = this.retrieverCache.putIfAbsent(cacheKey, newRetriever);
    				if (existingRetriever != null) {
    					newRetriever = null;  // no need to populate it in retrieveApplicationListeners
    				}
    			}
    		}
    		if (existingRetriever != null) {
    			Collection<ApplicationListener<?>> result = existingRetriever.getApplicationListeners();
    			if (result != null) {
    				return result;
    			}
    			// If result is null, the existing retriever is not fully populated yet by another thread.
    			// Proceed like caching wasn't possible for this current local attempt.
    		}
    		// 4. 缓存中没有,所以进入该方法
    		return retrieveApplicationListeners(eventType, sourceType, newRetriever);
    	}
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    因为缓存中没有,所以进入retrieveApplicationListeners方法,查询监听器,是之前SpringApplication对象创建的时候从spring.factries中加载的。

        private Collection<ApplicationListener<?>> retrieveApplicationListeners(ResolvableType eventType, @Nullable Class<?> sourceType, @Nullable AbstractApplicationEventMulticaster.CachedListenerRetriever retriever) {
        	// 1. 创建一个集合对象,用于存放ApplicationListener(spring 提供的监听器) 
            List<ApplicationListener<?>> allListeners = new ArrayList();
            Set<ApplicationListener<?>> filteredListeners = retriever != null ? new LinkedHashSet() : null;
            Set<String> filteredListenerBeans = retriever != null ? new LinkedHashSet() : null;
            LinkedHashSet listeners; // 所有的ApplicationListener
            LinkedHashSet listenerBeans;
            // 从默认的持有着中获取监听器,
            synchronized(this.defaultRetriever) {
                listeners = new LinkedHashSet(this.defaultRetriever.applicationListeners);
                listenerBeans = new LinkedHashSet(this.defaultRetriever.applicationListenerBeans);
            }
    
            Iterator var9 = listeners.iterator();
    		// 2. 循环所有的ApplicationListener,校验这些监听器是否支持ApplicationStartingEvent 事件
    		// LoggingApplicationListener,BackgroundPreinitializer,DelegatingApplicationListener
            while(var9.hasNext()) {
                ApplicationListener<?> listener = (ApplicationListener)var9.next();
                if (this.supportsEvent(listener, eventType, sourceType)) {
                    if (retriever != null) {
                        filteredListeners.add(listener);
                    }
    
                    allListeners.add(listener);
                }
            }
    		// 3. 没有查到这些监听器的Bean 对象,没有这里为空,直接跳过
    		// 该方法根据注册的Bean ,去查询是否支持事件,并添加到监听器集合中
            if (!listenerBeans.isEmpty()) {
                ConfigurableBeanFactory beanFactory = this.getBeanFactory();
                Iterator var16 = listenerBeans.iterator();
    
                while(var16.hasNext()) {
                    String listenerBeanName = (String)var16.next();
    
                    try {
                        if (this.supportsEvent(beanFactory, listenerBeanName, eventType)) {
                            ApplicationListener<?> listener = (ApplicationListener)beanFactory.getBean(listenerBeanName, ApplicationListener.class);
                            if (!allListeners.contains(listener) && this.supportsEvent(listener, eventType, sourceType)) {
                                if (retriever != null) {
                                    if (beanFactory.isSingleton(listenerBeanName)) {
                                        filteredListeners.add(listener);
                                    } else {
                                        filteredListenerBeans.add(listenerBeanName);
                                    }
                                }
    
                                allListeners.add(listener);
                            }
                        } else {
                            Object listener = beanFactory.getSingleton(listenerBeanName);
                            if (retriever != null) {
                                filteredListeners.remove(listener);
                            }
    
                            allListeners.remove(listener);
                        }
                    } catch (NoSuchBeanDefinitionException var13) {
                    }
                }
            }
    
            AnnotationAwareOrderComparator.sort(allListeners);
            // 4. 将这些监听器和Bean放入到缓存中
            if (retriever != null) {
                if (filteredListenerBeans.isEmpty()) {
                    retriever.applicationListeners = new LinkedHashSet(allListeners);
                    retriever.applicationListenerBeans = filteredListenerBeans;
                } else {
                    retriever.applicationListeners = filteredListeners;
                    retriever.applicationListenerBeans = filteredListenerBeans;
                }
            }
    
            return allListeners;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76

    5. 调用Spring 监听器

    接着将符合条件的监听器返回,可以看到,缓存中存放了三个监听器:
    在这里插入图片描述

    接着循环执行执行监听器的doInvokeListener方法,进入调用三个监听器的onApplicationEvent方法。

        private void doInvokeListener(ApplicationListener listener, ApplicationEvent event) {
            try {
                listener.onApplicationEvent(event);
            } catch (ClassCastException var6) {
                // 省略异常捕获代码 
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    首先循环的是LoggingApplicationListener(日志监听器),它的作用很重要,会完成日志系统的加载和初始化,默认使用的是Logback日志组件。

        private void onApplicationStartingEvent(ApplicationStartingEvent event) {
        	// 使用ClassLoader 加载日志系统,默认是LogbackLoggingSystem 
            this.loggingSystem = LoggingSystem.get(event.getSpringApplication().getClassLoader());
            // 日志系统初始化
            this.loggingSystem.beforeInitialize();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    接着进入到BackgroundPreinitializer(后台预先初始化监听器),该监听器的作用是将一些比较耗时的组件开启后台线程预先初始化,这里是刚开始启动阶段,所有这里也有没进行操作。

        public void onApplicationEvent(SpringApplicationEvent event) {
        	// 1. 查看spring.backgroundpreinitializer.ignore 配置,是否开启预先初始化
            if (ENABLED) {
            	// 2. 是否环境准备事件,是的话,会开启异步线程,初始化Jackson、Charset、ConversionService、Validation、MessageConverter等
                if (event instanceof ApplicationEnvironmentPreparedEvent && preinitializationStarted.compareAndSet(false, true)) {
                    this.performPreinitialization();
                }
                if ((event instanceof ApplicationReadyEvent || event instanceof ApplicationFailedEvent) && preinitializationStarted.get()) {
                    try {
                        preinitializationComplete.await();
                    } catch (InterruptedException var3) {
                        Thread.currentThread().interrupt();
                    }
                }
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    接着DelegatingApplicationListener(委托应用监听器),监听到事件后转发给环境变量context.listener.classes指定的那些事件监听器,这里也是跳过,没有任何操作。

        public void onApplicationEvent(ApplicationEvent event) {
        	// 1. 判断是否是环境准备事件,这里是刚启动,所以跳过
            if (event instanceof ApplicationEnvironmentPreparedEvent) {
                List<ApplicationListener<ApplicationEvent>> delegates = this.getListeners(((ApplicationEnvironmentPreparedEvent)event).getEnvironment());
                if (delegates.isEmpty()) {
                    return;
                }
    
                this.multicaster = new SimpleApplicationEventMulticaster();
                Iterator var3 = delegates.iterator();
    
                while(var3.hasNext()) {
                    ApplicationListener<ApplicationEvent> listener = (ApplicationListener)var3.next();
                    this.multicaster.addApplicationListener(listener);
                }
            }
    
            if (this.multicaster != null) {
                this.multicaster.multicastEvent(event);
            }
    
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    最后,监听器执行完毕,启动开始阶段标记为结束。

    总结

    1. 获取Spring Boot 自己的监听器SpringApplicationRunListener,调用starting开始启动;
    2. 创建步骤spring.boot.application.starting ,调用EventPublishingRunListener进行事件广播;
    3. 获取Spring 监听器ApplicationListener并过滤,最后有三个支持当前事件的监听器;
    4. 循环调用监听器的onApplicationEvent方法,最终只有LoggingApplicationListener(日志监听器)实际执行了处理(初始化日志系统);
    5. 执行完成,开始启动步骤标记为结束。
  • 相关阅读:
    Hadoop参数配置
    Layui + Flask | 弹出层(组件篇)(04)
    vue3快速上手
    Java类是如何默认继承Object的?(转载)
    基于Struts开发物流配送(快递)管理系统
    ViT:Vision transformer的cls token如何实现分类?
    2023下半年软考网管和网工考后分析!
    【必知必会的MySQL知识】①初探MySQL
    Debian衍生桌面项目SpiralLinux12.231001发布
    java基础入门(一)
  • 原文地址:https://blog.csdn.net/qq_43437874/article/details/125448788