SpringBoot 究竟是如何跑起来的?

电动汽车双层优化调度:架构设计与工程实践 电力系统优化调度是智能电网的核心技术,其本质是通过数学建模与算法设计实现资源的最优配置。双层优化作为一种分层决策框架,上层解决全局资源分配问题,下层处理分布式单元控制,特别适合电动汽车规模化接入场景。在工程实现层面,需要结合边缘计算设备(如NVIDIA Jetson)的实时处理能力,以及改进的通信协议(如IEEE C37.118.2)确保数据传输可靠性。实际部署中,负荷预测算法精度和实时定价机制直接影响用户体验,采用LSTM网络可将预测误差控制在8%以内。该技术已在华东示范区验证,使充电站收益提升28%,为 阅读详情

不得不说 SpringBoot 太复杂了,我本来只想研究一下 SpringBoot 最简单的 HelloWorld 程序是如何从 main 方法一步一步跑起来的,但是这却是一个相当深的坑。你可以试着沿着调用栈代码一层一层的深入进去,如果你不打断点,你根本不知道接下来程序会往哪里流动。这个不同于我研究过去的 Go 语言、Python 语言框架,它们通常都非常直接了当,设计上清晰易懂,代码写起来简单,里面的实现同样也很简单。但是 SpringBoot 不是,它的外表轻巧简单,但是它的里面就像一只巨大的怪兽,这只怪兽有千百只脚把自己缠绕在一起,把爱研究源码的读者绕的晕头转向。但是这 Java 编程的世界 SpringBoot 就是老大哥,你却不得不服。即使你的心中有千万头草泥马在奔跑,但是它就是天下第一。如果你是一个学院派的程序员,看到这种现象你会怀疑人生,你不得不接受一个规则 —— 受市场最欢迎的未必就是设计的最好的,里面夹杂着太多其它的非理性因素。

640?wx_fmt=png
图片

经过了一番痛苦的折磨,我还是把 SpringBoot 的运行原理摸清楚了,这里分享给大家。

Hello World

首先我们看看 SpringBoot 简单的 Hello World 代码,就两个文件 HelloControll.java 和 Application.java,运行 Application.java 就可以跑起来一个简单的 RESTFul Web 服务器了。

640?wx_fmt=png
图片

640?wx_fmt=png
图片
// HelloController.java
package hello;

import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestMapping;

@RestController
public class HelloController {

    @RequestMapping("/")
    public String index() {
        return "Greetings from Spring Boot!";
    }

}

// Application.java
package hello;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}

640?wx_fmt=png
图片

但是问题来了,在 Application 的 main 方法里我压根没有任何地方引用 HelloController 类,那么它的代码又是如何被服务器调用起来的呢?这就需要深入到 SpringApplication.run() 方法中看个究竟了。不过即使不看代码,我们也很容易有这样的猜想,SpringBoot 肯定是在某个地方扫描了当前的 package,将带有 RestController 注解的类作为 MVC 层的 Controller 自动注册进了 Tomcat Server。

还有一个让人不爽的地方是 SpringBoot 启动太慢了,一个简单的 Hello World 启动居然还需要长达 5 秒,要是再复杂一些的项目这样龟漫的启动速度那真是不好想象了。

再抱怨一下,这个简单的 HelloWorld 虽然 pom 里只配置了一个 maven 依赖,但是传递下去,它一共依赖了 36 个 jar 包,其中以 spring 开头的 jar 包有 15 个。说这是依赖地狱真一点不为过。

640?wx_fmt=png
图片

批评到这里就差不多了,下面就要正是进入主题了,看看 SpringBoot 的 main 方法到底是如何跑起来的。

SpringBoot 的堆栈

了解 SpringBoot 运行的最简单的方法就是看它的调用堆栈,下面这个启动调用堆栈还不是太深,我没什么可抱怨的。

640?wx_fmt=png
图片
public class TomcatServer {

  @Override
  public void start() throws WebServerException {
  ...
  }

}

640?wx_fmt=png
图片

我通过将 IDE 窗口全屏化,并将其它的控制台窗口源码窗口统统最小化,总算勉强一个屏幕装下了整个调用堆栈。

不过转念一想,这也不怪 SpringBoot,绝大多数都是 Tomcat 的调用堆栈,跟 SpringBoot 相关的只有不到 10 层。

探索 ClassLoader

SpringBoot 还有一个特色的地方在于打包时它使用了 FatJar 技术将所有的依赖 jar 包一起放进了最终的 jar 包中的 BOOT-INF/lib 目录中,当前项目的 class 被统一放到了 BOOT-INF/classes 目录中。

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

├── BOOT-INF
│   ├── classes
│   │   └── hello
│   └── lib
│       ├── classmate-1.3.4.jar
│       ├── hibernate-validator-6.0.12.Final.jar
│       ├── jackson-annotations-2.9.0.jar
│       ├── jackson-core-2.9.6.jar
│       ├── jackson-databind-2.9.6.jar
│       ├── jackson-datatype-jdk8-2.9.6.jar
│       ├── jackson-datatype-jsr310-2.9.6.jar
│       ├── jackson-module-parameter-names-2.9.6.jar
│       ├── javax.annotation-api-1.3.2.jar
│       ├── jboss-logging-3.3.2.Final.jar
│       ├── jul-to-slf4j-1.7.25.jar
│       ├── log4j-api-2.10.0.jar
│       ├── log4j-to-slf4j-2.10.0.jar
│       ├── logback-classic-1.2.3.jar
│       ├── logback-core-1.2.3.jar
│       ├── slf4j-api-1.7.25.jar
│       ├── snakeyaml-1.19.jar
│       ├── spring-aop-5.0.9.RELEASE.jar
│       ├── spring-beans-5.0.9.RELEASE.jar
│       ├── spring-boot-2.0.5.RELEASE.jar
│       ├── spring-boot-autoconfigure-2.0.5.RELEASE.jar
│       ├── spring-boot-starter-2.0.5.RELEASE.jar
│       ├── spring-boot-starter-json-2.0.5.RELEASE.jar
│       ├── spring-boot-starter-logging-2.0.5.RELEASE.jar
│       ├── spring-boot-starter-tomcat-2.0.5.RELEASE.jar
│       ├── spring-boot-starter-web-2.0.5.RELEASE.jar
│       ├── spring-context-5.0.9.RELEASE.jar
│       ├── spring-core-5.0.9.RELEASE.jar
│       ├── spring-expression-5.0.9.RELEASE.jar
│       ├── spring-jcl-5.0.9.RELEASE.jar
│       ├── spring-web-5.0.9.RELEASE.jar
│       ├── spring-webmvc-5.0.9.RELEASE.jar
│       ├── tomcat-embed-core-8.5.34.jar
│       ├── tomcat-embed-el-8.5.34.jar
│       ├── tomcat-embed-websocket-8.5.34.jar
│       └── validation-api-2.0.1.Final.jar
├── META-INF
│   ├── MANIFEST.MF
│   └── maven
│       └── org.springframework
└── org
    └── springframework
        └── boot

在运行机制上,使用 FatJar 技术运行程序是需要对 jar 包进行改造的,它还需要自定义自己的 ClassLoader 来加载 jar 包里面 lib 目录中嵌套的 jar 包中的类。我们可以对比一下两者的 MANIFEST 文件就可以看出明显差异

// Generated by Maven Shade Plugin
Manifest-Version: 1.0
Implementation-Title: gs-spring-boot
Implementation-Version: 0.1.0
Built-By: qianwp
Implementation-Vendor-Id: org.springframework
Created-By: Apache Maven 3.5.4
Build-Jdk: 1.8.0_191
Implementation-URL: https://projects.spring.io/spring-boot/#/spring-bo
 ot-starter-parent/gs-spring-boot
Main-Class: hello.Application

// Generated by SpringBootLoader Plugin
Manifest-Version: 1.0
Implementation-Title: gs-spring-boot
Implementation-Version: 0.1.0
Built-By: qianwp
Implementation-Vendor-Id: org.springframework
Spring-Boot-Version: 2.0.5.RELEASE
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: hello.Application
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Created-By: Apache Maven 3.5.4
Build-Jdk: 1.8.0_191
Implementation-URL: https://projects.spring.io/spring-boot/#/spring-bo
 ot-starter-parent/gs-spring-boot

public class JarLauncher{
    ...
  static void main(String[] args) {
    new JarLauncher().launch(args);
  }

  protected void launch(String[] args) {
    try {
      JarFile.registerUrlProtocolHandler();
      ClassLoader cl = createClassLoader(getClassPathArchives());
      launch(args, getMainClass(), cl);
    }
    catch (Exception ex) {
        ex.printStackTrace();
        System.exit(1);
    }
  }

  protected void launch(String[] args, String mcls, ClassLoader cl) {
        Runnable runner = createMainMethodRunner(mcls, args, cl);
        Thread runnerThread = new Thread(runner);
        runnerThread.setContextClassLoader(classLoader);
        runnerThread.setName(Thread.currentThread().getName());
        runnerThread.start();
  }

}

class MainMethodRunner {
  @Override
  public void run() {
    try {
      Thread th = Thread.currentThread();
      ClassLoader cl = th.getContextClassLoader();
      Class<?> mc = cl.loadClass(this.mainClassName);
      Method mm = mc.getDeclaredMethod("main", String[].class);
      if (mm == null) {
        throw new IllegalStateException(this.mainClassName
                        + " does not have a main method");
      }
      mm.invoke(null, new Object[] { this.args });
    } catch (Exception ex) {
      ex.printStackTrace();
      System.exit(1);
    }
  }
}

又一个问题来了,当 JVM 遇到一个不认识的类,BOOT-INF/lib 目录里又有那么多 jar 包,它是如何知道去哪个 jar 包里加载呢?我们继续看这个特别的 ClassLoader 的源码

class LaunchedURLClassLoader extends URLClassLoader {
  ...
  private Class<?> doLoadClass(String name) {
    if (this.rootClassLoader != null) {
      return this.rootClassLoader.loadClass(name);
    }

    findPackage(name);
    Class<?> cls = findClass(name);
    return cls;
  }

}

private void findPackage(final String name) {
    int lastDot = name.lastIndexOf('.');
    if (lastDot != -1) {
        String packageName = name.substring(0, lastDot);
        if (getPackage(packageName) == null) {
            try {
                definePackage(name, packageName);
            } catch (Exception ex) {
                // Swallow and continue
            }
        }
    }
}

private final HashMap<String, Package> packages = new HashMap<>();

protected Package getPackage(String name) {
    Package pkg;
    synchronized (packages) {
        pkg = packages.get(name);
    }
    if (pkg == null) {
        if (parent != null) {
            pkg = parent.getPackage(name);
        } else {
            pkg = Package.getSystemPackage(name);
        }
        if (pkg != null) {
            synchronized (packages) {
                Package pkg2 = packages.get(name);
                if (pkg2 == null) {
                    packages.put(name, pkg);
                } else {
                    pkg = pkg2;
                }
            }
        }
    }
    return pkg;
}

private void definePackage(String name, String packageName) {
  String path = name.replace('.', '/').concat(".class");
  for (URL url : getURLs()) {
    try {
      if (url.getContent() instanceof JarFile) {
        JarFile jf= (JarFile) url.getContent();
        if (jf.getJarEntryData(path) != null && jf.getManifest() != null) {
          definePackage(packageName, jf.getManifest(), url);
          return null;
        }
      }
    } catch (IOException ex) {
        // Ignore
    }
  }
  return null;
}

深层 jar 包的内嵌 class 的 URL 路径长下面这样,使用感叹号 ! 分割

jar:file:/workspace/springboot-demo/target/application.jar!/BOOT-INF/lib/snakeyaml-1.19.jar!/org/yaml/snakeyaml/Yaml.class

不得不说,SpringbootLoader 的设计还是很有意思的,它本身很轻量级,代码逻辑很独立没有其它依赖,它也是 SpringBoot 值得欣赏的点之一。

HelloController 自动注册

还剩下最后一个问题,那就是 HelloController 没有被代码引用,它是如何注册到 Tomcat 服务中去的?它靠的是注解传递机制。

640?wx_fmt=png
图片

SpringBoot 深度依赖注解来完成配置的自动装配工作,它自己发明了几十个注解,确实严重增加了开发者的心智负担,你需要仔细阅读文档才能知道它是用来干嘛的。Java 注解的形式和功能是分离的,它不同于 Python 的装饰器是功能性的,Java 的注解就好比代码注释,本身只有属性,没有逻辑,注解相应的功能由散落在其它地方的代码来完成,需要分析被注解的类结构才可以得到相应注解的属性。

那注解是又是如何传递的呢?

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

@ComponentScan
public @interface SpringBootApplication {
...
}

public @interface ComponentScan {
    String[] basePackages() default {};
}

@RestController
public class HelloController {
...
}

@Controller
public @interface RestController {
}

扫描处理注解是一个非常繁琐肮脏的活计,特别是这种用注解来注解注解(绕口)的高级使用方法,这种方法要少用慎用。SpringBoot 中有大量的注解相关代码,企图理解这些代码是乏味无趣的没有必要的,它只会把你的本来清醒的脑袋搞晕。SpringBoot 对于习惯使用的同学来说它是非常方便的,但是其内部实现代码不要轻易模仿,那绝对算不上模范 Java 代码。

640?wx_fmt=png
图片

最后老钱表示自己真的很讨厌 SpringBoot 这只怪兽,但是很无奈,这个世界人人都在使用它。这就好比老人们常常告诫年轻人的那句话:如果你改变不了世界,那就先适应这个世界吧!

640?wx_fmt=png

如何实现Docker镜像导入导出 ? 摘要: Docker镜像迁移与备份可通过两种方式实现:1)镜像操作(save/load)完整保存分层结构和元数据,适用于完整备份;2)容器操作(export/import)仅保存文件系统快照,适合创建轻量模板。关键区别在于前者保留历史记录和分层,后者生成单层镜像。save/load命令示例:docker save -o nginx.tar nginx:latest,export/import示例:docker export -o container.tar abc123 && docker 阅读详情

相关推荐

游戏开发实战:如何用旋转矩阵实现2D角色平滑转向(附Unity代码)

本文深入探讨了在Unity游戏开发中,如何运用旋转矩阵原理实现2D角色的平滑转向。文章从核心理论出发,提供了计算目标旋转、实现平滑插值(Lerp与SmoothDamp)的完整C#代码,并解决了与动画系统、物理引擎协同的常见难题,帮助开发者提升游戏操作手感与视觉流畅度。

weixin_29057237的博客 290

拿到一个新的SpringBoot+Maven多模块项目,怎么跑起来

本文讲解用IDEA打开并运行一个新的Sprinboot多模块项目所用到的配置。

lllll520520520520520的博客 4940

PCB压合原理与工艺全流程深度解析

PCB压合是多层印制电路板制造中的核心工序,其本质并非简单物理叠加,而是高分子树脂流动、玻璃布应力释放、铜箔热膨胀及界面能调控的协同过程。理解压合原理,需掌握温度-压力-时间三维曲线动态关系;优化工艺流程,须兼顾叠板精度、热压参数匹配与冷却策略;关键影响因素如半固化片(PP)选型、铜箔表面处理、设备温场均匀性、结构对称性、环境湿度及操作手法,共同决定层间结合力、翘曲度与孔壁质量。本文围绕PCB压合原理、工艺流程、关键影响因素三大维度展开,融合IPC标准、实测数据与产线案例,为工艺工程师、NPI人员及设备维护

weixin_29191081的博客 259

1个spring-boot项目是怎么跑起来的之123

一般写在src-main-resource目录的application.yaml文件中,可以被其他命名的application-1或者-2里的值覆盖;1.运行时参数,也就是环境变量,一些在本地配了值(如数据库地址)本地能跑起来,代码拿到别人电脑/流水线环境上需要换成另一些值的参数;但如果不想每次提交代码都去掉默认值,可以在configuration中配置。【不分先后,因为我拼凑不出完整流程】

weixin_46358769的博客 793

Camera(5)metadata

Metadata 是整个 Android camera 中类似于高楼大厦中的管道系统一样,贯穿了整个 Camera 的 Framework 和 vendor HAL。Metadata 里面包含了所有的控制、参数、返回值等等。Metadata分为Android原生的Metadata和芯片商或者用户自定义的Metadata,即Android Tag和Vendor Tag。Vendor Tag在Hal中定义,并在camx和chi目录中使用。

qq_55893245的博客 3056

从零部署Stable Diffusion:本地环境搭建完整教程

本文专为“想自己跑Stable Diffusion但完全没经验”的新手设计,覆盖从硬件准备到生成第一张图的完整流程。不涉及复杂算法原理,但会讲清每个操作的“为什么”(比如为什么需要显卡?为什么要装CUDA?),帮你知其然更知其所以然。先检查硬件是否达标(显卡是核心!安装必要软件(操作系统、Python、CUDA)下载Stable Diffusion核心代码和模型解决常见报错,生成第一张图附赠模型库推荐和性能优化技巧显卡是核心:N卡+足够显存(至少6G)是本地部署的基础。CUDA是翻译。

AI算力网络与通信的博客 1294

SpringBoot 究竟是如何跑起来的?

尽管 SpringBoot 可以自动完成大部分配置工作,但你可能需要根据你的需求进行一些自定义配置。SpringBoot 提供了多种方式来实现自定义配置。例如,你可以使用注解来定义一个配置类,然后在类中使用@Bean注解来定义需要的 Bean。除此之外,你还可以使用@Value注解将配置属性值注入到 Bean 中。如果你需要更复杂的自定义配置,你可以创建一个实现了接口的条件类,通过实现其中的matches方法来自定义配置的生效条件。

2301_77102488的博客 491

AD服务器搭建

一、角色安装1)打开服务器管理器,添加 角色和功能;2) 点 下一步;3)点 下一步;4)点 下一步;5)点击安装;6-1)将此服务器提升为域控制器;6-2)点下一步;6)输入密码 点 下一步;7)自动生成LIXF,点击下一步;8)确认目录,点下一步;8)确认安装条件,点安装; 8)安装成功,点关闭;...

潇峰的博客 6454

运行 Spring Boot 有哪几种方式?

运行 Spring Boot 有哪几种方式?

2301_77899321的博客 8578

Tiff、jpg图片怎么样添加地理坐标信息

Tiff、jpg图片怎么样添加地理坐标信息 一般,外业调查只有地图资料,如专题图、或者地形图,如何把这些地图资料的扫描地图带上坐标信息,从而内业实现信息化处理?以上问题,可以解决,但根据不同情况有不同的处理方法。     第一种,上面有经纬度外框,图片是矩形,但是没有地理信息。 通过图框的两个角的坐标确定范围,分别是左上(140°E,30°N);右下(150°W,30°N) 解决方

树木_xzw的博客 2万+

PAM4信号测试实战:从眼图分析到误码率测量的技术演进

在高速数字系统设计中,信号完整性测试是确保数据传输可靠性的核心环节。其基本原理在于通过时域和频域分析,精确评估信号在传输过程中的质量衰减与失真。随着数据速率向56Gbps及以上演进,传统的NRZ编码面临带宽瓶颈,而PAM4调制技术通过在每个符号周期内承载更多比特信息,成为提升信道效率的关键路径。这项技术的工程价值在于,它允许在相同带宽下实现翻倍的数据吞吐量,这对于数据中心互联、PCIe 6.0及800G以太网等高速接口至关重要。然而,PAM4信号电平间距更小,对噪声和抖动极为敏感,导致其原始误码率显著升高,

weixin_42522817的博客 659

SpringBoot 究竟是如何跑起来的

SpringBoot 究竟是如何跑起来的?看完这篇文章让你恍然大悟!!

万维网连五洲,二进制写尽天下事,喜欢结识新伙伴寻找志同道合的朋友,欢迎一起探讨学习 3256

34、高效探索柔性蛋白质环构象空间的算法

本文介绍了一种高效探索柔性蛋白质环构象空间的算法,通过种子采样和变形采样两种技术,同时满足运动学闭合和避免空间位阻冲突的约束条件。种子采样能够在闭合无冲突构象空间中广泛生成构象,而变形采样则可以对“有趣”区域进行更密集的探索。实验结果表明,这两种方法在不同大小和受限程度的蛋白质环上均表现出色,运行时间在可接受范围内。相关算法已集成到工具包LoopTK中,可在https://simtk.org/home/looptk获取。

moon9的博客 128

五分钟了解如何把Springboot+Vue跑起来

Spring Boot核心架构解析:本文介绍了Spring Boot项目的标准目录结构与环境配置要点,重点解析了四大核心组件:1)Entity层通过JPA注解实现对象关系映射;2)Repository层提供数据访问抽象;3)Service层封装业务逻辑;4)Controller层处理HTTP请求。内容涵盖设计理念、核心注解、代码示例及最佳实践,如事务管理、分层设计、DTO转换等,并提供了application.properties配置示例。开发时需注意环境依赖、接口隔离、业务分层等关键点,确保项目可维护性和

2301_79970509的博客 812

文档:亲测可用解决vnc灰屏,黑屏,鼠标大黑叉情况

主要解决vnc-viewer灰屏,黑屏,鼠标大黑叉等现象,详细见文档。

IDEA的使用指南

IDEA基于java的使用手册,可以最快速的上手,在使用中不断的丰富自己的使用技巧.

Spring Boot是如何跑起来的

不得不说 SpringBoot 太杂乱了,我原本只想研讨一下 SpringBoot 最简略的 HelloWorld 程序是如何从 main 办法一步一步跑起来的,可是这却是一个适当深的坑。你可以试着沿着调用栈代码一层一层的深入进去,假如你不打断点,你根本不知道接下来程序会往哪里活动。这个不同于我研讨过去的 Go 言语、Python 言语结构,它们一般都十分直接了当,规划上清晰易懂,代码写起来简略,...

zxl2016的博客 660

visio安装包

Microsoft Office Visio 是一款专业的制图软件。利用经过更新的形状、协作工具和数据链接图表,创建专业级图表以简化复杂的信息。

Spring Boot(3) SpringBoot是怎么跑起来的(1)

1、今天稍微学习了一下Spring Boot的运行机制,在这里把我收获到的知识分享出来,是对Spring Boot运行机制的一点点个人的看法。 2、大家看到SpringBoot项目的时候最大的疑惑应该是为什么本来SSM里的那些xml配置文件都不见了呢,其实Spring Boot是对现在许多主流的第三方框架进行了整合,我们可以在一个空项目的pom文件中看到下边这样的配置,其实仔细研究一下就可以看到...

weixin_39452731的博客 321

蓝牙模块HM-10手册

蓝牙模块HM-10手册,该手册对蓝牙模块HM-10进行了详细的介绍和使用方法以及一些AT指令。

springboot 扫描jar包中bean_Spring Boot这只怪物到底是如何跑起来的?

作者:钱文品 |《Redis 深度历险:核心原理与应用实践》作者微信公众号:码洞(ID:codehole)不得不说 SpringBoot 太复杂了,我本来只想研究一下 SpringBoot 最简单的 HelloWorld 程序是如何从 main 方法一步一步跑起来的,但是这却是一个相当深的坑。你可以试着沿着调用栈代码一层一层的深入进去,如果你不打断点,你根本不知道接下来程序会往哪里流动。...

weixin_39817391的博客 389

springboot是如何跑起来的01

1springboot根据类型进行deduce判断,类型是写死的servlet或者reactive 细节 :根据反射拿classname,用指定的classloader从classpath 依赖的Jar包中加载类 扫描各个jar包下的meta-inf下面的spring.factorys 2setInitializers 在spring容器初始化的时候 扫描所有Jar包的spring.factorires得到propertys的配置然后暂存到map里 这步很重要,跑完后会得到一堆上下文类,...

piaoweimei的博客 660

Spring Boot这只怪物到底是如何跑起来的?

作者:钱文品 |《Redis 深度历险:核心原理与应用实践》作者微信公众号:码洞(ID:codehole)不得不说 SpringBoot 太复杂了,我本来只想研究一下 S...

Java后端技术 442
上一篇: LevelDB 入门 —— 全面了解 LevelDB 的功能特性
下一篇: BAT 经典算法笔试题: 镜像二叉树
码洞
博客等级 码龄10年 41粉丝 137原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值