【JavaWeb】Tlias智能学习辅助系统|后端Web进阶(SpringBoot原理与Maven高级)

目录

第一部分:SpringBoot 原理篇

1. 配置优先级

五种配置方式优先级(从高到低)

2. Bean 管理

2.1 Bean 的作用域

2.2 管理第三方 Bean

3. SpringBoot 自动配置原理(核心)

起步依赖

自动配置

3.1 为什么引入依赖后 Bean 不能直接用?

3.2 解决方案一:@ComponentScan组件扫描(不推荐)

3.3 解决方案二:@Import

3.4 源码追踪(核心流程)

3.5 @Conditional 条件装配

4. 自定义 Starter(实战)

需求

规范

实现步骤

第二部分:Maven 高级

1. 分模块设计与开发

1.1 为什么需要分模块?

1.2 分模块策略

1.3 实践

2. 继承与聚合

2.1 继承(解决依赖重复配置)

2.2 版本锁定(dependencyManagement)

2.3 聚合(一键构建)

继承与聚合对比

3. 私服(Nexus)

3.1 为什么需要私服?

3.2 私服介绍

3.3 资源上传与下载

步骤分析

具体操作

总结


在前十几天中,我们一直在学习 Web 开发的应用层面——会配置、会开发、会调试。但一个合格的后端开发者,不能只停留在“会用”,更要懂“为什么这样用”。

今天,我们就来深入 SpringBoot 的底层原理,同时补齐 Maven 高级功能(分模块设计、继承聚合、私服)。这两块知识,是让你从“码农”走向“架构师”的第一步。


第一部分:SpringBoot 原理篇

1. 配置优先级

SpringBoot 支持三种配置文件:

  1. application.properties

  2. application.yml

  3. application.yaml

如果三者同时存在,并都配置了同一个属性(如 server.port),优先级从高到低为:

application.properties  >  application.yml  >  application.yaml

注意事项:虽然springboot支持多种格式配置文件,但是在项目开发时,推荐统一使用一种格式的配置。(yml是主流)

除了配置文件,SpringBoot 还支持两种外部配置

  1. Java 系统属性-Dserver.port=9000

  2. 命令行参数--server.port=10010

五种配置方式优先级(从高到低)
命令行参数  >  系统属性  >  properties  >  yml  >  yaml

实际场景:项目打包后,启动时动态指定端口:

java -Dserver.port=9000 -jar app.jar --server.port=10010

最终生效的是 10010

注意事项:

  • Springboot项目进行打包时,需要引入插件 spring-boot-maven-plugin (基于官网骨架创建项目,会自动添加该插件)


2. Bean 管理

2.1 Bean 的作用域

Spring 容器中的 Bean 默认是 单例(singleton) 的。

作用域说明
singleton(默认)容器中只有一个实例,容器启动时创建
prototype每次获取都创建新实例
request(web)每次 HTTP 请求创建一个实例
session(web)每个 HTTP Session 创建一个实例
application(web)每个 ServletContext 创建一个实例

知道了bean的5种作用域了,我们要怎么去设置一个bean的作用域呢?

  • 可以借助Spring中的@Scope注解来进行配置作用域

1). 测试一

  • 控制器

//默认bean的作用域为:singleton (单例)
@RestController
@RequestMapping("/depts")
public class DeptController {

    @Autowired
    private DeptService deptService;

    public DeptController(){
        System.out.println("DeptController constructor ....");
    }

    //省略其他代码...
}
  • 测试类

@SpringBootTest
class SpringbootWebConfig2ApplicationTests {

    @Autowired
    private ApplicationContext applicationContext; //IOC容器对象

    //bean的作用域
    @Test
    public void testScope(){
        for (int i = 0; i < 10; i++) {
            DeptController deptController = applicationContext.getBean(DeptController.class);
            System.out.println(deptController);
        }
    }
}

重启SpringBoot服务,运行测试方法,查看控制台打印的日志:

注意singleton 的 Bean 默认在容器启动时创建,可用 @Lazy 延迟到第一次使用时创建。

2). 测试二

修改控制器DeptController代码:

@Scope("prototype") //bean作用域为非单例
@RestController
@RequestMapping("/depts")
public class DeptController {

    @Autowired
    private DeptService deptService;

    public DeptController(){
        System.out.println("DeptController constructor ....");
    }

    //省略其他代码...
}

重启SpringBoot服务,再次执行测试方法,查看控制吧打印的日志:

注意事项:

  • prototype的bean,每一次使用该bean的时候都会创建一个新的实例

  • 实际开发当中,绝大部分的Bean是单例的,也就是说绝大部分Bean不需要配置scope属性

总结:

  • 默认singleton的bean,在容器启动时被创建,可以使用@Lazy注解来延迟初始化(延迟到第一次使用时)

  • prototype的bean,每一次使用该bean的时候都会创建一个新的实例。

  • 实际开发当中,绝大部分的bean是单例的,也就是说绝大部分bean不需要配置scope属性。

2.2 管理第三方 Bean

当我们要引入第三方依赖中的类(如阿里云 OSS 工具类)时,无法使用 @Component 注解。此时要用 @Bean 显式声明。

方式一:在启动类中直接声明

@ServletComponentScan
@EnableScheduling
@SpringBootApplication
public class TliasWebManagementApplication {

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

    @Bean
    public AliyunOSSOperator aliyunOSSOperator(AliyunOSSProperties ossProperties) {
        return new AliyunOSSOperator(ossProperties);
    }
}

方式二:使用配置类(推荐)

@Configuration
public class OSSConfig {
    @Bean
    public AliyunOSSOperator aliyunOSSOperator(AliyunOSSProperties ossProperties) {
        return new AliyunOSSOperator(ossProperties);
    }
}

推荐做法:将第三方 Bean 统一放在 @Configuration 类中,便于维护。

  • 通过@Bean注解的name 或 value属性可以声明bean的名称,如果不指定,默认bean的名称就是方法名。

  • 如果第三方bean需要依赖其他bean对象,直接在bean定义方法中设置形参即可,容器会根据类型自动装配。


3. SpringBoot 自动配置原理(核心)

起步依赖

假如我们没有使用SpringBoot,用的是Spring框架进行web程序的开发,此时我们就需要引入web程序开发所需要的一些依赖。

当我们引入了 spring-boot-starter-web 之后,maven会通过依赖传递特性,将web开发所需的常见依赖都传递下来。

所以,起步依赖的原理就是Maven的依赖传递。

自动配置

SpringBoot的自动配置就是当spring容器启动后,一些配置类、bean对象就自动存入到了IOC容器中,不需要我们手动去声明,从而简化了开发,省去了繁琐的配置操作。

3.1 为什么引入依赖后 Bean 不能直接用?

SpringBoot 的包扫描默认只扫描启动类所在包及其子包。第三方依赖的包(如 com.example)不会被扫描到,所以其 @Component 失效。

3.2 解决方案一:@ComponentScan组件扫描(不推荐)
@SpringBootApplication
@ComponentScan({"com.itheima","com.example"}) //指定要扫描的包
public class SpringbootWebConfigApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringbootWebConfigApplication.class, args);
    }
}

缺点:

  1. 使用繁琐

  2. 性能低

3.3 解决方案二:@Import

@Import 可以手动导入指定类,让 Spring 将其加载到 IOC 容器。

四种导入方式:

方式示例
导入普通类@Import(TokenParser.class)
导入配置类@Import(HeaderConfig.class)
导入 ImportSelector 实现类@Import(MyImportSelector.class)
使用 @EnableXxx 注解(封装了 @Import)@EnableHeaderConfig

SpringBoot 正是采用 第 4 种方式,通过 @EnableAutoConfiguration 注解实现自动配置。

1). 使用@Import导入普通类

@Import(TokenParser.class) //导入的类会被Spring加载到IOC容器中
@SpringBootApplication
public class SpringbootWebConfigApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringbootWebConfigApplication.class, args);
    }
}

2). 使用@Import导入配置类:

  • 配置类

@Configuration
public class HeaderConfig {
    @Bean
    public HeaderParser headerParser(){
        return new HeaderParser();
    }

    @Bean
    public HeaderGenerator headerGenerator(){
        return new HeaderGenerator();
    }
}
  • 启动类

    @Import(HeaderConfig.class) //导入配置类
    @SpringBootApplication
    public class SpringbootWebConfig2Application {
        public static void main(String[] args) {
            SpringApplication.run(SpringbootWebConfig2Application.class, args);
        }
    }
  • 测试类

@SpringBootTest
public class AutoConfigurationTests {
    @Autowired
    private ApplicationContext applicationContext;

    @Test
    public void testHeaderParser(){
        System.out.println(applicationContext.getBean(HeaderParser.class));
    }

    @Test
    public void testHeaderGenerator(){
        System.out.println(applicationContext.getBean(HeaderGenerator.class));
    }
    
    //省略其他代码...
}

3). 使用@Import导入ImportSelector接口实现类:

  • ImportSelector接口实现类

public class MyImportSelector implements ImportSelector {
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        //返回值字符串数组(数组中封装了全限定名称的类)
        return new String[]{"com.example.HeaderConfig"};
    }
}
  • 启动类

@Import(MyImportSelector.class) //导入ImportSelector接口实现类
@SpringBootApplication
public class SpringbootWebConfig2Application {
    public static void main(String[] args) {
        SpringApplication.run(SpringbootWebConfig2Application.class, args);
    }
}

结论:我们不用自己指定要导入哪些bean对象和配置类了,让第三方依赖它自己来指定。

4). 使用第三方依赖提供的 @EnableXxxxx注解

  • 第三方依赖中提供的注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import(MyImportSelector.class)//指定要导入哪些bean对象或配置类
public @interface EnableHeaderConfig { 
}
  • 在使用时只需在启动类上加上@EnableXxxxx注解即可

@EnableHeaderConfig  //使用第三方依赖提供的Enable开头的注解
@SpringBootApplication
public class SpringbootWebConfig2Application {
    public static void main(String[] args) {
        SpringApplication.run(SpringbootWebConfig2Application.class, args);
    }
}
3.4 源码追踪(核心流程)

入口:@SpringBootApplication → 包含三个核心注解:

  • @SpringBootConfiguration:表明是配置类

  • @ComponentScan:默认扫描当前包及子包

  • @EnableAutoConfiguration:自动配置的核心

@EnableAutoConfiguration 内部通过 @Import(AutoConfigurationImportSelector.class) 导入了一个 ImportSelector

该 Selector 的 selectImports() 方法会读取以下文件中的配置类全类名:

这些配置类中定义了大量的 @Bean,但并非全部生效——它们通常带有 @Conditional 条件注解,只有满足条件才会注册 Bean。

3.5 @Conditional 条件装配

@Conditional注解:

  • 作用:按照一定的条件进行判断,在满足给定条件后才会注册对应的bean对象到Spring的IOC容器中。

  • 位置:方法、类

  • @Conditional本身是一个父注解,派生出大量的子注解:

    • @ConditionalOnClass:判断环境中有对应字节码文件,才注册bean到IOC容器。

    • @ConditionalOnMissingBean:判断环境中没有对应的bean(类型或名称),才注册bean到IOC容器。

    • @ConditionalOnProperty:判断配置文件中有对应属性和值,才注册bean到IOC容器。

注解作用
@ConditionalOnClass类路径下存在指定类时才生效
@ConditionalOnMissingBean容器中不存在指定 Bean 时才生效
@ConditionalOnProperty配置文件中存在指定属性值时才生效

例如 Gson 自动配置:

只有当项目中引入了 Gson 依赖,该 Bean 才会被创建。


4. 自定义 Starter(实战)

很多第三方技术没有官方的 SpringBoot Starter,我们可以自己封装一个,供团队复用。

需求

将阿里云 OSS 工具类封装成 aliyun-oss-spring-boot-starter,引入后直接注入 AliyunOSSOperator 即可使用。

规范
  • 官方 Starter 命名:spring-boot-starter-xxx

  • 第三方 Starter 命名:xxx-spring-boot-starter

通常包含两个模块:

  • xxx-spring-boot-starter:仅做依赖管理

  • xxx-spring-boot-autoconfigure:自动配置核心代码

将来在项目当中进行相关功能开发时,只需要引入一个起步依赖就可以了,因为它会将autoconfigure自动配置的依赖给传递下来。

实现步骤

1. 创建 autoconfigure 模块

  • 引入所需依赖(OSS SDK、Lombok、SpringBoot 等)

  • 编写配置属性类 AliyunOSSProperties

  • 编写工具类 AliyunOSSOperator(去掉 @Component

  • 编写自动配置类:

@Configuration
@EnableConfigurationProperties(AliyunOSSProperties.class)
public class AliyunOSSAutoConfiguration {
    @Bean
    public AliyunOSSOperator aliyunOSSOperator(AliyunOSSProperties props) {
        return new AliyunOSSOperator(props);
    }
}
  • 在 resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 中写入:

com.aliyun.oss.AliyunOSSAutoConfiguration

2. 创建 starter 模块

  • 仅保留 pom.xml

  • 依赖 autoconfigure 模块

3. 测试

在测试项目中引入 starter 依赖,配置 aliyun.oss.endpoint 和 bucketName,直接注入 AliyunOSSOperator 即可上传文件。

从此,团队中任何项目要使用阿里云 OSS,只需引入 starter 并配置必要参数,无需重复编写工具类。


第二部分:Maven 高级

1. 分模块设计与开发

1.1 为什么需要分模块?

一个大项目全部写在一个模块中会导致:

  1. 代码臃肿,难以维护

  2. 多人协作冲突频繁

  3. 通用组件无法复用

1.2 分模块策略

通常按功能模块拆分,例如:

  • tlias-pojo:实体类

  • tlias-utils:工具类

  • tlias-web-management:主业务

分模块设计就是将项目按照功能/结构拆分成若干个子模块,方便项目的管理维护、拓展,也方便模块键的相互调用、资源共享。

也可以按层次拆分(controller / service / mapper)。

1.3 实践

将原有项目中的 pojo 和 utils 分别抽取为独立 Maven 模块,然后在主模块中通过 <dependency> 引入它们。

<dependency>
    <groupId>com.itheima</groupId>
    <artifactId>tlias-pojo</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

tlias-pojo 模块的pom.xml文件中引入依赖

<dependencies>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.34</version>
    </dependency>
    
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
        <version>3.2.8</version>
    </dependency>
</dependencies>

这样其他项目组也可以直接复用这些通用模块。

注意:分模块开发需要先针对模块功能进行设计,再进行编码。不会先将工程开发完毕,然后进行拆分。

总结:

  • 什么是分模块设计:将项目按照功能拆分成若干个子模块

  • 为什么要分模块设计:方便项目的管理维护、扩展,也方便模块间的相互调用,资源共享

  • 注意事项:分模块设计需要先针对模块功能进行设计,再进行编码。不会先将工程开发完毕,然后进行拆分


2. 继承与聚合

2.1 继承(解决依赖重复配置)

多个子模块如果都需要相同的依赖(如 Lombok),可以在父工程中统一配置,子工程继承即可。

父工程(tlias-parent) 打包方式为 pom

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.8</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>com.itheima</groupId>
    <artifactId>tlias-parent</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>pom</packaging>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

</project>

Maven打包方式:

  • jar:普通模块打包,springboot项目基本都是jar包(内嵌tomcat运行)

  • war:普通web程序打包,需要部署在外部的tomcat服务器中运行

  • pom:父工程或聚合工程,该模块不写代码,仅进行依赖管理

子工程 继承父工程:

<parent>
    <groupId>com.itheima</groupId>
    <artifactId>tlias-parent</artifactId>
    <version>1.0-SNAPSHOT</version>
    <relativePath>../tlias-parent/pom.xml</relativePath>
</parent>

<artifactId>tlias-utils</artifactId>
<version>1.0-SNAPSHOT</version>

父工程中 <dependencies> 里的依赖,子工程会自动继承。

注意:

  • 在子工程中,配置了继承关系之后,坐标中的groupId是可以省略的,因为会自动继承父工程的 。

  • relativePath指定父工程的pom文件的相对位置(如果不指定,将从本地仓库/远程仓库查找该工程)。

    • ../ 代表的上一级目录

2.2 版本锁定(dependencyManagement)

如果某些依赖只在部分子模块中使用,但希望统一版本,可以在父工程中使用 <dependencyManagement>

<!--统一管理依赖版本-->
<dependencyManagement>
    <dependencies>
        <!--JWT令牌-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
    </dependencies>
</dependencyManagement>

子模块使用时无需指定 version

<dependencies>
    <!--JWT令牌-->
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
    </dependency>
</dependencies>

区别<dependencies> 会直接继承,<dependencyManagement> 只管理版本,不强制引入。

还可以通过 自定义属性 集中管理版本号:

<properties>
    <jwt.version>0.9.1</jwt.version>
</properties>
...
<version>${jwt.version}</version>
2.3 聚合(一键构建)

分模块后,要打包整个项目需要分别对每个模块执行 install,非常繁琐。

  • 聚合:将多个模块组织成一个整体,同时进行项目的构建。

  • 聚合工程:一个不具有业务功能的“空”工程(有且仅有一个pom文件) 【PS:一般来说,继承关系中的父工程与聚合关系中的聚合工程是同一个】

  • 作用:快速构建项目(无需根据依赖关系手动构建,直接在聚合工程上构建即可)

在父工程 pom.xml 中配置:

<!-- 聚合其他模块 -->
<modules>
    <module>../tlias-pojo</module>
    <module>../tlias-utils</module>
    <module>../tlias-web-management</module>
</modules>

之后在父工程执行 mvn clean package,所有子模块都会一起构建。

聚合与继承通常放在同一个父工程中,既管理依赖,又管理构建。

继承与聚合对比
  • 作用

    • 聚合用于快速构建项目

    • 继承用于简化依赖配置、统一管理依赖

  • 相同点:

    • 聚合与继承的pom.xml文件打包方式均为pom,通常将两种关系制作到同一个pom文件中

    • 聚合与继承均属于设计型模块,并无实际的模块内容

  • 不同点:

    • 聚合是在聚合工程中配置关系,聚合可以感知到参与聚合的模块有哪些

    • 继承是在子模块中配置关系,父模块无法感知哪些子模块继承了自己


3. 私服(Nexus)

3.1 为什么需要私服?

团队内部开发的通用模块(如 tlias-utils)无法上传到中央仓库,也无法从其他同事的本地仓库获取。此时需要在公司内部搭建一个 私服,作为团队共享的仓库。

3.2 私服介绍
  • 私服:是一种特殊的远程仓库,它是架设在局域网内的仓库服务,用来代理位于外部的中央仓库,用于解决团队内部的资源共享与资源同步问题。

  • 依赖查找顺序:

    • 本地仓库

    • 私服仓库

    • 中央仓库

  • 注意事项:私服在企业项目开发中,一个项目/公司,只需要一台即可(无需我们自己搭建,会使用即可)。

3.3 资源上传与下载
步骤分析

资源上传与下载,我们需要做三步配置,执行一条指令。

  • 第一步配置:在maven的配置文件中配置访问私服的用户名、密码。

  • 第二步配置:在maven的配置文件中配置连接私服的地址(url地址)。

  • 第三步配置:在项目的pom.xml文件中配置上传资源的位置(url地址)。

  • 配置好了上述三步之后,要上传资源到私服仓库,就执行执行maven生命周期:deploy

私服仓库说明:

  • RELEASE:存储自己开发的RELEASE发布版本的资源。

  • SNAPSHOT:存储自己开发的SNAPSHOT发布版本的资源。

  • Central:存储的是从中央仓库下载下来的依赖。

项目版本说明:

  • RELEASE(发布版本):功能趋于稳定、当前更新停止,可以用于发行的版本,存储在私服中的RELEASE仓库中。

  • SNAPSHOT(快照版本):功能不稳定、尚处于开发中的版本,即快照版本,存储在私服的SNAPSHOT仓库中。

具体操作

私服已经搭建好了,已经启动起来了,我们可以访问私服测试:http://localhost:8081

私服准备好了之后,我们要做如下几步配置:

1. 设置私服的访问用户名/密码(在自己maven安装目录下的conf/settings.xml中的servers中配置)

<server>
    <id>maven-releases</id>
    <username>admin</username>
    <password>admin</password>
</server>
    
<server>
    <id>maven-snapshots</id>
    <username>admin</username>
    <password>admin</password>
</server>

2. 设置私服依赖下载的仓库组地址(在自己maven安装目录下的conf/settings.xml中的mirrors中配置)

<mirror>
    <id>maven-public</id>
    <mirrorOf>*</mirrorOf>
    <url>http://localhost:8081/repository/maven-public/</url>
</mirror>

3. 设置私服依赖下载的仓库组地址(在自己maven安装目录下的conf/settings.xml中的profiles中配置)

<profile>
    <id>allow-snapshots</id>
    <activation>
        <activeByDefault>true</activeByDefault>
    </activation>
    <repositories>
        <repository>
            <id>maven-public</id>
            <url>http://localhost:8081/repository/maven-public/</url>
            <releases>
                <enabled>true</enabled>
            </releases>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
        </repository>
    </repositories>
</profile>

注意:原阿里云服务器关掉

4. IDEA的maven工程的pom文件中配置上传(发布)地址(直接在tlias-parent中配置发布地址)

<distributionManagement>
    <!-- release版本的发布地址 -->
    <repository>
        <id>maven-releases</id>
        <url>http://localhost:8081/repository/maven-releases/</url>
    </repository>
    <!-- snapshot版本的发布地址 -->
    <snapshotRepository>
        <id>maven-snapshots</id>
        <url>http://localhost:8081/repository/maven-snapshots/</url>
    </snapshotRepository>
</distributionManagement>

配置完成之后,我们就可以在tlias-parent中执行deploy生命周期,将项目发布到私服仓库中。

通过日志,我们可以看到,这几个模块打的jar包确实已经上传到了私服仓库中(由于当前我们的项目是SNAPSHOT版本,所以jar包是上传到了snapshot仓库中)。

那接下来,我们再来打开私服来看一下:

我们看到,我们项目中的这几个模块,在私服中都有了。 那接下来,当其他项目组的开发人员在项目中,就可以直接通过依赖的坐标,就可以完成引入对应的依赖,此时本地仓库没有,就会自动从私服仓库中下载。

备注说明:在真实的企业开发中,私服都是在远程服务器中的,并不是在本地的。这里我们只是为了方便演示。


总结

模块核心知识点
配置优先级命令行 > 系统属性 > properties > yml > yaml
Bean 管理作用域(singleton/prototype)、第三方 Bean 用 @Bean
自动配置原理@EnableAutoConfiguration → ImportSelector → 读取 .imports 文件 → @Conditional 按需加载
自定义 Starter两个模块(starter + autoconfigure),配置 .imports 文件
分模块设计按功能拆分,便于维护和复用
继承与聚合父工程管理依赖版本,聚合实现一键构建
私服团队内部资源共享,nexus 搭建,deploy 上传
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值