• SpringBoot启动流程源码分析


    前言#

    SpringBoot项目的启动流程是很多面试官面试中高级Java程序员喜欢问的问题。这个问题的答案涉及到了SpringBoot工程中的源码,也许我们之前看过别的大牛写过的有关SpringBoot项目启动流程的文章,但是自己没有去研究一遍总是会记忆不深刻。有句话叫做“纸上来得终觉浅,绝知此事要躬行”,我觉得说得非常在理。底层的东西,也只有自己深入研究过一遍甚至好几遍源码才能彻底搞懂并记忆牢固。下面笔者来带领大家详细分析SpringBoot启动过程中到底做了哪些事情,把本文仔细看完了,面对面试官问的有关SpringBoot启动过程做了哪些工作的面试题就迎刃而解了!

    @SpringBootApplication注解部分#

    该注解包含以下三个注解:

    @SpringBootConfiguration
    @EnableAutoConfiguration
    @ComponentScan

    SpringBoot 启动Tomcat源码分析#

    1. @SpringBootApplication
    2. @EnableAutoConfiguration
    3. @Import(AutoConfigurationImportSelector.class)
    4. List configurations = getCandidateConfigurations(annotationMetadata,

    attributes);

    5.获取spring-boot-configurations下面META-INF/ spring.factorie所有的类注册到IOC容器中。

    org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration,\

    org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration,\

    省略....

     6.其中ServletWebServerFactoryAutoConfiguration种通过import方式注入了springboot中支持的三种容器启动方式对应的bean

    Import({ ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class,

    ServletWebServerFactoryConfiguration.EmbeddedTomcat.class,

    ServletWebServerFactoryConfiguration.EmbeddedJetty.class,

    ServletWebServerFactoryConfiguration.EmbeddedUndertow.class })

     7.我们查看分析EmbeddedTomcat:

    @Configuration

    @ConditionalOnClass({ Servlet.class, Tomcat.class, UpgradeProtocol.class })

    @ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)

    public static class EmbeddedTomcat {

    @Bean

    public TomcatServletWebServerFactory tomcatServletWebServerFactory() {

    return new TomcatServletWebServerFactory();

    }

     

    }

     8.TomcatServletWebServerFactory中的该方法就是设置我们的tomcat信息

    public WebServer getWebServer(ServletContextInitializer... initializers) {

    Tomcat tomcat = new Tomcat();

    File baseDir = (this.baseDirectory != null ? this.baseDirectory

    : createTempDir("tomcat"));

    tomcat.setBaseDir(baseDir.getAbsolutePath());

    Connector connector = new Connector(this.protocol);

    tomcat.getService().addConnector(connector);

    customizeConnector(connector);

    tomcat.setConnector(connector);

    tomcat.getHost().setAutoDeploy(false);

    configureEngine(tomcat.getEngine());

    for (Connector additionalConnector : this.additionalTomcatConnectors) {

    tomcat.getService().addConnector(additionalConnector);

    }

    prepareContext(tomcat.getHost(), initializers);

    return getTomcatWebServer(tomcat);

    }

     9.该方法被触发的链路分析:

    SpringApplication.run-->SpringApplication.refreshContext-->SpringApplication.refresh-->
    ServletWebServerApplicationContext.refresh-->AbstractApplicationContext.refresh-->
    ServletWebServerApplicationContext.refresh-->ServletWebServerApplicationContext.createWebServer-->TomcatServletWebServerFactory.getWebServer
    其中
    ServletWebServerApplicationContext.createWebServer方法源码:
    private void createWebServer() {
    WebServer webServer = this.webServer;
    ServletContext servletContext = this.getServletContext();
    if (webServer == null && servletContext == null) {
    ServletWebServerFactory factory = this.getWebServerFactory();
    this.webServer = factory.getWebServer(new ServletContextInitializer[]{this.getSelfInitializer()});
    } else if (servletContext != null) {
    try {
    this.getSelfInitializer().onStartup(servletContext);
    } catch (ServletException var4) {
    throw new ApplicationContextException("Cannot initialize servlet context", var4);
    }
    }

    this.initPropertySources();
    }

    SpringBoot dispatcherServlet注入分析

    同样的前5步和启动tomcat的过程一样,只不过在第5步中也SpringMVC相关的自动配置类也会被注入到容器中,具体如下:

    5.获取spring-boot-configurations下面META-INF/ spring.factorie所有的类注册到IOC容器中。

    org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration,\

    6.@ConditionalOnClass(DispatcherServlet.class)

    7.注册我们的dispatcherServlet类

    @Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)

    public DispatcherServlet DispatcherServlet dispatcherServlet = new DispatcherServlet();

    dispatcherServlet.setDispatchOptionsRequest(

    this.webMvcProperties.isDispatchOptionsRequest());

    dispatcherServlet.setDispatchTraceRequest(

    this.webMvcProperties.isDispatchTraceRequest());

    dispatcherServlet.setThrowExceptionIfNoHandlerFound(

    this.webMvcProperties.isThrowExceptionIfNoHandlerFound());

    return dispatcherServlet;

    }

    WebMvcProperties配置SpringMVC相关信息:

    @ConfigurationProperties(prefix = "spring.mvc")

    public class WebMvcProperties {}

    启动类入口方法#

    首先我们通过SpringApplication类的静态Run方法进入SpringBoot项目的启动入口

    /**
    * @param primarySource springboot启动类
    * @param args 启动参数
    */
    public static ConfigurableApplicationContext run(Class primarySource, String... args) {   
        return run(new Class[]{primarySource}, args);
    }
    

    从上面的源码中我们可以看到SpringBoot启动类返回的应用上下文类是ConfigurableApplicationContext

    然后我们进入另一个静态run方法

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

    在上面这个静态run方法里面最终会通过SpringApplication类的构造函数实例化一个SpringApplication类实例对象,后面在调用SpringApplication实例对象的run方法

    SpringApplication类实例化和初始化#

    接下来我们看看SpringApplication类在实例化时做了什么事情

    public SpringApplication(Class... primarySources) {
            this((ResourceLoader)null, primarySources);
        }
    

    可以看到在SpringApplication类上面这个构造方法里面又调用了另一个构造方法

    public SpringApplication(ResourceLoader resourceLoader, Class... primarySources) {
            // 实例化sources属性    
            this.sources = new LinkedHashSet(); 
            // 打印模式为控制台打印
            this.bannerMode = Mode.CONSOLE;
            // 设置记录启动日志信息标识为true
            this.logStartupInfo = true; 
            // 设置添加命令行属性标识为true
            this.addCommandLineProperties = true;
            //设置addConversionService属性为true
            this.addConversionService = true;
            // 设置headless属性为true
            this.headless = true; 
            // 设置注册应用关停钩子属性为true
            this.registerShutdownHook = true; 
            // 实例化additionalProfiles属性
            this.additionalProfiles = new HashSet();
            //默认非自定义环境 
            this.isCustomEnvironment = false;
            // 上一步传过来的resourceLoader为null
            this.resourceLoader = resourceLoader; 
            // 断言primarySources参数不能为空,也就是springboot应用类不能为空
            Assert.notNull(primarySources, "PrimarySources must not be null");
            // 将传递过来的springboot启动类参数转成List后加入LinkedHashSet集合后赋值给primarySources属性
            this.primarySources = new LinkedHashSet(Arrays.asList(primarySources));
            // 根据类路径推断web应用类型
            this.webApplicationType = WebApplicationType.deduceFromClasspath(); 
     this.setInitializers(this.getSpringFactoriesInstances(ApplicationContextInitializer.class));  // 设置初始化器属性   
            // 设置监听器属性
            this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));
            // 推断主启动类
            this.mainApplicationClass = this.deduceMainApplicationClass();
        }
    

    从上面的源码中我们分析的结果来看,实例化SpringApplication类的过程做了以下几件事情:

    • 初始化SpringApplication启动类中的大部分属性变量
    • 推断web应用类型
    • 通过加载类路径目录META-INF下的spring.factories文件读取出初始化器和监听器集合并设置到SpringApplication实例对应的初始化器和监听器属性列表中
    • 推断主启动类并赋值给SpringApplication启动类的mainApplicationClass属性

    推断Web应用类型#

    进入WebApplicationType#deduceFromClasspath方法

    private static final String[] SERVLET_INDICATOR_CLASSES = new String[]{"javax.servlet.Servlet", "org.springframework.web.context.ConfigurableWebApplicationContext"};
    static WebApplicationType deduceFromClasspath() {
            if (ClassUtils.isPresent("org.springframework.web.reactive.DispatcherHandler", (ClassLoader)null) && !ClassUtils.isPresent("org.springframework.web.servlet.DispatcherServlet", (ClassLoader)null) && !ClassUtils.isPresent("org.glassfish.jersey.servlet.ServletContainer", (ClassLoader)null)) {
                return REACTIVE;
            } else {
                String[] var0 = SERVLET_INDICATOR_CLASSES;
                int var1 = var0.length;
                for(int var2 = 0; var2 < var1; ++var2) {
                    String className = var0[var2];
                    if (!ClassUtils.isPresent(className, (ClassLoader)null)) {
                        return NONE;
                    }
                }
                return SERVLET;
            }
        }
    
    • 若满足类路径(包括依赖jar包)中存在org.springframework.web.reactive.DispatcherHandler类,同时不存在org.springframework.web.servlet.DispatcherServlet类和org.glassfish.jersey.servlet.ServletContainer类则返回REACTIVE类型Web应用
    • 遍历判断类路径中是否同时存在javax.servlet.Servlet类和org.springframework.web.context.ConfigurableWebApplicationContext类,满足则返回SERVLET类型Web应用,否则返回非Web应用

    加载spring.factories文件中配置的初始化器和监听器#

    通过调用SpringApplication#getSpringFactoriesInstances方法得到的返回值设置初始化器和监听器属性,传入的参数分别为ApplicationContextInitializer类和ApplicationListener

    /**
    *根据传入的type参数获取类路径META-INF/spring.factories文件中的自动配置初始化类集合
    @param type 类型
    @return Collection 泛型集合
    */
    private  Collection getSpringFactoriesInstances(Class type) {
            return this.getSpringFactoriesInstances(type, new Class[0]);
        }
    /**
    *上一个方法的多参数重载方法
    * @param type 类型
    * @param parameterTypes 参数类型数组
    * @param args 参数数组
    */
    private  Collection getSpringFactoriesInstances(Class type, Class[] parameterTypes, Object... args) {
            ClassLoader classLoader = this.getClassLoader();
            // 通过SpringFactoriesLoader#loadFactoryNames方法获取
            Set<String> names = new LinkedHashSet(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
            List instances = this.createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
            AnnotationAwareOrderComparator.sort(instances);
            return instances;
        }
    public static List<String> loadFactoryNames(Class factoryClass, @Nullable ClassLoader classLoader) {
            String factoryClassName = factoryClass.getName();
            return (List)loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
        }
    private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
            // 如果缓存里有则直接从双层缓存中获取类加载器对应的结果
            MultiValueMap<String, String> result = (MultiValueMap)cache.get(classLoader);
            if (result != null) {
                return result;
            } else {
                // 缓存中没有则去META-INF/spring.factories文件中加载并读取解析
                try {
                    Enumeration<URL> urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories");
                    LinkedMultiValueMap result = new LinkedMultiValueMap();
                    while(urls.hasMoreElements()) {
                        URL url = (URL)urls.nextElement();
                        UrlResource resource = new UrlResource(url);
                        Properties properties = PropertiesLoaderUtils.loadProperties(resource);
                        Iterator var6 = properties.entrySet().iterator();
                        while(var6.hasNext()) {
                            Entry entry = (Entry)var6.next();
                            String factoryClassName = ((String)entry.getKey()).trim();
                            String[] var9 = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
                            int var10 = var9.length;
                            for(int var11 = 0; var11 < var10; ++var11) {
                                String factoryName = var9[var11];
                                result.add(factoryClassName, factoryName.trim());
                            }
                        }
                    }
                    cache.put(classLoader, result);
                    return result;
                } catch (IOException var13) {
                    throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var13);
                }
            }
        }
    /*** 创建spring.factories文件中的初始化类实例集合*/
    private  List createSpringFactoriesInstances(Class type, Class[] parameterTypes, ClassLoader classLoader, Object[] args, Set<String> names) {
            List instances = new ArrayList(names.size());
            Iterator var7 = names.iterator();
            while(var7.hasNext()) {
                String name = (String)var7.next();
                try {
                    // 调用反射工具类加载spring.factories文件中的初始化类
                    Class instanceClass = ClassUtils.forName(name, classLoader);
                    Assert.isAssignable(type, instanceClass);
                    Constructor constructor = instanceClass.getDeclaredConstructor(parameterTypes);
                    // 通过构造函数实例化类
                    T instance = BeanUtils.instantiateClass(constructor, args);
                    instances.add(instance);
                } catch (Throwable var12) {
                    throw new IllegalArgumentException("Cannot instantiate " + type + " : " + name, var12);
                }
            }
            return instances;
        }
    

    推断主应用类#

     private Class deduceMainApplicationClass() {
            try {
                StackTraceElement[] stackTrace = (new RuntimeException()).getStackTrace();
                StackTraceElement[] var2 = stackTrace;
                int var3 = stackTrace.length;
                for(int var4 = 0; var4 < var3; ++var4) {
                    StackTraceElement stackTraceElement = var2[var4];
                    if ("main".equals(stackTraceElement.getMethodName())) {
                        return Class.forName(stackTraceElement.getClassName());
                    }
                }
            } catch (ClassNotFoundException var6) {
            }
            return null;
        }
    

    通过上面的源码我们可以看到推断主应用类是通过实例化一个运行时异常,并拿到该运行时异常的堆栈数组,然后循环遍历堆栈数组,判断堆栈元素的方法名是否为main方法,若是则返回通过反射加载全类名后的主启动类;若是运行时异常堆栈元素中不存在main方法,则返回空。SpringApplication类实例化后就会调用run方法,下面我们再回到SpringApplication类非静态的run方法源码

    SpringApplication类实例run方法#

    public ConfigurableApplicationContext run(String... args) {
            // new了一个 StopWatch并启动了它
            StopWatch stopWatch = new StopWatch();
            stopWatch.start();
            // springboot启动时使用ConfigurableApplicationContext作为BeanFactory接口的实现类
            ConfigurableApplicationContext context = null;
            // 实例化一个异常报告集合
            Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList();
            // 设置系统属性java.awt.headless,默认为false
            this.configureHeadlessProperty();
            // 获取所有的启动监听器
            SpringApplicationRunListeners listeners = this.getRunListeners(args);
            // 遍历启动类监听器列表,并逐个启动
            listeners.starting();
            Collection exceptionReporters;
            try {
                // 实例化启动类命令参数
                ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
                // 准备启动环境
                ConfigurableEnvironment environment = this.prepareEnvironment(listeners, applicationArguments);
                // 配置环境中可忽略的bean信息
                this.configureIgnoreBeanInfo(environment);
                // 打印springboot项目的logo图标
                Banner printedBanner = this.printBanner(environment);
                // 创建应用上下文,也就是Spring IOC容器
                context = this.createApplicationContext();
                // 收集异常报告集合
                exceptionReporters = this.getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[]{ConfigurableApplicationContext.class}, context);
                // 准备应用上下文环境,会去加载配置类基于注解的bean、xml配置文件中定义的bean
                this.prepareContext(context, environment, listeners, applicationArguments, printedBanner);
                // 刷新上下文,对于servlet应用程序这个方法会去创建和启动web服务器
                this.refreshContext(context);
                // 这个方法啥也没干
                this.afterRefresh(context, applicationArguments);
                // 启动完成,记时停止
                stopWatch.stop();
                if (this.logStartupInfo) {
                    // 如果开启了记录启动日志,则记录应用程序启动过程耗时
                    (new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), stopWatch);
                }
                // 应用运行时监听器发布应用启动事件
                listeners.started(context);
                // 调用启动类中的任务
                this.callRunners(context, applicationArguments);
            } catch (Throwable var10) {
                // 启动过程发生异常则处理异常,并跑出IllegalStateException类型异常
                this.handleRunFailure(context, var10, exceptionReporters, listeners);
                throw new IllegalStateException(var10);
            }
    

    SpringApplicationRunListeners.starting方法#

    public void starting() {
            Iterator var1 = this.listeners.iterator();
            while(var1.hasNext()) {
                SpringApplicationRunListener listener = (SpringApplicationRunListener)var1.next();
                listener.starting();
            }
        }
    

    通过上面的源码可以看到通过循环遍历启动类监听器集合中的每个启动类监听器,然后调用每个启动类监听器的starting方法

    这个starting方法实际上就是通过事件广播发布了一个应用启动事件

    private final SimpleApplicationEventMulticaster initialMulticaster;
    public void starting() {
            this.initialMulticaster.multicastEvent(new ApplicationStartingEvent(this.application, this.args));
        }
    

    准备启动环境#

    然后我们再回过去看SpringApplication#prepareEnvironment方法,这个方法是准备启动环境的意思

    private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments) {
            // 获取或者创建一个配置环境
            ConfigurableEnvironment environment = this.getOrCreateEnvironment();
            // 配置环境
            this.configureEnvironment((ConfigurableEnvironment)environment, applicationArguments.getSourceArgs());
            // 通过启动类应用监听器发布环境准备事件
            listeners.environmentPrepared((ConfigurableEnvironment)environment);
            // 将配置环境绑定到SpringApplication
            this.bindToSpringApplication((ConfigurableEnvironment)environment);
            if (!this.isCustomEnvironment) {
                // 如果是非自定义环境则根据需要转换成推断出的Web环境
                environment = (new EnvironmentConverter(this.getClassLoader())).convertEnvironmentIfNecessary((ConfigurableEnvironment)environment, this.deduceEnvironmentClass());
            }
            // 通过环境变量添加属性配置源
            ConfigurationPropertySources.attach((Environment)environment);
            // 返回经过处理的配置环境
            return (ConfigurableEnvironment)environment;
        }
    

    getOrCreateEnvironment方法#

    private ConfigurableEnvironment environment;
    private ConfigurableEnvironment getOrCreateEnvironment() {
            if (this.environment != null) {
                // this.environment不为空则直接返回
                return this.environment;
            } else {
                switch(this.webApplicationType) {
                // 根据web应用类型创建环境
                case SERVLET:
                    // servlet web应用返回标准StandardServletEnvironment实例
                    return new StandardServletEnvironment();
                case REACTIVE:
                    // reactive web应用环境返回StandardReactiveWebEnvironment实例
                    return new StandardReactiveWebEnvironment();
                default:
                    // 默认返回非web应用的StandardEnvironment实例
                    return new StandardEnvironment();
                }
            }
        }
    

    前面的WebApplicationType#deduceFromClasspath方法码分析中我们知道返回的是一个SERVLET枚举。因此,spring-boot项目中具有spring-boot-starter-web起步依赖时getOrCreateEnvironment方法返回的是一个StandardServletEnvironment实例

    SpringApplicationRunListeners#environmentPrepared#

    接下来我们进入SpringApplicationRunListeners#environmentPrepared方法

    public void environmentPrepared(ConfigurableEnvironment environment) {
            Iterator var2 = this.listeners.iterator();
    
        <span class="token keyword">whilespan><span class="token punctuation">(span>var2class="token punctuation">.</span>hasNextspan><span class="token punctuation">(span><span class="token punctuation">)span><span class="token punctuation">)span> <span class="token punctuation">{span>
            SpringApplicationRunListener listener class="token operator">=</span> (span>SpringApplicationRunListenerclass="token punctuation">)</span>var2.span><span class="token function">nextspan><span class="token punctuation">(span><span class="token punctuation">)span><span class="token punctuation">;span>
            <span class="token comment">// 通过迭代遍历启动监听器发布环境准备事件span>
            listenerclass="token punctuation">.</span>environmentPreparedspan><span class="token punctuation">(span>environmentclass="token punctuation">)</span>;span>
        <span class="token punctuation">}span>
    <span class="token punctuation">}span>
    
    
        <span class="token keyword">whilespan><span class="token punctuation">(span>var2class="token punctuation">.</span>hasNextspan><span class="token punctuation">(span><span class="token punctuation">)span><span class="token punctuation">)span> <span class="token punctuation">{span>
            SpringApplicationRunListener listener class="token operator">=</span> (span>SpringApplicationRunListenerclass="token punctuation">)</span>var2.span><span class="token function">nextspan><span class="token punctuation">(span><span class="token punctuation">)span><span class="token punctuation">;span>
            <span class="token comment">// 通过迭代遍历启动监听器发布环境准备事件span>
            listenerclass="token punctuation">.</span>environmentPreparedspan><span class="token punctuation">(span>environmentclass="token punctuation">)</span>;span>
        <span class="token punctuation">}span>
    <span class="token punctuation">}span>
    
    

    SpringApplication#bindToSpringApplication方法#

    准备好环境后进入SpringApplication#bindToSpringApplication方法

    protected void bindToSpringApplication(ConfigurableEnvironment environment) {
            try {
                Binder.get(environment).bind("spring.main", Bindable.ofInstance(this));
            } catch (Exception var3) {
                throw new IllegalStateException("Cannot bind to SpringApplication", var3);
            }
        }
    

    Binder#get方法#

    public static Binder get(Environment environment) {
            // 这里传入了一个属性源占位符解析器类实例参数
            return new Binder(ConfigurationPropertySources.get(environment), new PropertySourcesPlaceholdersResolver(environment));
        }
    

    Binder类构造方法#

    public Binder(Iterable<ConfigurationPropertySource> sources, PlaceholdersResolver placeholdersResolver) {
            this(sources, placeholdersResolver, (ConversionService)null, (Consumer)null);
        }
    public Binder(Iterable<ConfigurationPropertySource> sources, PlaceholdersResolver placeholdersResolver, ConversionService conversionService, Consumer<PropertyEditorRegistry> propertyEditorInitializer) {
            Assert.notNull(sources, "Sources must not be null");
            this.sources = sources;
            this.placeholdersResolver = placeholdersResolver != null ? placeholdersResolver : PlaceholdersResolver.NONE;
            this.conversionService = conversionService != null ? conversionService : ApplicationConversionService.getSharedInstance();
            this.propertyEditorInitializer = propertyEditorInitializer;
        }
    

    PropertySourcesPlaceholdersResolver类构造函数#

    public PropertySourcesPlaceholdersResolver(Environment environment) {
            this(getSources(environment), (PropertyPlaceholderHelper)null);
        }
    public PropertySourcesPlaceholdersResolver(Iterable<PropertySource> sources, PropertyPlaceholderHelper helper) {
            this.sources = sources;
            // 属性占位符解析器会去解析"${" 和 "}"两个符号包裹的环境变量
            this.helper = helper != null ? helper : new PropertyPlaceholderHelper("${", "}", ":", true);
        }
    

    PropertySourcesPlaceholdersResolver#getSources方法#

    private static PropertySources getSources(Environment environment) {
            // 断言environment不为空 
            Assert.notNull(environment, "Environment must not be null");
            // 断言environment是一个ConfigurableEnvironment类实例
            Assert.isInstanceOf(ConfigurableEnvironment.class, environment, "Environment must be a ConfigurableEnvironment");
            return ((ConfigurableEnvironment)environment).getPropertySources();
        }
    

    ConfigurationPropertySources#attach方法#

    public static void attach(Environment environment) {
            Assert.isInstanceOf(ConfigurableEnvironment.class, environment);
            // 从环境变量中获取多属性配置源
            MutablePropertySources sources = ((ConfigurableEnvironment)environment).getPropertySources();
            PropertySource attached = sources.get("configurationProperties");
            if (attached != null && attached.getSource() != sources) {
                // 若attached且attach#getSource得到的结果不等于sources,则删除sources中configurationProperties对应的键值对并置空attached
                sources.remove("configurationProperties");
                attached = null;
            }
            if (attached == null) {
                // 若attached为空则构建新的属性源并添加到sources的属性源列表的第一个位置
                sources.addFirst(new ConfigurationPropertySourcesPropertySource("configurationProperties", new SpringConfigurationPropertySources(sources)));
            }
    
    class="token punctuation">}
    
    
    <span class="token punctuation">}span>
    
    

    Binder#bind方法#

    public  BindResult bind(String name, Bindable target) {
            return this.bind((ConfigurationPropertyName)ConfigurationPropertyName.of(name), target, (BindHandler)null);
        }
    public  BindResult bind(ConfigurationPropertyName name, Bindable target, BindHandler handler) {
            Assert.notNull(name, "Name must not be null");
            Assert.notNull(target, "Target must not be null");
            handler = handler != null ? handler : BindHandler.DEFAULT;
            Binder.Context context = new Binder.Context();
            T bound = this.bind(name, target, handler, context, false);
            return BindResult.of(bound);
        }
    protected final  T bind(ConfigurationPropertyName name, Bindable target, BindHandler handler, Binder.Context context, boolean allowRecursiveBinding) {
            context.clearConfigurationProperty();
            try {
                target = handler.onStart(name, target, context);
                if (target == null) {
                    return null;
                } else {
                    Object bound = this.bindObject(name, target, handler, context, allowRecursiveBinding);
                    return this.handleBindResult(name, target, handler, context, bound);
                }
            } catch (Exception var7) {
                return this.handleBindError(name, target, handler, context, var7);
            }
        }
    // 绑定对象
    private  Object bindObject(ConfigurationPropertyName name, Bindable target, BindHandler handler, Binder.Context context, boolean allowRecursiveBinding) {
            ConfigurationProperty property = this.findProperty(name, context);
            if (property == null && this.containsNoDescendantOf(context.getSources(), name)) {
                return null;
            } else {
                AggregateBinder aggregateBinder = this.getAggregateBinder(target, context);
                if (aggregateBinder != null) {
                    return this.bindAggregate(name, target, handler, context, aggregateBinder);
                } else if (property != null) {
                    try {
                        return this.bindProperty(target, context, property);
                    } catch (ConverterNotFoundException var10) {
                        Object bean = this.bindBean(name, target, handler, context, allowRecursiveBinding);
                        if (bean != null) {
                            return bean;
                        } else {
                            throw var10;
                        }
                    }
                } else {
                    return this.bindBean(name, target, handler, context, allowRecursiveBinding);
                }
            }
        }
    private  Object bindProperty(Bindable target, Binder.Context context, ConfigurationProperty property) {
            context.setConfigurationProperty(property);
            Object result = property.getValue();
            // 使用占位符解析器解析属性占位符
            result = this.placeholdersResolver.resolvePlaceholders(result);
            // 从context中获取转换器转换result
            result = context.getConverter().convert(result, target);
            return result;
        }
    

    预备环境其实就做了下面几件事情:

    • 创建一个ConfigurableEnvironment类型的配置环境
    • 将配置环境实例ConfigurableEnvironment与SpringApplication启动类实例绑定到一个Binder对象中去
    • 上一步的 绑定属性的过程会去解析属性占位符,并按照配置环境配置的转换服务转转绑定结果,如果绑定成功或失败都会有对应的事件处理方法

    创建Spring应用上下文#

    现在我们回到SpringApplication#run方法的context = this.createApplicationContext();这行代码的具体实现,进入SpringApplication#createApplicationContext方法体,源码如下:

    protected ConfigurableApplicationContext createApplicationContext() {
            Class contextClass = this.applicationContextClass;
            if (contextClass == null) {
                // 若启动类的应用上下文为空,则根据实例化SpringApplication启动类过程中推断出来的web应用类型加载对应的应用上下文类ApplicationContext
                try {
                    switch(this.webApplicationType) {
                    case SERVLET:
                        contextClass = Class.forName("org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext");
                        break;
                    case REACTIVE:
                        contextClass = Class.forName("org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext");
                        break;
                    default:
                        contextClass = Class.forName("org.springframework.context.annotation.AnnotationConfigApplicationContext");
                    }
                } catch (ClassNotFoundException var3) {
                    throw new IllegalStateException("Unable create a default ApplicationContext, please specify an ApplicationContextClass", var3);
                }
            }
            return (ConfigurableApplicationContext)BeanUtils.instantiateClass(contextClass);
        }
    

    从以上的源码可以看出创建应用上下文过程主要做了下面两件事情:

    1. 根据web应用类型加载对应的ApplicationContext实现类:
    • 如果是Servlet应用程序则加载AnnotationConfigServletWebServerApplicationContext类作为应用上下文类;
    • 如果是Reactive应用程序则加载AnnotationConfigReactiveWebServerApplicationContext类作为应用上下文类;
    • 默认加载AnnotationConfigApplicationContext类作为应用上下文类
    1. 调用BeanUtils工具类实例化应用上下文类,并返回这个实例化的应用上下文对象

    准备Spring应用上下文#

    接下来我们再回到SpringApplication#run方法的this.prepareContext(context, environment, listeners, applicationArguments, printedBanner);这行代码,并进入SpringApplication#prepareContext方法体内部

    private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment, SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) {
            // 应用上下文设置配置环境属性
            context.setEnvironment(environment);
            // 应用上下文后置处理
            this.postProcessApplicationContext(context);
            // 申请初始化器
            this.applyInitializers(context);
            // 启动运行时监听器发布应用上下文预备事件
            listeners.contextPrepared(context);
            if (this.logStartupInfo) {
                // 记录日志
                this.logStartupInfo(context.getParent() == null);
                this.logStartupProfileInfo(context);
            }
            // 获取ConfigurableListableBeanFactory类型beanFactory,注意该类是个接口类
            ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
            // 注册启动参数类型单例bean
            beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
            if (printedBanner != null) {
                // 注册PrintedBanner类型单例bean
                beanFactory.registerSingleton("springBootBanner", printedBanner);
            }
            // 判断应用上下文中的beanFactory是否是一个DefaultListableBeanFactory类型的实例
            if (beanFactory instanceof DefaultListableBeanFactory) {
                ((DefaultListableBeanFactory)beanFactory).setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);// 设置是否允许覆写bean定义标识
            }
            if (this.lazyInitialization) {
                // 若是延迟初始化,则添加延迟初始化类型beanFactory后置处理器
                context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
            }
            // 获取启动来源类集合,也就是我们项目中加上@SpringBootApplication注解的启动类集合
            Set<Object> sources = this.getAllSources();
            // 断言启动来源类不为空
            Assert.notEmpty(sources, "Sources must not be empty");
            // 加载应用上下文,这个方法会通过beanDefinationReader读取通过注解和xml配置的bean
            this.load(context, sources.toArray(new Object[0]));
            // 启动监听器发布上下文完成加载事件
            listeners.contextLoaded(context);
        }
    

    SpringApplication#postProcessApplicationContext方法#

    protected void postProcessApplicationContext(ConfigurableApplicationContext context) {
            if (this.beanNameGenerator != null) {            context.getBeanFactory().registerSingleton("org.springframework.context.annotation.internalConfigurationBeanNameGenerator", this.beanNameGenerator); // 应用上下文中的beanFactory注册单例BeanNameGenerator bean
            }
            if (this.resourceLoader != null) {
                if (context instanceof GenericApplicationContext) {
                    ((GenericApplicationContext)context).setResourceLoader(this.resourceLoader);// 设置资源加载器
                }
                if (context instanceof DefaultResourceLoader) {
                    ((DefaultResourceLoader)context).setClassLoader(this.resourceLoader.getClassLoader());// 设置类加载器
                }
            }
            if (this.addConversionService) {
               context.getBeanFactory().setConversionService(ApplicationConversionService.getSharedInstance());// 设置conversionService属性
            }
        }
    

    SpringApplication#applyInitializers方法#

    protected void applyInitializers(ConfigurableApplicationContext context) {
            // 获取初始化器迭代器
            Iterator var2 = this.getInitializers().iterator();
            // 循环遍历初始化器迭代器
            while(var2.hasNext()) {
                ApplicationContextInitializer initializer = (ApplicationContextInitializer)var2.next();
                // 根据解析器calss类型和应用上下文初始化器calss类型解析参数类型
                Class requiredType = GenericTypeResolver.resolveTypeArgument(initializer.getClass(), ApplicationContextInitializer.class);
                Assert.isInstanceOf(requiredType, context, "Unable to call initializer.");
                initializer.initialize(context);
            }
        }
    

    GenericTypeResolver#resolveTypeArgument方法#

    @Nullable
    public static Class resolveTypeArgument(Class clazz, Class genericIfc) {
            ResolvableType resolvableType = ResolvableType.forClass(clazz).as(genericIfc);
            return !resolvableType.hasGenerics() ? null : getSingleGeneric(resolvableType);
    }
    // 加载解析类
    public static ResolvableType forClass(@Nullable Class clazz) {
            return new ResolvableType(clazz);
    }
    // 判断是否有解析类型数组
    public boolean hasGenerics() {
            return this.getGenerics().length > 0;
    }
    // 获取单个解析类型
    @Nullable
    private static Class getSingleGeneric(ResolvableType resolvableType) {
            Assert.isTrue(resolvableType.getGenerics().length == 1, () -> {
                return "Expected 1 type argument on generic interface [" + resolvableType + "] but found " + resolvableType.getGenerics().length;
            });
            return resolvableType.getGeneric(new int[0]).resolve();
    }
    

    加载Spring应用上下文中的bean#

    SpringApplication#load方法#

    protected void load(ApplicationContext context, Object[] sources) {
            if (logger.isDebugEnabled()) {
                // 如果开启了debug级别日志,则记录debug日志
                logger.debug("Loading source " + StringUtils.arrayToCommaDelimitedString(sources));
            }
            // 创建BeanDefinitionLoader类实例
            BeanDefinitionLoader loader = this.createBeanDefinitionLoader(this.getBeanDefinitionRegistry(context), sources);
            if (this.beanNameGenerator != null) {
                loader.setBeanNameGenerator(this.beanNameGenerator);
            }
            if (this.resourceLoader != null) {
                loader.setResourceLoader(this.resourceLoader);
            }
            if (this.environment != null) {
                loader.setEnvironment(this.environment);
            }
            loader.load();
        }
    // 通过BeanDefinitionRegistry类实例参数和应用源数组构造BeanDefinitionLoader类实例
    protected BeanDefinitionLoader createBeanDefinitionLoader(BeanDefinitionRegistry registry, Object[] sources) {
            return new BeanDefinitionLoader(registry, sources);
     }
    

    BeanDefinitionLoader类带两个参数的构造方法源码:

    BeanDefinitionLoader(BeanDefinitionRegistry registry, Object... sources) {
            // 断言registry和sources两个参数不能为空
            Assert.notNull(registry, "Registry must not be null");
            Assert.notEmpty(sources, "Sources must not be empty");
            this.sources = sources;
            // 初始化基于注解的BeanDefinitionReader
            this.annotatedReader = new AnnotatedBeanDefinitionReader(registry);
            // 初始化基于xml的BeanDefinitionReader
            this.xmlReader = new XmlBeanDefinitionReader(registry);
            if (this.isGroovyPresent()) {
                // 如果存在groovy脚本则初始化基于Groovy的BeanDefinitionReader
                this.groovyReader = new GroovyBeanDefinitionReader(registry);
            }
            // 初始化类路径bean定义扫描器
            this.scanner = new ClassPathBeanDefinitionScanner(registry);
            // 扫描器添加排除过滤器,排除扫描启动类
            this.scanner.addExcludeFilter(new BeanDefinitionLoader.ClassExcludeFilter(sources));
        }
    

    BeanDefinitionLoader#load方法#

    然后我们回到BeanDefinitionLoader#load方法,springboot项目中的bean具体是如何加载的我们在springboot项目的启动调试过程再来分析

    int load() {
            int count = 0;
            Object[] var2 = this.sources;
            int var3 = var2.length;
            for(int var4 = 0; var4 < var3; ++var4) {
                Object source = var2[var4];
                // 每加载一个bean来源,记录加载数量的count会+1
                count += this.load(source);
            }
            return count;
        }
    // 这个加载bean的方法会根据不同的bean来源进行加载,bean是如何加载的关键就在下面这几个load方法里面
    private int load(Object source) {
            Assert.notNull(source, "Source must not be null");
            if (source instanceof Class) {
                // 加载配置类中的bean
                return this.load((Class)source);
            } else if (source instanceof Resource) {
                // 加载类路径资源中的bean,包括groovy和xml文件中配置的bean
                return this.load((Resource)source);
            } else if (source instanceof Package) {
                // 加载包下面的不同配置类中的bean
                return this.load((Package)source);
            } else if (source instanceof CharSequence) {
                // 加载根据制定路径的xml文件中配置的bean
                return this.load((CharSequence)source);
            } else {
                throw new IllegalArgumentException("Invalid source type " + source.getClass());
            }
        }
    private int load(Class source) {
            if (this.isGroovyPresent() && BeanDefinitionLoader.GroovyBeanDefinitionSource.class.isAssignableFrom(source)) {
                BeanDefinitionLoader.GroovyBeanDefinitionSource loader = (BeanDefinitionLoader.GroovyBeanDefinitionSource)BeanUtils.instantiateClass(source, BeanDefinitionLoader.GroovyBeanDefinitionSource.class);
                this.load(loader);
            }
            if (this.isComponent(source)) {
                this.annotatedReader.register(new Class[]{source});
                return 1;
            } else {
                return 0;
            }
        }
    private int load(Resource source) {
            if (source.getFilename().endsWith(".groovy")) {
                if (this.groovyReader == null) {
                    throw new BeanDefinitionStoreException("Cannot load Groovy beans without Groovy on classpath");
                } else {
                    return this.groovyReader.loadBeanDefinitions(source);
                }
            } else {
                return this.xmlReader.loadBeanDefinitions(source);
            }
        }
    private int load(Package source) {
            return this.scanner.scan(new String[]{source.getName()});
        }
    private int load(CharSequence source) {
            String resolvedSource = this.xmlReader.getEnvironment().resolvePlaceholders(source.toString());
            try {
                return this.load(ClassUtils.forName(resolvedSource, (ClassLoader)null));
            } catch (ClassNotFoundException | IllegalArgumentException var10) {
                Resource[] resources = this.findResources(resolvedSource);
                int loadCount = 0;
                boolean atLeastOneResourceExists = false;
                Resource[] var6 = resources;
                int var7 = resources.length;
                for(int var8 = 0; var8 < var7; ++var8) {
                    Resource resource = var6[var8];
                    if (this.isLoadCandidate(resource)) {
                        atLeastOneResourceExists = true;
                        loadCount += this.load(resource);
                    }
                }
                if (atLeastOneResourceExists) {
                    return loadCount;
                } else {
                    Package packageResource = this.findPackage(resolvedSource);
                    if (packageResource != null) {
                        return this.load(packageResource);
                    } else {
                        throw new IllegalArgumentException("Invalid source '" + resolvedSource + "'");
                    }
                }
            }
        }
    

    刷新Spring应用上下文#

    然后我们回到SpringApplication#run方法中的this.refreshContext(context);这行代码,并进入方法体

     private void refreshContext(ConfigurableApplicationContext context) {
            // 刷新应用上下文
            this.refresh(context);
            if (this.registerShutdownHook) {
                try {
                    // 如果需要注册关闭钩子,则应用上下文注册关闭钩子
                    context.registerShutdownHook();
                } catch (AccessControlException var3) {
                }
            }
        }
    protected void refresh(ApplicationContext applicationContext) {
            Assert.isInstanceOf(AbstractApplicationContext.class, applicationContext);
            ((AbstractApplicationContext)applicationContext).refresh();
        }
    

    AbstractApplicationContext#refresh方法#

    public void refresh() throws BeansException, IllegalStateException {
            // 刷新应用上下文过程使用了监视器锁
            synchronized(this.startupShutdownMonitor) {
                // 预刷新
                this.prepareRefresh();
                // 获取刷新beanFactory
                ConfigurableListableBeanFactory beanFactory = this.obtainFreshBeanFactory();
                // 预备beanFactory
                this.prepareBeanFactory(beanFactory);
                try {
                    // 后置处理beanFactory
                    this.postProcessBeanFactory(beanFactory);
                    // 执行beanFactory后置处理器
                    this.invokeBeanFactoryPostProcessors(beanFactory);
                    // 注册bean后置处理器
                    this.registerBeanPostProcessors(beanFactory);
                    // 初始化消息源
                    this.initMessageSource();
                    // 初始化应用事件广播
                    this.initApplicationEventMulticaster();
                    // 调用onRefres方法,如果是Servlet应用程序,这个方法会去创建web服务器
                    this.onRefresh();
                    // 注册监听器
                    this.registerListeners();
                    // 结束beanFactory初始化
                    this.finishBeanFactoryInitialization(beanFactory);
                    // 结束刷新,如果是Servlet应用程序,个方法会去启动web服务器
                    this.finishRefresh();
                } catch (BeansException var9) {
                    if (this.logger.isWarnEnabled()) {
                        this.logger.warn("Exception encountered during context initialization - cancelling refresh attempt: " + var9);
                    }
                    // 发生异常时会销毁bean并取消刷新
                    this.destroyBeans();
                    this.cancelRefresh(var9);
                    throw var9;
                } finally {
                    // finally语句块中重新设置公共缓存
                    this.resetCommonCaches();
                }
            }
        }
    

    ServletWebServerApplicationContext#onRefresh方法#

    protected void onRefresh() {
            super.onRefresh();
            try {
                this.createWebServer();
            } catch (Throwable var2) {
                throw new ApplicationContextException("Unable to start web server", var2);
            }
        }
    

    ServletWebServerApplicationContext#createWebServer方法#

    private void createWebServer() {
            WebServer webServer = this.webServer;
            ServletContext servletContext = this.getServletContext();
            if (webServer == null && servletContext == null) {
                ServletWebServerFactory factory = this.getWebServerFactory();
                this.webServer = factory.getWebServer(new ServletContextInitializer[]{this.getSelfInitializer()});
            } else if (servletContext != null) {
                try {
                    this.getSelfInitializer().onStartup(servletContext);
                } catch (ServletException var4) {
                    throw new ApplicationContextException("Cannot initialize servlet context", var4);
                }
            }
            this.initPropertySources();
        }
    

    Spring容器启动后运行任务#

    SpringApplication#callRunners方法#

    private void callRunners(ApplicationContext context, ApplicationArguments args) {
            List<Object> runners = new ArrayList();
            // 通过应用上下文获取所有ApplicationRunner接口实现类的bean集合
            runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
            // 通过应用上下文获取所有CommandLineRunner接口实现类的bean集合
            runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
            AnnotationAwareOrderComparator.sort(runners);
            Iterator var4 = (new LinkedHashSet(runners)).iterator();
            // 遍历执行ApplicationRunner实现类和CommandLineRunner实现类中的run方法
            while(var4.hasNext()) {
                Object runner = var4.next();
                if (runner instanceof ApplicationRunner) {
                    this.callRunner((ApplicationRunner)runner, args);
                }
                if (runner instanceof CommandLineRunner) {
                    this.callRunner((CommandLineRunner)runner, args);
                }
            }
    private void callRunner(ApplicationRunner runner, ApplicationArguments args) {
            try {
                runner.run(args);
            } catch (Exception var4) {
                throw new IllegalStateException("Failed to execute ApplicationRunner", var4);
            }
    }
    private void callRunner(CommandLineRunner runner, ApplicationArguments args) {
            try {
                runner.run(args.getSourceArgs());
            } catch (Exception var4) {
                throw new IllegalStateException("Failed to execute CommandLineRunner", var4);
            }
        }
    

    处理启动异常#

    最后我们来看看SprignBoot应用程序启动发生异常时调用的方法

    SpringApplication#handleRunFailure方法#

     private void handleRunFailure(ConfigurableApplicationContext context, Throwable exception, Collection exceptionReporters, SpringApplicationRunListeners listeners) {
            try {
                try {
                    // 处理程序退出编码
                    this.handleExitCode(context, exception);
                    if (listeners != null) {
                        // 应用启动监听器发布启动失败事件
                        listeners.failed(context, exception);
                    }
                } finally {
                    // 报告异常
                    this.reportFailure(exceptionReporters, exception);
                    if (context != null) {
                        // 关闭Spring IOC容器
                        context.close();
                    }
                }
            } catch (Exception var9) {
                logger.warn("Unable to close ApplicationContext", var9);
            }
            // 调用反射工具类抛出运行时异常
            ReflectionUtils.rethrowRuntimeException(exception);
        }
    

    小结#

    可以看到SpringBoot项目启动过程的源码的源码还是非常复杂的,但是在难啃的骨头只要坚持下去还是能啃下它的。通过分析SpringBoot项目启动过程的源码分析,我们可以总结出SpringBoot项目启动过程主要做了以下几件事情:

    一、 实例化和初始化SpringApplication对象实例,在这个过程会去初始化SpringApplication对象的属性,包括:

    • 1.设置是够注册关停钩子标识
    • 2.推断Web应用程序类型
    • 3.加载META-INF/spring.factories配置文件中配置的初始化器和启动监听器
    • 4.推断项目主启动类等工作

    二、 运行SpringApplication实例对象的run方法,该方法返回的是一个AnnotationConfig在这个过程中又可以分解为以下几个步骤

    • 1.启动定时器记录记录整个SpringBoot应用启动过程花费时长
      1. 获取SpringApplication实例对象的启动类监听器并遍历发布应用开始启动事件
    • 3.实例化启动命令行参数
    • 4.打印SpringBoot项目图标
    • 5. 启动监听器发布应用开始启动事件
    • 6. 准备启动环境:这一过程会实例化一个ConfigurableEnvironment类的配置环境对象,并将从应用配置文件中读取到的环境变量填充到配置环境对象中;监听器发布环境准备事件,然后再将初始化的配置环境对象与SpringApplication实例对象绑定,绑定过程中会解析环境变量中的属性占位符变量
    • 7. 创建Spring应用上下文:这一过程会根据前面实例化和初始化SpringApplication过程中推断出的应用程序类型通过反射的方式加载和实例化具体的Spring应用上下文实现类。Servlet类型对应的Spring应用上下文是AnnotationConfigServletWebSewrverApplicationContext类实例
    • 8. 准备Spring应用上下文: 这一过程会去设置Spring应用上下文环境的环境属性、 后处理Spring应用上下文、监听器发布应用上下文加载事件、添加Spring应用上下文beanFactory后置处理器、加载配置类和xml配置文件以及扫描包下的Bean定义并注册到beanFactory,Spring应用上下文实际的beanFactoryDefaultListableBeanFactory,它里面使用了一个初始容量为256的ConcurrentHashMap的数据结构存放BeanDefination对象
    • 9 . 刷新应用上下文:如果是Servlet类型应用这个过程会去实例化和初始化一个web服务器并启动这个web服务器,如过启动web服务器失败则会在SpringApplication实例对象设置了关停钩子的情况下注册关停钩子,同事关闭web服务器并销毁所有的bean -10; 运行实现了ApplicationRunnerCommandLineRunnner接口组件类中的任务 -11. 如果整个运行过程捕获到异常则收集异常信息,监听器发布应用失败事件,抛出捕获到异常
    配置文件加载顺序参考链接: https://blog.csdn.net/qq_45136501/article/details/127792779

    __EOF__

  • 本文作者: DiligentCoder
  • 本文链接: https://www.cnblogs.com/LoveShare/p/16886802.html
  • 关于博主: 评论和私信会在第一时间回复。或者直接私信我。
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
  • 声援博主: 如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。
  • 相关阅读:
    一文者懂LLaMA 2(原理、模型、训练)
    立仪科技光谱共焦应用之金属隔膜静态重复性测量
    小功能⭐️退出游戏 && 监听事件
    请求工具类和base64转成MultipartFile工具类
    core dump(介绍,status中的core dump标志,应用--调试),ulimit命令
    C++学习 --list
    Go 在运维开发中的应用
    那些误导消费者的PoE交换机,你知道多少?
    java中的异常[35]
    jxTMS设计思想之兴趣点
  • 原文地址:https://www.cnblogs.com/LoveShare/p/16886802.html