SpringBoot 核心流程

1、手写模拟SpringBoot启动过程

通过手写模拟实现一个Spring Boot,让大家能以非常简单的方式就能知道Spring Boot大概是如何工作的。

完整的代码地址:zhouyu-springboot: 手写模拟springboot

依赖

建一个工程,两个Module:

  1. springboot模块,表示springboot框架的源码实现
  2. user包,表示用户业务系统,用来写业务代码来测试我们所模拟出来的SpringBoot

 首先,SpringBoot是基于的Spring,所以我们要依赖Spring,然后我希望我们模拟出来的SpringBoot也支持Spring MVC的那一套功能,所以也要依赖Spring MVC,包括Tomcat等,所以在SpringBoot模块中要添加以下依赖:

<dependencies>
<dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.3.18</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
            <version>5.3.18</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>5.3.18</version>
        </dependency>

        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>4.0.1</version>
        </dependency>

        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-core</artifactId>
            <version>9.0.60</version>
        </dependency>
</dependencies>

在User模块下我们进行正常的开发就行了,比如先添加SpringBoot依赖:

<dependencies>
<dependency>
<groupId>org.example</groupId>
<artifactId>springboot</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>

 然后定义相关的Controller和Service:

 

@RestController
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping("test")
    public String test(){
        return userService.test();
    }
}

 因为我们模拟实现的是SpringBoot,而不是SpringMVC,所以我直接在user包下定义了UserController和UserService,最终我希望能运行MyApplication中的main方法,就直接启动了项目,并能在浏览器中正常的访问到UserController中的某个方法。

核心注解和核心类

我们在真正使用SpringBoot时,核心会用到SpringBoot一个类和注解:

  1. @SpringBootApplication,这个注解是加在应用启动类上的,也就是main方法所在的类
  2. SpringApplication,这个类中有个run()方法,用来启动SpringBoot应用的

所以我们也来模拟实现他们。

一个@ZhouyuSpringBootApplication注解:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Configuration
@ComponentScan
public @interface ZhouyuSpringBootApplication {
}

一个用来实现启动逻辑的ZhouyuSpringApplication类。

public class ZhouyuSpringApplication {

    public static void run(Class clazz){

    }

}

注意run方法需要接收一个Class类型的参数,这个class是用来干嘛的,等会就知道了。

有了以上两者,我们就可以在MyApplication中来使用了,比如:

@ZhouyuSpringBootApplication
public class MyApplication {

    public static void main(String[] args) {
        ZhouyuSpringApplication.run(MyApplication.class);
    }
}

现在用来是有模有样了,但中看不中用,所以我们要来好好实现以下run方法中的逻辑了。

run方法

run方法中需要实现什么具体的逻辑呢?

首先,我们希望run方法一旦执行完,我们就能在浏览器中访问到UserController,那势必在run方法中要启动Tomcat,通过Tomcat就能接收到请求了。

大家如果学过Spring MVC的底层原理就会知道,在SpringMVC中有一个Servlet非常核心,那就是DispatcherServlet,这个DispatcherServlet需要绑定一个Spring容器,因为DispatcherServlet接收到请求后,就会从所绑定的Spring容器中找到所匹配的Controller,并执行所匹配的方法。

所以,在run方法中,我们要实现的逻辑如下:

  1. 创建一个Spring容器
  2. 创建Tomcat对象
  3. 生成DispatcherServlet对象,并且和前面创建出来的Spring容器进行绑定
  4. 将DispatcherServlet添加到Tomcat中
  5. 启动Tomcat

创建Spring容器

这个步骤比较简单,代码如下:

public class ZhouyuSpringApplication {

    public static void run(Class clazz){
        AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext();
        applicationContext.register(clazz);
        applicationContext.refresh();

        
    }
}

 

我们创建的是一个AnnotationConfigWebApplicationContext容器,并且把run方法传入进来的class作为容器的配置类,比如在MyApplication的run方法中,我们就是把MyApplication.class传入到了run方法中,最终MyApplication就是所创建出来的Spring容器的配置类,并且由于MyApplication类上有@ZhouyuSpringBootApplication注解,而@ZhouyuSpringBootApplication注解的定义上又存在@ComponentScan注解,所以AnnotationConfigWebApplicationContext容器在执行refresh时,就会解析MyApplication这个配置类,从而发现定义了@ComponentScan注解,也就知道了要进行扫描,只不过扫描路径为空,而AnnotationConfigWebApplicationContext容器会处理这种情况,如果扫描路径会空,则会将MyApplication所在的包路径做为扫描路径,从而就会扫描到UserService和UserController。

所以Spring容器创建完之后,容器内部就拥有了UserService和UserController这两个Bean。

启动Tomcat

我们用的是Embed-Tomcat,也就是内嵌的Tomcat,真正的SpringBoot中也用的是内嵌的Tomcat,而对于启动内嵌的Tomcat,也并不麻烦,代码如下:

public static void startTomcat(WebApplicationContext applicationContext){

Tomcat tomcat = new Tomcat();

Server server = tomcat.getServer();
Service service = server.findService("Tomcat");

Connector connector = new Connector();
connector.setPort(8081);

Engine engine = new StandardEngine();
engine.setDefaultHost("localhost");

Host host = new StandardHost();
host.setName("localhost");

String contextPath = "";
Context context = new StandardContext();
context.setPath(contextPath);
context.addLifecycleListener(new Tomcat.FixContextListener());

host.addChild(context);
engine.addChild(host);

service.setContainer(engine);
service.addConnector(connector);

tomcat.addServlet(contextPath, "dispatcher", new DispatcherServlet(applicationContext));
context.addServletMappingDecoded("/*", "dispatcher");

try {
tomcat.start();
} catch (LifecycleException e) {
e.printStackTrace();
}

}

 

代码虽然看上去比较多,但是逻辑并不复杂,比如配置了Tomcat绑定的端口为8081,后面向当前Tomcat中添加了DispatcherServlet,并设置了一个Mapping关系,最后启动,其他代码则不用太过关心。

而且在构造DispatcherServlet对象时,传入了一个ApplicationContext对象,也就是一个Spring容器,就是我们前文说的,DispatcherServlet对象和一个Spring容器进行绑定。

接下来,我们只需要在run方法中,调用startTomcat即可:

public static void run(Class clazz){
AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext();
applicationContext.register(clazz);
applicationContext.refresh();

startTomcat(applicationContext);

}

 

实际上代码写到这,一个极度精简版的SpringBoot就写出来了,比如现在运行MyApplication,就能正常的启动项目,并能接收请求。

实际上代码写到这,一个极度精简版的SpringBoot就写出来了,比如现在运行MyApplication,就能正常的启动项目,并能接收请求。

实现Tomcat和Jetty的切换

虽然我们前面已经实现了一个比较简单的SpringBoot,不过我们可以继续来扩充它的功能,比如现在我有这么一个需求,这个需求就是我现在不想使用Tomcat了,而是想要用Jetty,那该怎么办?

我们前面代码中默认启动的是Tomcat,那我现在想改成这样子:

  1. 如果项目中有Tomcat的依赖,那就启动Tomcat
  2. 如果项目中有Jetty的依赖就启动Jetty
  3. 如果两者都没有则报错
  4. 如果两者都有也报错

这个逻辑希望SpringBoot自动帮我实现,对于程序员用户而言,只要在Pom文件中添加相关依赖就可以了,想用Tomcat就加Tomcat依赖,想用Jetty就加Jetty依赖。

那SpringBoot该如何实现呢?

我们知道,不管是Tomcat还是Jetty,它们都是应用服务器,或者是Servlet容器,所以我们可以定义接口来表示它们,这个接口叫做WebServer(别问我为什么叫这个,因为真正的SpringBoot源码中也叫这个)。

并且在这个接口中定义一个start方法:

public interface WebServer {
    
    public void start();

}

有了WebServer接口之后,就针对Tomcat和Jetty提供两个实现类:

public class TomcatWebServer implements WebServer{

    @Override
    public void start() {
        System.out.println("启动Jetty");
    }
}
public class JettyWebServer implements WebServer{

    @Override
    public void start() {
       System.out.println("启动Tomcat");
    }
}

而在ZhouyuSpringApplication中的run方法中,我们就要去获取对应的WebServer,然后启动对应的webServer,代码为:

public static void run(Class clazz){
AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext();
applicationContext.register(clazz);
applicationContext.refresh();

WebServer webServer = getWebServer(applicationContext);
webServer.start();

}

public static WebServer getWebServer(ApplicationContext applicationContext){
return null;
}

 

这样,我们就只需要在getWebServer方法中去判断到底该返回TomcatWebServer还是JettyWebServer。

前面提到过,我们希望根据项目中的依赖情况,来决定到底用哪个WebServer,我就直接用SpringBoot中的源码实现方式来模拟了。

模拟实现条件注解

首先我们得实现一个条件注解@ZhouyuConditionalOnClass,对应代码如下:

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Conditional(ZhouyuOnClassCondition.class)
public @interface ZhouyuConditionalOnClass {
    String value() default "";
}

注意核心为@Conditional(ZhouyuOnClassCondition.class)中的ZhouyuOnClassCondition,因为它才是真正得条件逻辑:

public class ZhouyuOnClassCondition implements Condition {

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        Map<String, Object> annotationAttributes = 
metadata.getAnnotationAttributes(ZhouyuConditionalOnClass.class.getName());

        String className = (String) annotationAttributes.get("value");

        try {
            context.getClassLoader().loadClass(className);
            return true;
        } catch (ClassNotFoundException e) {
            return false;
        }
    }
}

 

具体逻辑为,拿到@ZhouyuConditionalOnClass中的value属性,然后用类加载器进行加载,如果加载到了所指定的这个类,那就表示符合条件,如果加载不到,则表示不符合条件。

模拟实现自动配置类

有了条件注解,我们就可以来使用它了,那如何实现呢?

这里就要用到自动配置类的概念,我们先看代码:

@Configuration
public class WebServiceAutoConfiguration {

    @Bean
    @ZhouyuConditionalOnClass("org.apache.catalina.startup.Tomcat")
    public TomcatWebServer tomcatWebServer(){
        return new TomcatWebServer();
    }

    @Bean
    @ZhouyuConditionalOnClass("org.eclipse.jetty.server.Server")
    public JettyWebServer jettyWebServer(){
        return new JettyWebServer();
    }
}

 

这个代码还是比较简单的,通过一个WebServiceAutoConfiguration的Spring配置类,在里面定义了两个Bean,一个TomcatWebServer,一个JettyWebServer,不过这两个要生效的前提是符合当前所指定的条件,比如:

  1. 只有存在"org.apache.catalina.startup.Tomcat"类,那么才有TomcatWebServer这个Bean
  2. 只有存在"org.eclipse.jetty.server.Server"类,那么才有TomcatWebServer这个Bean

并且我们只需要在ZhouyuSpringApplication中getWebServer方法,如此实现:

public static WebServer getWebServer(ApplicationContext applicationContext){
// key为beanName, value为Bean对象
Map<String, WebServer> webServers = applicationContext.getBeansOfType(WebServer.class);

if (webServers.isEmpty()) {
throw new NullPointerException();
}
if (webServers.size() > 1) {
throw new IllegalStateException();
}

// 返回唯一的一个
return webServers.values().stream().findFirst().get();
}

这样整体SpringBoot启动逻辑就是这样的:

  1. 创建一个AnnotationConfigWebApplicationContext容器
  2. 解析MyApplication类,然后进行扫描
  3. 通过getWebServer方法从Spring容器中获取WebServer类型的Bean
  4. 调用WebServer对象的start方法

有了以上步骤,我们还差了一个关键步骤,就是Spring要能解析到WebServiceAutoConfiguration这个自动配置类,因为不管这个类里写了什么代码,Spring不去解析它,那都是没用的,此时我们需要SpringBoot在run方法中,能找到WebServiceAutoConfiguration这个配置类并添加到Spring容器中。

MyApplication是Spring的一个配置类,但是MyApplication是我们传递给SpringBoot,从而添加到Spring容器中去的,而WebServiceAutoConfiguration就需要SpringBoot去自动发现,而不需要程序员做任何配置才能把它添加到Spring容器中去,而且要注意的是,Spring容器扫描也是扫描不到WebServiceAutoConfiguration这个类的,因为我们的扫描路径是"com.zhouyu.user",而WebServiceAutoConfiguration所在的包路径为"com.zhouyu.springboot"。

那SpringBoot中是如何实现的呢?通过SPI,当然SpringBoot中自己实现了一套SPI机制,也就是我们熟知的spring.factories文件,那么我们模拟就不搞复杂了,就直接用JDK自带的SPI机制。

发现自动配置类

为了实现这个功能,以及为了最后的效果演示,我们需要把springboot源码和业务代码源码拆分两个maven模块,也就相当于两个项目,最后的源码结构为:

现在我们只需要在springboot项目中的resources目录下添加如下目录(META-INF/services)和文件:

SPI的配置就完成了,相当于通过com.zhouyu.springboot.AutoConfiguration文件配置了springboot中所提供的配置类。

并且提供一个接口:

public interface AutoConfiguration {
}

并且WebServiceAutoConfiguration实现该接口:

@Configuration
public class WebServiceAutoConfiguration implements AutoConfiguration {

    @Bean
    @ZhouyuConditionalOnClass("org.apache.catalina.startup.Tomcat")
    public TomcatWebServer tomcatWebServer(){
        return new TomcatWebServer();
    }

    @Bean
    @ZhouyuConditionalOnClass("org.eclipse.jetty.server.jetty")
    public JettyWebServer jettyWebServer(){
        return new JettyWebServer();
    }
}

 然后我们再利用spring中的@Import技术来导入这些配置类,我们在@ZhouyuSpringBootApplication的定义上增加如下代码:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Configuration
@ComponentScan
@Import(ZhouyuImportSelect.class)
public @interface ZhouyuSpringBootApplication {
}

ZhouyuImportSelect类为:

public class ZhouyuImportSelect implements DeferredImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        ServiceLoader<AutoConfiguration> serviceLoader = ServiceLoader.load(AutoConfiguration.class);

        List<String> list = new ArrayList<>();
        for (AutoConfiguration autoConfiguration : serviceLoader) {
            list.add(autoConfiguration.getClass().getName());
        }

        return list.toArray(new String[0]);
    }
}

这就完成了从com.zhouyu.springboot.AutoConfiguration文件中获取自动配置类的名字,并导入到Spring容器中,从而Spring容器就知道了这些配置类的存在,而对于user项目而言,是不需要修改代码的。

此时运行MyApplication,就能看到启动了Tomcat:

 因为SpringBoot默认在依赖中添加了Tomcat依赖,而如果在User模块中再添加jetty的依赖:

<dependencies>
<dependency>
<groupId>org.example</groupId>
<artifactId>springboot</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>

<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<version>9.4.43.v20210629</version>
</dependency>
</dependencies>

总结

到此,我们实现了一个简单版本的SpringBoot,因为SpringBoot首先是基于Spring的,而且提供的功能也更加强大,随着后续内容的展开,相信大家会对本文中的各个功能会有更加深刻的理解,也希望大家都自己去实现一边,完整的代码地址:zhouyu-springboot: 手写模拟springboot

 

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
手把手视频详细讲解项目开发全过程,需要的小伙伴自行百度网盘下载,链接见附件,永久有效。 视频简介: 目前业界最流行的微服务架构正在或者已被各种规模的互联网公司广泛接受和认可,业已成为互联网开发人员必备技术。无论是互联网、云计算还是大数据Java平台已成为全栈的生态体系,其重要性几乎不可替代。Spring Boot作为微服务的基础设施之一,背靠强大的Spring 生态社区,支撑Spring Cloud技术体系。本课程采用由浅入深,层层递进的讲解方式, 让你轻松掌握SpringBoot的快速构建Spring项目的方式,并且还深入剖析SpringBoot内部核心原理,如:自动配置原理,start原理,自定义start等, 让你知其然,知其所以然 讲解方式: 本课程采用由浅入深,层层递进的讲解方式, 让你轻松掌握SpringBoot的快速构建Spring项目的方式,并且还深入剖析SpringBoot内部核心原理,如:自动配置原理,start原理,自定义start等, 让你知其然,知其所以然 课程亮点: 1、课程由浅到深,由原理到实践,适合零基础入门学习。 2、课程中包含大量SpringBoot 原理讲解、源码分析。 3、课程中涉及很多SpringBoot 实用插件技术、监控技术; 适用人群: 1、有一定的Java基础以及SSM框架知识。 2、对目前职业有进一步提升要求,希望从事数据行业高薪工作的在职人员。 基础课程主讲内容包括: 阶段一:SpringBoot 快速入门 1. SpringBoot介绍 2. SpringBoot核心功能 3. SpringBoot的优势 4. SpringBoot入门程序 5. SpringBoot配置文件类型 6. 配置文件与配置类的属性映射方式 7. SpringBoot整合Mybatis 8. SpringBoot整合Junit 9. SpringBoot整合Redis 阶段二: SpringBoot核心原理 1. 起步依赖原理分析 2. 自动配置原理解析 3. 自定义起步依赖并实现自动配置 4. 事件监听 5. 初始化流程 6. SpringBoot服务监控
学习尚硅谷视频整理的文档 Spring Boot 1 1 Spring Boot入门 4 1.1 简介 4 1.2 微服务(martin fowler发表了一篇文章) 5 1.3 环境约束 7 1.4 第一个Spring Boot项目(jar):HelloWorld 8 1.5 入门案例详解 11 1.5.1 POM文件 11 1.5.2 主程序类,主入口类 12 1.6 使用Spring Initializer向导快速创建Spring Boot 16 2 Spring Boot配置 18 2.1 配置文件 18 2.2 YML语法 19 2.3 YML配置文件值获取 21 2.4 properties配置文件乱码问题 24 2.5 @ConfigurationProperties与@Value的区别 25 2.6 配置@PropertySource、@ImportResource、@Bean 27 2.7 配置文件占位符 30 2.8 Profile多环境支持 31 2.9 配置文件的加载位置 33 2.10 外部配置加载顺序 36 2.11 自动配置原理 37 2.12 @Conditional派生注解 41 3 Spring Boot与日志 42 3.1 日志框架分类和选择 42 3.2 SLF4j使用 43 3.3 其他日志框架统一转换成slf4j+logback 44 3.4 Spring Boot日志使用 45 3.5 Spring Boot默认配置 47 3.6 指定日志文件和日志Profile功能 52 3.7 切换日志框架(不使用SLF4j+LogBack) 54 4 Spring Boot与Web开发 55 4.1 Web开发简介 55 4.2 静态资源映射规则 56 4.3 引入Thymeleaf 60 4.4 Thymeleaf语法 61 4.5 SpringMVC自动配置原理 67 4.6 SpringBoot扩展与全面接管 70 4.7 如何修改SpringBoot的默认配置 72 4.8 【实验】CRUD操作 73 4.8.1 默认访问首页 73 4.8.2 登录页面国际化 74 4.8.3 登录 80 4.8.4 拦截器进行登录检查 81 4.8.5 实验要求(没按要求做,不想改了!) 82 4.8.6 CRUD-员工列表 83 4.8.7 CRUD-员工修改 86 4.8.8 CRUD-员工添加 87 4.8.9 CRUD-员工删除 88 4.9 错误处理原理&错误页面定制 90 4.10 配置嵌入式Servlet容器(springboot 1.50版本) 97 4.10.1 如何定制和修改Servelt容器的相关配置 97 4.10.2 注册servlet三大组件【servlet,filter,listener】 98 4.10.3 替换为其他嵌入式容器 102 4.10.4 嵌入式servlet容器自动配置原理 103 4.10.5 嵌入式servlet容器启动原理 103 4.11 使用外置的Servlet容器 104 4.11.1 步骤 104 4.11.2 原理 107 5 Spring Boot与Docker(虚拟化容器技术) 110 5.1 简介 110 5.2 核心概念 111 5.3 安装Docker 112 5.4 Docker常用命令&操作 113 5.5 安装MySQL示例 114 6 Spring Boot与数据访问 115 6.1 JDBC 115 6.1.1 实现 115 6.1.2 自动配置原理 116 6.2 整合Durid数据源 117 6.3 整合Mybatis 122 6.3.1 注解版 123 6.3.2 配置文件版 124 6.4 整合SpringData JPA 125 6.4.1 SpringData简介 125 6.4.2 整合 126 7 Spring Boot启动配置原理 128 7.1 启动流程Springboot 1.50版本) 128 7.1.1 创建SpringApplication对象 129 7.1.2 运行run方法 130 7.1.3 编写事件监听机制 132 8 Spring Boot自定义starters 136 8.1 概述 136 8.2 步骤 137 9 更多Springboot整合示例 144 10 Spring Boot与缓存 145 10.1 JSR107缓存规范 145 10.2 Spring的缓存抽象 146 10.2.1 基本概念 146 10.2.2 整合项目 146 10.2.3 CacheEnable注解 148 10.2.4 Cache注解 150 10.3 整合redis 154 10.3.1 在Docker上安装redis 154 10.3.2 Redis的Template 154 10.3.3 整合(百度) 155
Spring Boot 启动过程可以分为以下几个阶段: 1. 加载应用程序类:Spring Boot 启动时,首先会根据配置文件中的 `spring.main.sources` 属性加载应用程序类。如果该属性未指定,则默认加载启动类。 2. 加载 Spring 环境:Spring Boot 会根据配置文件中的 `spring.config.name`、`spring.config.location`、`spring.profiles.active` 等属性来加载 Spring 环境。其中,`spring.config.name` 指定配置文件名,`spring.config.location` 指定配置文件路径,`spring.profiles.active` 指定激活的环境。Spring Boot 会依次从 classpath、文件系统、URL 和其他地方加载配置文件,然后将它们合并成一个统一的配置环境。 3. 执行自动配置:Spring Boot 会根据应用程序的依赖和配置,自动配置应用程序所需要的组件。Spring Boot 自动配置是基于条件的,即只有在满足一定的条件时才会自动配置。 4. 创建 Spring 应用程序上下文:Spring Boot 会创建一个 Spring 应用程序上下文,加载所有的 Bean 定义和依赖,并且将它们组合在一起。Spring Boot 应用程序上下文是基于 Spring 的 `ApplicationContext` 接口实现的,但是它会根据应用程序的类型和配置来自动决定使用哪个具体的实现。 5. 注册应用程序监听器:Spring Boot 会注册一些应用程序监听器,例如 `ApplicationStartedEvent`、`ApplicationEnvironmentPreparedEvent`、`ApplicationPreparedEvent`、`ApplicationFailedEvent` 等,用于监听应用程序的启动过程。 6. 启动 Web 服务器:如果应用程序是 Web 项目,Spring Boot 会启动嵌入式 Web 服务器,例如 Tomcat、Jetty 或 Undertow。Spring Boot 嵌入式 Web 服务器是基于 Servlet 规范实现的,并且支持 Servlet 3.0 规范及以上版本。 7. 运行应用程序:Spring Boot 启动后,应用程序会开始运行,并且接收来自外部的请求。在运行期间,Spring Boot 还会监听一些系统事件,例如 `ContextRefreshedEvent`、`ContextClosedEvent` 等,用于监听应用程序的运行过程。 以上就是 Spring Boot 的详细启动流程

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值