程序项目代做,有需求私信(小程序、网站、爬虫、电路板设计、驱动、应用程序开发、毕设疑难问题处理等)

Spring Boot -- 启动流程分析之SpringApplication

我们在开发Spring Boot程序的时候,我们只需要在启动类上加入@SpringBootApplication注解,然后运行SpringApplication.run(),这样Spring容器就运行起来了。

@SpringBootApplication(scanBasePackages={"com.jnu.example"})
@CoreMapperScan
@EnableAspectAutoProxy
public class App {
    public static void main(String[] args) {
        SpringApplication.run(BlogApplication.class, args);
    }
}

那么问题来了,相比最初Spring MVC繁琐的xml的配置方式,现在只需要简单几行代码,Spring容器就可以启动起来,我们就可以从容器中获取到bean,Spring Boot内部是如何做到的呢?

下图是我绘制的Spring启动的流程图,这个图是个精简版,还有许多不完善的地方,后续几篇博客我将会通过源码分析来解读Spring的启动流程:

其中源码版本以2.2.2.RELEASE为例:

    <!--  spring-boot-starter-parent  整合第三方常用框架依赖信息(各种依赖信息)-->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.2.RELEASE</version>
    </parent>

一、SpringApplication准备阶段(构造函数)

SpringApplication 在运行前做了一系列的准备工作,如:推断 Web 应用类型、加载 Spring 上下文初始器、事件监听器等。接下来,就通过源码的方式进行学习。

我们首先进入SpringApplication的静态帮助方法run里面,该方法接受两个参数:

复制代码
    /**
     * Static helper that can be used to run a {@link SpringApplication} from the
     * specified source using default settings.
     * @param primarySource the primary source to load
     * @param args the application arguments (usually passed from a Java main method)
     * @return the running {@link ApplicationContext}
     */
    public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
        return run(new Class<?>[] { primarySource }, args);
    }
复制代码
  • primarySource:要加载的主要配置类,也就是被@SpringBootApplication注解的App类;
  • args:一个可变数组,保存的是应用程序的运行参数,具体可以指定哪些属性,可以查看官网说明:Common Application properties

接下来,调用了另一个静态帮助方法run,并将primarySource转换为数组参数传入;

    public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
        return new SpringApplication(primarySources).run(args);
    }

这里通过入参primarySources创建了一个SpringApplication对象,其中,准备阶段的工作皆在 SpringApplication 的构造器中处理:

复制代码
    /**
     * Create a new {@link SpringApplication} instance. The application context will load
     * beans from the specified primary sources (see {@link SpringApplication class-level}
     * documentation for details. The instance can be customized before calling
     * {@link #run(String...)}.
     * @param primarySources the primary bean sources
     * @see #run(Class, String[])
     * @see #SpringApplication(ResourceLoader, Class...)
     * @see #setSources(Set)
     */
    public SpringApplication(Class<?>... primarySources) {
        this(null, primarySources);
    }
复制代码

这里调用了一个构造方法,然后初始化primarySource,webApplicationType、initializers、listeners、mainApplicationClass等字段:

复制代码
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
    
        // resourceLoader 主要用来获取 Resource 及 ClassLoader。这里值为 null
        this.resourceLoader = resourceLoader;
        
        // 断言primarySources不能为null,否则报错
        Assert.notNull(primarySources, "PrimarySources must not be null");
        
        // primarySources是SpringApplication.run的参数,存放的是主配置类
        this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
        
        // 进行Web应用的类型推断
        this.webApplicationType = WebApplicationType.deduceFromClasspath();
        
        // 加载应用上下文初始化器 initializer
        setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
        
        // 加载应用事件监听器 listener
        setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
        
        // 推断引导类,也就是找到入口类
        this.mainApplicationClass = deduceMainApplicationClass();
    }
复制代码

1.1、推断Web应用类型

// 进行Web应用的类型推断
this.webApplicationType = WebApplicationType.deduceFromClasspath();

SpringApplication允许指定应用的类型,大体上分为Web应用和非Web应用。从Spring Boot2.0开始,Web应用又可以分为Servlet Web和Reactive Web。而在准备阶段,是通过检查当前ClassPath下某些Class是否存在,从而推导Web应用的类型:

复制代码
/**
 * An enumeration of possible types of web application.
 *
 * @author Andy Wilkinson
 * @author Brian Clozel
 * @since 2.0.0
 */
public enum WebApplicationType {

    /**
     * The application should not run as a web application and should not start an
     * embedded web server.
     */
    NONE,

    /**
     * The application should run as a servlet-based web application and should start an
     * embedded servlet web server.
     */
    SERVLET,

    /**
     * The application should run as a reactive web application and should start an
     * embedded reactive web server.
     */
    REACTIVE;

    private static final String[] SERVLET_INDICATOR_CLASSES = { "javax.servlet.Servlet",
            "org.springframework.web.context.ConfigurableWebApplicationContext" };

    private static final String WEBMVC_INDICATOR_CLASS = "org.springframework.web.servlet.DispatcherServlet";

    private static final String WEBFLUX_INDICATOR_CLASS = "org.springframework.web.reactive.DispatcherHandler";

    private static final String JERSEY_INDICATOR_CLASS = "org.glassfish.jersey.servlet.ServletContainer";

    private static final String SERVLET_APPLICATION_CONTEXT_CLASS = "org.springframework.web.context.WebApplicationContext";

    private static final String REACTIVE_APPLICATION_CONTEXT_CLASS = "org.springframework.boot.web.reactive.context.ReactiveWebApplicationContext";

    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;
    }

    static WebApplicationType deduceFromApplicationContext(Class<?> applicationContextClass) {
        if (isAssignable(SERVLET_APPLICATION_CONTEXT_CLASS, applicationContextClass)) {
            return WebApplicationType.SERVLET;
        }
        if (isAssignable(REACTIVE_APPLICATION_CONTEXT_CLASS, applicationContextClass)) {
            return WebApplicationType.REACTIVE;
        }
        return WebApplicationType.NONE;
    }

    private static boolean isAssignable(String target, Class<?> type) {
        try {
            return ClassUtils.resolveClassName(target, null).isAssignableFrom(type);
        }
        catch (Throwable ex) {
            return false;
        }
    }

}
复制代码

可以看到,在方法中利用 ClassUtils.isPresent 进行判断, 当DispatcherHandler存在,而DispatcherServlet和ServletContainer不存在时,则当前应用推导为 Reactive web 类型;当 Servlet 和 ConfigurableWebApplicationContext 不存在时,当前应用为非 Web 类型;其他的则为 Servlet Web 类型。

注:Reactive:Reactive响应式编程是一种新的编程风格,其特点是异步或并发、事件驱动、推送PUSH机制以及观察者模式的衍生。

该函数执行完后,结果如下:

1.2、加载应用上下文初始器ApplicationContextInitializer

setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class))

接着进入加载Spring应用上下文初始器的过程;

复制代码
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();
        // Use names and ensure unique to protect against duplicates
        Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
        List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
        AnnotationAwareOrderComparator.sort(instances);
        return instances;
    }
复制代码

可以看到,这里是通过 Spring 工厂加载机制 SpringFactoriesLoader.loadFactoryNames(type, classLoader) 方法获取,采用这种方式可以将非本项目的外部包的bean加载到Spring容器中,比如一些第三方模块采用这种方式实现@Enable模块的功能,具体可以看考博客:Spring Boot自定义Starter

该方法是从项目引用的所有的jar的 META-INF/spring.factories 资源中获取key为 ApplicationContextInitializer 的实现类集合,如下是 spring-boot-autoconfigure 包下的 spring.factories 文件:

# Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\
org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener

这里获取的就是 SharedMetadataReaderFactoryContextInitializer 和 ConditionEvaluationReportLoggingListener 上下文初始化器,接下来通过 createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names) 方法初始化这些实现类:

复制代码
 @SuppressWarnings("unchecked")
    private <T> List<T> createSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes,
            ClassLoader classLoader, Object[] args, Set<String> names) {
        List<T> instances = new ArrayList<>(names.size());
        for (String name : names) {
            try {
                Class<?> instanceClass = ClassUtils.forName(name, classLoader);
                Assert.isAssignable(type, instanceClass);
                Constructor<?> constructor = instanceClass.getDeclaredConstructor(parameterTypes);
                T instance = (T) BeanUtils.instantiateClass(constructor, args);
                instances.add(instance);
            }
            catch (Throwable ex) {
                throw new IllegalArgumentException("Cannot instantiate " + type + " : " + name, ex);
            }
        }
        return instances;
    }
复制代码

这里先通过 BeanUtils.instantiate 初始化这些类,然后将初始化的类保存至List进行返回。

并进行排序操作,最后添加到SpringApplication的initializers集合变量中。至此,该流程结束。 

    /**
     * Sets the {@link ApplicationContextInitializer} that will be applied to the Spring
     * {@link ApplicationContext}.
     * @param initializers the initializers to set
     */
    public void setInitializers(Collection<? extends ApplicationContextInitializer<?>> initializers) {
        this.initializers = new ArrayList<>(initializers);
    }

我们举例来看看初始器中的内容,如SharedMetadataReaderFactoryContextInitializer:

复制代码
/*
 * Copyright 2012-2019 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.boot.autoconfigure;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.boot.type.classreading.ConcurrentReferenceCachingMetadataReaderFactory;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigUtils;
import org.springframework.context.annotation.ConfigurationClassPostProcessor;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.core.Ordered;
import org.springframework.core.PriorityOrdered;
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.core.type.classreading.MetadataReaderFactory;

/**
 * {@link ApplicationContextInitializer} to create a shared
 * {@link CachingMetadataReaderFactory} between the
 * {@link ConfigurationClassPostProcessor} and Spring Boot.
 *
 * @author Phillip Webb
 */
class SharedMetadataReaderFactoryContextInitializer
        implements ApplicationContextInitializer<ConfigurableApplicationContext>, Ordered {

    public static final String BEAN_NAME = "org.springframework.boot.autoconfigure."
            + "internalCachingMetadataReaderFactory";

    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        applicationContext.addBeanFactoryPostProcessor(new CachingMetadataReaderFactoryPostProcessor());
    }

    @Override
    public int getOrder() {
        return 0;
    }

    /**
     * {@link BeanDefinitionRegistryPostProcessor} to register the
     * {@link CachingMetadataReaderFactory} and configure the
     * {@link ConfigurationClassPostProcessor}.
     */
    private static class CachingMetadataReaderFactoryPostProcessor
            implements BeanDefinitionRegistryPostProcessor, PriorityOrdered {

        @Override
        public int getOrder() {
            // Must happen before the ConfigurationClassPostProcessor is created
            return Ordered.HIGHEST_PRECEDENCE;
        }

        @Override
        public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        }

        @Override
        public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
            register(registry);
            configureConfigurationClassPostProcessor(registry);
        }

        private void register(BeanDefinitionRegistry registry) {
            BeanDefinition definition = BeanDefinitionBuilder
                    .genericBeanDefinition(SharedMetadataReaderFactoryBean.class, SharedMetadataReaderFactoryBean::new)
                    .getBeanDefinition();
            registry.registerBeanDefinition(BEAN_NAME, definition);
        }

        private void configureConfigurationClassPostProcessor(BeanDefinitionRegistry registry) {
            try {
                BeanDefinition definition = registry
                        .getBeanDefinition(AnnotationConfigUtils.CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME);
                definition.getPropertyValues().add("metadataReaderFactory", new RuntimeBeanReference(BEAN_NAME));
            }
            catch (NoSuchBeanDefinitionException ex) {
            }
        }

    }

    /**
     * {@link FactoryBean} to create the shared {@link MetadataReaderFactory}.
     */
    static class SharedMetadataReaderFactoryBean
            implements FactoryBean<ConcurrentReferenceCachingMetadataReaderFactory>, BeanClassLoaderAware,
            ApplicationListener<ContextRefreshedEvent> {

        private ConcurrentReferenceCachingMetadataReaderFactory metadataReaderFactory;

        @Override
        public void setBeanClassLoader(ClassLoader classLoader) {
            this.metadataReaderFactory = new ConcurrentReferenceCachingMetadataReaderFactory(classLoader);
        }

        @Override
        public ConcurrentReferenceCachingMetadataReaderFactory getObject() throws Exception {
            return this.metadataReaderFactory;
        }

        @Override
        public Class<?> getObjectType() {
            return CachingMetadataReaderFactory.class;
        }

        @Override
        public boolean isSingleton() {
            return true;
        }

        @Override
        public void onApplicationEvent(ContextRefreshedEvent event) {
            this.metadataReaderFactory.clearCache();
        }

    }

}
View Code
复制代码

可以看到该类实现了 Spring 的 ApplicationContextInitializer 接口,并重写了initialize()方法。同理,其他的 Initializer 接口也是类似实现。 而在这里则是在上下文中加入了 CachingMetadataReaderFactoryPostProcessor bean工厂后置处理器。

ApplicationContextInitializer接口的主要作用是在 ConfigurableApplicationContext#refresh()方法调用之前做一些初始化工作。

1.3、加载应用事件监听器ApplicationListener

setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class))

接着加载应用事件监听器 ,过程与“加载应用上下文初始器”基本一致,同样是调用 getSpringFactoriesInstances 方法,不过这里获取的是key为ApplicationListener 的对象集合:

如下是spring-boot-autoconfigure包下的spring.factories文件:

# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.autoconfigure.BackgroundPreinitializer

最后,将获取的 BackgroundPreinitializer对象通过setListeners方法放入listeners 属性变量中:

    /**
     * Sets the {@link ApplicationListener}s that will be applied to the SpringApplication
     * and registered with the {@link ApplicationContext}.
     * @param listeners the listeners to set
     */
    public void setListeners(Collection<? extends ApplicationListener<?>> listeners) {
        this.listeners = new ArrayList<>(listeners);
    }

我们同样举例,来看看监听器中的内容,如BackgroundPreinitializer:

复制代码
/*
 * Copyright 2012-2019 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.boot.autoconfigure;

import java.nio.charset.StandardCharsets;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;

import javax.validation.Configuration;
import javax.validation.Validation;

import org.springframework.boot.context.event.ApplicationFailedEvent;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.boot.context.event.ApplicationStartingEvent;
import org.springframework.boot.context.event.SpringApplicationEvent;
import org.springframework.boot.context.logging.LoggingApplicationListener;
import org.springframework.context.ApplicationListener;
import org.springframework.core.annotation.Order;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;

/**
 * {@link ApplicationListener} to trigger early initialization in a background thread of
 * time consuming tasks.
 * <p>
 * Set the {@link #IGNORE_BACKGROUNDPREINITIALIZER_PROPERTY_NAME} system property to
 * {@code true} to disable this mechanism and let such initialization happen in the
 * foreground.
 *
 * @author Phillip Webb
 * @author Andy Wilkinson
 * @author Artsiom Yudovin
 * @since 1.3.0
 */
@Order(LoggingApplicationListener.DEFAULT_ORDER + 1)
public class BackgroundPreinitializer implements ApplicationListener<SpringApplicationEvent> {

    /**
     * System property that instructs Spring Boot how to run pre initialization. When the
     * property is set to {@code true}, no pre-initialization happens and each item is
     * initialized in the foreground as it needs to. When the property is {@code false}
     * (default), pre initialization runs in a separate thread in the background.
     * @since 2.1.0
     */
    public static final String IGNORE_BACKGROUNDPREINITIALIZER_PROPERTY_NAME = "spring.backgroundpreinitializer.ignore";

    private static final AtomicBoolean preinitializationStarted = new AtomicBoolean(false);

    private static final CountDownLatch preinitializationComplete = new CountDownLatch(1);

    @Override
    public void onApplicationEvent(SpringApplicationEvent event) {
        if (!Boolean.getBoolean(IGNORE_BACKGROUNDPREINITIALIZER_PROPERTY_NAME)
                && event instanceof ApplicationStartingEvent && multipleProcessors()
                && preinitializationStarted.compareAndSet(false, true)) {
            performPreinitialization();
        }
        if ((event instanceof ApplicationReadyEvent || event instanceof ApplicationFailedEvent)
                && preinitializationStarted.get()) {
            try {
                preinitializationComplete.await();
            }
            catch (InterruptedException ex) {
                Thread.currentThread().interrupt();
            }
        }
    }

    private boolean multipleProcessors() {
        return Runtime.getRuntime().availableProcessors() > 1;
    }

    private void performPreinitialization() {
        try {
            Thread thread = new Thread(new Runnable() {

                @Override
                public void run() {
                    runSafely(new ConversionServiceInitializer());
                    runSafely(new ValidationInitializer());
                    runSafely(new MessageConverterInitializer());
                    runSafely(new JacksonInitializer());
                    runSafely(new CharsetInitializer());
                    preinitializationComplete.countDown();
                }

                public void runSafely(Runnable runnable) {
                    try {
                        runnable.run();
                    }
                    catch (Throwable ex) {
                        // Ignore
                    }
                }

            }, "background-preinit");
            thread.start();
        }
        catch (Exception ex) {
            // This will fail on GAE where creating threads is prohibited. We can safely
            // continue but startup will be slightly slower as the initialization will now
            // happen on the main thread.
            preinitializationComplete.countDown();
        }
    }

    /**
     * Early initializer for Spring MessageConverters.
     */
    private static class MessageConverterInitializer implements Runnable {

        @Override
        public void run() {
            new AllEncompassingFormHttpMessageConverter();
        }

    }

    /**
     * Early initializer for javax.validation.
     */
    private static class ValidationInitializer implements Runnable {

        @Override
        public void run() {
            Configuration<?> configuration = Validation.byDefaultProvider().configure();
            configuration.buildValidatorFactory().getValidator();
        }

    }

    /**
     * Early initializer for Jackson.
     */
    private static class JacksonInitializer implements Runnable {

        @Override
        public void run() {
            Jackson2ObjectMapperBuilder.json().build();
        }

    }

    /**
     * Early initializer for Spring's ConversionService.
     */
    private static class ConversionServiceInitializer implements Runnable {

        @Override
        public void run() {
            new DefaultFormattingConversionService();
        }

    }

    private static class CharsetInitializer implements Runnable {

        @Override
        public void run() {
            StandardCharsets.UTF_8.name();
        }

    }

}
View Code
复制代码

可以看到,该类实现了Spring的ApplicationListener 接口,在重写的 onApplicationEvent 方法中触发相应的事件进行操作。同理,其他Listener也是类似实现。而该接口的主要功能是另起一个后台线程触发那些耗时的初始化,包括验证器、消息转换器等等。 

1.4、推断应用引导类

// 推断引导类,也就是找到入口类
this.mainApplicationClass = deduceMainApplicationClass();

准备阶段的最后一步是推断应用的引导类,也就是获取启动 main 方法的类,执行的是 deduceMainApplicationClass() 方法:

复制代码
    private Class<?> deduceMainApplicationClass() {
        try {
            StackTraceElement[] stackTrace = new RuntimeException().getStackTrace();
            for (StackTraceElement stackTraceElement : stackTrace) {
                if ("main".equals(stackTraceElement.getMethodName())) {
                    return Class.forName(stackTraceElement.getClassName());
                }
            }
        }
        catch (ClassNotFoundException ex) {
            // Swallow and continue
        }
        return null;
    }
复制代码

可以看到,通过 getStackTrace()方法获取当前线程的执行栈,再通过 getMethodName()获取方法名,判断是否是main 方法,最后返回main方法的所在类。

二、SpringApplication运行阶段——事件监听机制

前面我们已经讲了 SpringApplication 的构造方法,这里我们就来讲讲 SpringApplication 的核心,也就是run方法:

复制代码
public ConfigurableApplicationContext run(String... args) {
        // 这是 Spring 的一个计时器,计算代码的执行时间(ms级别)
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        
        // 这俩变量在后面赋值处进行说明
        ConfigurableApplicationContext context = null;
        Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
        
        // 用来设置java.awt.headless属性值
        configureHeadlessProperty();
        
        // 该对象属于组合模式的实现,核心是内部关联的 SpringApplicationRunListener 集合,SpringApplicationRunListener 是 Spring Boot 的运行时监听器
        SpringApplicationRunListeners listeners = getRunListeners(args);
        // 会在不同的阶段调用对应的方法,这里表示SpringApplication的run方法刚开始执行
        listeners.starting();
        
        try {
        
            // 用来获取 SpringApplication.run(args)传入的参数
            ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
            
            // 获取 properties 配置文件
            ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
            
            // 设置 spring.beaninfo.ignore 的属性值,判断是否跳过搜索BeanInfo类
            configureIgnoreBeanInfo(environment);
            
            // 这里是项目启动时,控制台打印的 Banner
            Banner printedBanner = printBanner(environment);
            
            // 这里就是创建 Spring 应用上下文
            context = createApplicationContext();
            
            // 获取 spring.factories 中key为 SpringBootExceptionReporter 的类名集合
            exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
                    new Class[] { ConfigurableApplicationContext.class }, context);
                    
            // 这里是准备 Spring 应用上下文
            prepareContext(context, environment, listeners, applicationArguments, printedBanner);
            
            // 这里是启动 Spring 应用上下文,底层调用的是 ApplicationContext 的 refresh() 方法,到这里就正式进入了 Spring 的生命周期,同时,SpringBoot的自动装配特性也随之启动
            refreshContext(context);
            
            // 里面是空的,猜测应该是交由开发人员自行扩展
            afterRefresh(context, applicationArguments);
            stopWatch.stop();
            
            // 这里打印启动信息
            if (this.logStartupInfo) {
                new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
            }
            
            // ApplicationContext 启动时,调用该方法
            listeners.started(context);
            
            // 项目启动后,做的一些操作,开发人员可自行扩展
            callRunners(context, applicationArguments);
        }
        catch (Throwable ex) {
            handleRunFailure(context, ex, exceptionReporters, listeners);
            throw new IllegalStateException(ex);
        }
    
        try {
        
            // ApplicationContext 启动完成时,调用该方法
            listeners.running(context);
        }
        catch (Throwable ex) {
            handleRunFailure(context, ex, exceptionReporters, null);
            throw new IllegalStateException(ex);
        }
        return context;
    }
复制代码

这段代码在最开始使用了事件/监听器模式,因此在需要先介绍一下监听器模式。

2.1、监听器模式

将一个监听器(listener)与特定的控件(如按钮等)绑定起来,当发生用户点击等事件(Event)时,调用监听器的处理方法,从而响应用户的动作,就叫做事件/监听器模式。

下面列出spring boot中主要的应用事件类型:

  • ApplicationFailedEvent:该事件为spring boot启动失败时的操作;
  • ApplicationPreparedEvent:上下文context准备时触发;
  • ApplicationReadyEvent:上下文已经准备完毕的时候触发;
  • ApplicationStartingEvent:SpringApplication刚启动时触发;
  • ApplicationStartedEvent:上下文已经启动时触发;
  • SpringApplicationEvent:获取SpringApplication;
  • ApplicationEnvironmentPreparedEvent:环境事先准备;

我们1.3节中介绍的ApplicationListener的实现类就是监听器,它用于监听某个事件的发生,当某个应用事件发生的时候,就会调用对应的ApplicationListener的onApplicationEvent 方法;

2.2、获取SpringApplicationRunListeners

 // 该对象属于组合模式的实现,核心是内部关联的 SpringApplicationRunListener 集合,SpringApplicationRunListener 是 Spring Boot 的运行时监听器
 SpringApplicationRunListeners listeners = getRunListeners(args);

SpringApplicationRunListeners我们单从字面意思来看,Spring应用运行监听器列表,啥是运行监听器列表?说白了,就是在SpringApplication启动各个阶段,会调用一次listeners.xxx()方法,然后广播各个阶段对应的事件(ApplicationEvent),最终会调用各个阶段对应的监听器(ApplicationListener)列表,执行监听器的onApplicationEvent ()方法。SpringApplicationRunListeners这个名字我总感觉有点变扭,应该叫事件发布者更为贴切。

我们先来看看 SpringApplicationRunListeners 对象,从代码可以看出该对象是由 getRunListeners 方法创建的:

    private SpringApplicationRunListeners getRunListeners(String[] args) {
        Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
        return new SpringApplicationRunListeners(logger,
                getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args));
    }

可以看到,通过传入的 getSpringFactoriesInstances 方法的返回值,执行 SpringApplicationRunListeners 的构造方法,进行对象的创建。

同样是调用 getSpringFactoriesInstances 方法,不过这里获取的是 key 为 SpringApplicationRunListener 的对象集合.

 最后,就是将该集合传入 SpringApplicationRunListeners 的构造方法:

复制代码
/**
 * A collection of {@link SpringApplicationRunListener}.
 *
 * @author Phillip Webb
 */
class SpringApplicationRunListeners {

    private final Log log;

    private final List<SpringApplicationRunListener> listeners;

    SpringApplicationRunListeners(Log log, Collection<? extends SpringApplicationRunListener> listeners) {
        this.log = log;
        this.listeners = new ArrayList<>(listeners);
    }

    void starting() {
        for (SpringApplicationRunListener listener : this.listeners) {
            listener.starting();
        }
    }
      ...
}
复制代码

里面是将集合赋值到 listeners 属性,可以看到 SpringApplicationRunListeners 属于组合模式的实现,核心其实是内部关联的 SpringApplicationRunListener 对象集合,当外部调用该阶段方法时,就会迭代执行集合中 SpringApplicationRunListener 对应的方法。所以接下来我们就来讨论 SpringApplicationRunListener。

2.3、监听器触发机制

SpringApplicationRunListener 负责在 SpringBoot 的不同阶段广播相应的事件,然后调用实际的 ApplicationListener 类,在该类的 onApplicationEvent 方法中,根据不同的 Spring Boot 事件执行相应操作。整个过程大概如此:

接下来进行详细讨论,先来看看 SpringApplicationRunListener 定义:

复制代码
public interface SpringApplicationRunListener {

    // 在run()方法开始执行时被调用,表示应用刚刚启动,对应的 Spring Boot 事件为 ApplicationStartingEvent
    void starting();

    // ConfigurableEnvironment 构建完成时调用,对应的 Spring Boot 事件为 ApplicationEnvironmentPreparedEvent
    void environmentPrepared(ConfigurableEnvironment environment);

    // ApplicationContext 构建完成时调用,对应的 Spring Boot 事件为 ApplicationContextInitializedEvent
    void contextPrepared(ConfigurableApplicationContext context);

    // ApplicationContext 完成加载但还未启动时调用,对应的 Spring Boot 事件为 ApplicationPreparedEvent
    void contextLoaded(ConfigurableApplicationContext context);

    // ApplicationContext 已启动,但 callRunners 还未执行时调用,对应的 Spring Boot 事件为 ApplicationStartedEvent
    void started(ConfigurableApplicationContext context);

    // ApplicationContext 启动完毕被调用,对应的 Spring Boot 事件为 ApplicationReadyEvent
    void running(ConfigurableApplicationContext context);

    // 应用出错时被调用,对应的 Spring Boot 事件为 ApplicationFailedEvent
    void failed(ConfigurableApplicationContext context, Throwable exception);

}
复制代码

我们来看看它的实现类,也就是上面加载的 spring.factories 文件中的 EventPublishingRunListener 类,该类也是 Spring Boot 内建的唯一实现类,具体广播事件的操作在该类中进行,代码如下: 

复制代码
public class EventPublishingRunListener implements SpringApplicationRunListener, Ordered {

    private final SpringApplication application;

    private final String[] args;

    private final SimpleApplicationEventMulticaster initialMulticaster;

    public EventPublishingRunListener(SpringApplication application, String[] args) {
        this.application = application;
        this.args = args;
        this.initialMulticaster = new SimpleApplicationEventMulticaster();
        for (ApplicationListener<?> listener : application.getListeners()) {
//添加ApplicationListener监听器
this.initialMulticaster.addApplicationListener(listener); } } @Override public void starting() {
//调用广播器来触发事件
this.initialMulticaster.multicastEvent(new ApplicationStartingEvent(this.application, this.args)); } ... }
复制代码

可以看到,通过构造方法创建 EventPublishingRunListener 实例的过程中,调用了 getListeners 方法,将 SpringApplication 中所有 ApplicationListener 监听器关联到了 initialMulticaster 属性中。没错,这里的 ApplicationListener 监听器就是在 SpringApplication 准备阶段从 spring.factories 文件加载的 key 为 ApplicationListener 的实现类集合,该实现类集合全部重写了 onApplicationEvent 方法。

2.4、SimpleApplicationEventMulticaster 广播器触发ApplicationStaringEvent事件

这里又引出了另一个类, 也就是 SimpleApplicationEventMulticaster ,该类是 Spring 的事件广播器,也就是通过它来广播各种事件。接着,当外部迭代的执行到 EventPublishingRunListener 的 starting 方法时,会通过 SimpleApplicationEventMulticaster 的 multicastEvent 方法进行事件的广播,这里广播的是 ApplicationStartingEvent 事件,我们进入 multicastEvent 方法:

复制代码
public class SimpleApplicationEventMulticaster extends AbstractApplicationEventMulticaster {

    ...
    
    @Override
    public void multicastEvent(ApplicationEvent event) {
        multicastEvent(event, resolveDefaultEventType(event));
    }

    @Override
    public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
        ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
//获取线程池 Executor executor
= getTaskExecutor();
//获取对当前事件感兴趣的监听器列表
for (ApplicationListener<?> listener : getApplicationListeners(event, type)) { if (executor != null) { executor.execute(() -> invokeListener(listener, event)); } else { invokeListener(listener, event); } } } }
复制代码

通过 getApplicationListeners 方法,根据事件类型返回从上面关联的 ApplicationListener 集合中筛选出匹配的 ApplicationListener 集合:

然后依次遍历这些监听器,同步或异步的调用 invokeListener 方法:

复制代码
/**
     * Invoke the given listener with the given event.
     * @param listener the ApplicationListener to invoke
     * @param event the current event to propagate
     * @since 4.1
     */
    protected void invokeListener(ApplicationListener<?> listener, ApplicationEvent event) {
        ErrorHandler errorHandler = getErrorHandler();
        if (errorHandler != null) {
            try {
                doInvokeListener(listener, event);
            }
            catch (Throwable err) {
                errorHandler.handleError(err);
            }
        }
        else {
            doInvokeListener(listener, event);
        }
    }

    @SuppressWarnings({"rawtypes", "unchecked"})
    private void doInvokeListener(ApplicationListener listener, ApplicationEvent event) {
        try {
            listener.onApplicationEvent(event);
        }
        catch (ClassCastException ex) {
            String msg = ex.getMessage();
            if (msg == null || matchesClassCastMessage(msg, event.getClass())) {
                // Possibly a lambda-defined listener which we could not resolve the generic event type for
                // -> let's suppress the exception and just log a debug message.
                Log logger = LogFactory.getLog(getClass());
                if (logger.isTraceEnabled()) {
                    logger.trace("Non-matching event type for listener: " + listener, ex);
                }
            }
            else {
                throw ex;
            }
        }
    }
复制代码

可以看到,最终调用的是 doInvokeListener 方法,在该方法中执行了 ApplicationListener 的 onApplicationEvent 方法,入参为广播的事件对象。我们就拿其中一个的监听器来看看 onApplicationEvent 中的实现,如 BackgroundPreinitializer 类:

复制代码
public class BackgroundPreinitializer implements ApplicationListener<SpringApplicationEvent> {

    ...
    
    @Override
    public void onApplicationEvent(SpringApplicationEvent event) {
        if (!Boolean.getBoolean(IGNORE_BACKGROUNDPREINITIALIZER_PROPERTY_NAME)
                && event instanceof ApplicationStartingEvent && preinitializationStarted.compareAndSet(false, true)) {
            performPreinitialization();
        }
        if ((event instanceof ApplicationReadyEvent || event instanceof ApplicationFailedEvent)
                && preinitializationStarted.get()) {
            try {
                preinitializationComplete.await();
            }
            catch (InterruptedException ex) {
                Thread.currentThread().interrupt();
            }
        }
    }
    
    ...
}
复制代码

在该方法中,通过 instanceof 判断事件的类型,从而进行相应的操作。该监听器主要的操作是新建一个后台线程去执行那些耗时的初始化工作,包括验证器、消息转换器等。LoggingApplicationListener 监听器则是对 Spring Boot 的日志系统做一些初始化的前置操作。另外两个监听器在该阶段无任何操作。 至此,SpringBoot 事件机制的整体流程大概如此,我们简要回顾一下几个核心组件: 

  • SpringApplicationRunListeners:首先,在SpringApplication的run 方法的执行过程中,通过该类在 Spring Boot 不同的阶段调用不同的阶段方法,如在刚启动阶段调用的 listeners.starting 方法。
  • SpringApplicationRunListener:而 SpringApplicationRunListeners 属于组合模式的实现,它里面关联了 SpringApplicationRunListener 实现类集合,当外部调用阶段方法如listeners.starting 时,会迭代执行该集合中的阶段方法。实现类集合是 spring.factories 文件中定义好的类。
  • EventPublishingRunListener:该类是 Spring Boot 内置的 SpringApplicationRunListener 唯一实现类,所以,当外部调用各阶段的方法时,真正执行的是该类中的方法。
  • SimpleApplicationEventMulticaster:在阶段方法中,会通过 Spring 的 SimpleApplicationEventMulticaster 事件广播器,广播各个阶段对应的事件,如这里的 starting 方法广播的事件是 ApplicationStartingEvent。
  • ApplicationListener:最后 ApplicationListener 的实现类也就是 Spring Boot 监听器会监听到广播的事件,根据不同的事件,进行相应的操作。

以发布ApplicationStartingEvent事件为例,时序图如下:

到这里 Spring Boot 事件监听机制差不多就结束了,值得注意的是 Spring Boot 监听器实现的是 Spring 的 ApplicationListener 类,事件类最终继承的也是 Spring 的 ApplicationEvent 类,所以,Spring Boot 的事件和监听机制都基于 Spring 而实现的。 

三、SpringApplication运行阶段——加载启动参数

当执行完 listeners.starting 方法后,接着进入构造 ApplicationArguments 阶段:

ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);

该类是用于简化 Spring Boot 应用启动参数的封装接口,我们启动项目时输入的命令参数会封装在该类中。一种是通过 IDEA 输入的参数,如下:

另一种是 springboot jar包运行时传递的参数: cmd中运行java -jar xxx.jar name=张三 pwa=123 。

然后,可以通过 @Autowired 注入 ApplicationArguments 的方式进行使用:

复制代码
public class Test {

    @Autowired
    private ApplicationArguments applicationArguments;

    public void getArgs() {
        // 获取 args 中的所有 non option 参数
        applicationArguments.getNonOptionArgs();

        // 获取 args 中所有的 option 参数的 name
        applicationArguments.getOptionNames();

        // 获取传递给应用程序的原始未处理参数
        applicationArguments.getSourceArgs();

        // 获取 args 中指定 name 的 option 参数的值
        applicationArguments.getOptionValues("name");

        // 判断从参数中解析的 option 参数是否包含指定名称的选项
        applicationArguments.containsOption("name");
    }
}
复制代码

四、SpringApplication运行阶段——加载外部化配置的资源文件

在介绍加载外部化配置资源文件之前,我们先来介绍一下PropertySource以及外部化配置的资源类型有哪些。

4.1、外部化配置的资源类型

除了在项目中我们常常用到的properties、YAML文件外,还有环境变量、系统属性、启动参数等。所有的资源类型将近二十种,这里只介绍比较熟悉的:

  • properties:文件是以.properties为后缀,文件中的数据以key-value格式保存;
  • yaml:文件是以.yml为后缀,文件中的数据以key-value格式保存;
  • 环境变量:这是通过 System.getenv() 方式获取的默认配置,也是key value格式,下面列出部分配置,其它的还请自行了解,如下:

名称

Key
Java安装目录 JAVA_HOME
classpath环境变量 CLASSPATH
用户临时文件目录 TEMP
计算机名 COMPUTERNAME
用户名 USERNAME
  • 系统属性:这是通过 System.getProperties() 方式获取的默认配置,也是key value格式,下面列出部分配置,其它的还请自行了解,如下:
名称 key
运行时环境版本 java.version Java
Java安装目录 java.home
要使用的 JIT编译器的名称 java.compiler
操作系统的架构 os.arch
操作系统的版本 os.version
  • 启动参数:就是我们上面介绍到的,一种是在 jar 包运行时行时传递的参数,如:java -jar xxx.jar name=张三 pwa=123 ,还有一种是在 IDEA 的 Program arguments 中输入数据。

可以看到,外部化配置中的数据都是key value 格式。这里还要注意它们的加载顺序,当key相同时,会出现覆盖的情况。

接下来,我们的重心来围绕 properties 和 YAML 配置文件,这两者也是我们日常工作中常用的。首先来看取值方式,在 Spring 时代有 Environment 、 @Value 、 XML 三种方式,在 Spring Boot 时代则是 @ConfigurationProperties 方式。其中,涉及到了一个核心类,它就是 Environment ,该对象不仅可以获取所有的外部化配置数据,就连另外几种取值方式的数据来源也是从该类中获取。这里,主要对 Environment 和 @ConfigurationProperties 进行详细讨论。 

4.2 、PropertySource

PropertySource ,我们将其称之为配置源,官方定义它是外部化配置的API描述方式,是外部化配置的一个媒介。 用我们的话来说,它是一个抽象类,提供了统一存储外部化配置数据的功能,而每种外部化配置有具体的实现类 。我们来看看 PropertySource 对象的数据格式,一般包含: 

  • name : 外部化配置的名称;
  • source : 存储配置中的数据,底层一般数据格式都是key value;

 这个类结构简化如下,主要提供不同的基础操作,如 get、contains 等:

public abstract class PropertySource<T> {protected final   
    String name;//属性源名称
    protected final T source;//属性源(比如来自Map,那就是一个Map对象)
    public String getName();  //获取属性源的名字  
    public T getSource();        //获取属性源  
    public boolean containsProperty(String name);  //是否包含某个属性  
    public abstract Object getProperty(String name);   //得到属性名对应的属性值   
} 

其类图如下:

  • MapPropertySource:其source来自于一个Map,用法如下:
复制代码
public static void main(String[] args) {

        Map<String,Object> map=new HashMap<>();
        map.put("name","wang");
        map.put("age",23);

        MapPropertySource source_1=new MapPropertySource("person",map);
        System.out.println(source_1.getProperty("name"));//wang
        System.out.println(source_1.getProperty("age"));//23
        System.out.println(source_1.getName());//person
        System.out.println(source_1.containsProperty("class"));//false

 }
复制代码
  • PropertiesPropertySource:source是一个Properties对象,继承自MapPropertySource。与MapPropertySource用法相同;

  • ResourcePropertySource:继承自PropertiesPropertySource,它的source来自于一个Resource资源;

  • ServletConfigPropertySource:source为ServletConfig对象;

  • ServletContextPropertySource:source为ServletContext对象;

  • StubPropertySource:临时作为一个PropertySource的占位,后期会被真实的PropertySource取代;

  • SystemEnvironmentPropertySource:继承自MapPropertySource,它的source也是一个map,但来源于系统环境。 【重点】与MapPropertySource不同的是,取值时它将会忽略大小写,”.”和”_”将会转化。遇到转化情况会打印出日志;

  • CompositePropertySource:内部可以保存多个PropertySource;

private final Set<PropertySource<?>> propertySources = new LinkedHashSet<PropertySource<?>>();

4.3、PropertySources

包含多个PropertySource,继承了Iterable接口,所以它的子类还具有迭代的能力。 接口定义:

public interface PropertySources extends Iterable<PropertySource<?>> {

    boolean contains(String name);//是否包含某个PropertySource

    PropertySource<?> get(String name);//获取某个PropertySource
}

PropertySource的实现类MutablePropertySources,它包含了一个CopyOnWriteArrayList集合,用来包含多个PropertySource:

private final List<PropertySource<?>> propertySourceList = new CopyOnWriteArrayList<PropertySource<?>>();

这个类还具有几个比较重要的方法,用来向集合中加减PropertySource:

addFirst
addLast
addBefore
addAfter
remove

4.4、PropertyResolver

实现这个类的接口具有解析PropertySource、根据PropertySource转换文本中的占位符的能力.

复制代码
public interface PropertyResolver {  

    //是否包含某个属性  
    boolean containsProperty(String key);  

    //获取属性值 如果找不到返回null   
    String getProperty(String key);  

    //获取属性值,如果找不到返回默认值        
    String getProperty(String key, String defaultValue);  

    //获取指定类型的属性值,找不到返回null  
    <T> T getProperty(String key, Class<T> targetType);  

    //获取指定类型的属性值,找不到返回默认值  
    <T> T getProperty(String key, Class<T> targetType, T defaultValue);  

    //获取属性值为某个Class类型,找不到返回null,如果类型不兼容将抛出ConversionException  
    <T> Class<T> getPropertyAsClass(String key, Class<T> targetType);  

    //获取属性值,找不到抛出异常IllegalStateException  
    String getRequiredProperty(String key) throws IllegalStateException;  

    //获取指定类型的属性值,找不到抛出异常IllegalStateException         
    <T> T getRequiredProperty(String key, Class<T> targetType) throws IllegalStateException;  

    //替换文本中的占位符(${key})到属性值,找不到不解析  
    String resolvePlaceholders(String text);  

    //替换文本中的占位符(${key})到属性值,找不到抛出异常IllegalArgumentException  
    String resolveRequiredPlaceholders(String text) throws IllegalArgumentException;  

}  
复制代码

它的实现类主要有两种:

  • 各种Resolver:主要是PropertySourcesPropertyResolver
  • 各种Evironment

PropertySourcesPropertyResolver:

复制代码
MutablePropertySources sources = new MutablePropertySources();
sources.addLast(new MapPropertySource("map", new HashMap<String, Object>() {
            {
                put("name", "wang");
                put("age", 12);
            }
}));//向MutablePropertySources添加一个MapPropertySource

PropertyResolver resolver = new PropertySourcesPropertyResolver(sources);
System.out.println(resolver.containsProperty("name"));//输出 true
System.out.println(resolver.getProperty("age"));//输出 12
System.out.println(resolver.resolvePlaceholders("My name is ${name} .I am ${age}."));
//输出 My name is wang .I am 12.
复制代码

4.5、Environment 

Spring抽象了一个Environment来表示Spring应用程序环境配置,它整合了各种各样的外部化配置的资源类型,并且提供统一访问的方法。

复制代码
public interface Environment extends PropertyResolver {  
        //得到当前明确激活的剖面  
    String[] getActiveProfiles();  

        //得到默认激活的剖面,而不是明确设置激活的  
    String[] getDefaultProfiles();  

        //是否接受某些剖面  
    boolean acceptsProfiles(String... profiles);  

}  
复制代码

亲爱的读者和支持者们,自动博客加入了打赏功能,陆陆续续收到了各位老铁的打赏。在此,我想由衷地感谢每一位对我们博客的支持和打赏。你们的慷慨与支持,是我们前行的动力与源泉。

日期姓名金额
2023-09-06*源19
2023-09-11*朝科88
2023-09-21*号5
2023-09-16*真60
2023-10-26*通9.9
2023-11-04*慎0.66
2023-11-24*恩0.01
2023-12-30I*B1
2024-01-28*兴20
2024-02-01QYing20
2024-02-11*督6
2024-02-18一*x1
2024-02-20c*l18.88
2024-01-01*I5
2024-04-08*程150
2024-04-18*超20
2024-04-26.*V30
2024-05-08D*W5
2024-05-29*辉20
2024-05-30*雄10
2024-06-08*:10
2024-06-23小狮子666
2024-06-28*s6.66
2024-06-29*炼1
2024-06-30*!1
2024-07-08*方20
2024-07-18A*16.66
2024-07-31*北12
2024-08-13*基1
2024-08-23n*s2
2024-09-02*源50
2024-09-04*J2
2024-09-06*强8.8
2024-09-09*波1
2024-09-10*口1
2024-09-10*波1
2024-09-12*波10
2024-09-18*明1.68
2024-09-26B*h10
2024-09-3010
2024-10-02M*i1
2024-10-14*朋10
2024-10-22*海10
2024-10-23*南10
2024-10-26*节6.66
2024-10-27*o5
2024-10-28W*F6.66
2024-10-29R*n6.66
2024-11-02*球6
2024-11-021*鑫6.66
2024-11-25*沙5
2024-11-29C*n2.88
posted @   大奥特曼打小怪兽  阅读(2049)  评论(0编辑  收藏  举报
编辑推荐:
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
如果有任何技术小问题,欢迎大家交流沟通,共同进步

公告 & 打赏

>>

欢迎打赏支持我 ^_^

最新公告

程序项目代做,有需求私信(小程序、网站、爬虫、电路板设计、驱动、应用程序开发、毕设疑难问题处理等)。

了解更多

点击右上角即可分享
微信分享提示