SpringBoot 之自动装配原理

SpringBoot 自动装配了什么东西

自动装配了一系列被 @Configration 修饰的自动配置类的 Bean 定义到BeanDefinitionMap中,这时还未实例化。

SpringBoot 面试自动装配原理

通过@EnableAutoConfiguration导入AutoConfigurationImportSelector类。
AutoConfigurationImportSelector中读取META-INF/spring.factories配置文件中的所有自动配置类的类全路径名,然后批量导入。都是以xxxAutoConfiguration结尾,每个配置都会配上xxxProperties作为配置属性。
spring.factories配置了上百个配置类,现实中不可能会全部引入。
所以每个配置类,都是按条件注解 ConditionalOnClass 进行自动注入的。
被加载到容器中的自动配置类中都装载了需要的 Bean

SpringBoot 的配置文件

Spring Boot有一个全局配置文件:application.propertiesapplication.yml

各种属性都可以在这个文件中进行配置,最常配置的比如:server.portlogging.level.* 等等,实际用到的往往只是很少的一部分,这些属性都可以在官方文档中查找到:
https://docs.spring.io/spring-boot/docs/2.1.0.RELEASE/reference/htmlsingle/#common-application-properties

SpringBoot 自动装配原理

Starter 组件: 提供开箱即用的组件,也叫场景启动器。
什么叫自动装配:自动将 Bean 装配到 IoC 容器中。

SpringBoot 关于自动装配的源码在spring-boot-autoconfigure-x.x.x.x.jar中。

SpringBoot的启动类上有一个@SpringBootApplication注解,这个注解是SpringBoot项目必不可少的注解。
自动装配原理就是这个注解来实现的。

SpringBootApplication注解

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
		@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM,
				classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
  • @SpringBootConfiguration:SpringBoot的配置类,标注在某个类上,表示这是一个Spring Boot的配置类。
  • @EnableAutoConfigurationSpringBoot的精华所在,开启自动配置类。
  • @ComponentScan:包扫描

@EnableAutoConfiguration注解解剖

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
  • @AutoConfigurationPackage:自动配置包,作用是把使用了该注解的类所在的包及子包下所有的组件扫描到IoC容器中。
  • @Import:给Spring容器中来导入一些组件,并且由AutoConfigurationImportSelector类来导入。

AutoConfigurationImportSelector类解析:
AutoConfigurationImportSelector 实现了 ImportSelector ,它只有一个 selectImports 抽象方法,并且返回一个 String 数组,这个数组中指定了需要装配到IoC容器的类名称。
@Import 中导入了一个 ImportSelector的实现类之后,会把该实现类中返回的Class名称都装载到IoC容器中。

@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
	if (!isEnabled(annotationMetadata)) {
		return NO_IMPORTS;
	}
	// 从 META-INF/spring-autoconfigure-metadata.properties 中加载自动装配的条件元数据,就是只有满足条件的 Bean 才能进行装配
	AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
			.loadMetadata(this.beanClassLoader);
	AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(
			autoConfigurationMetadata, annotationMetadata);
	// 收集所有符合条件的配置类,完成自动装配。
	return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}

重点分析配置类收集方法: getAutoConfigurationEntry

protected AutoConfigurationEntry getAutoConfigurationEntry(
		AutoConfigurationMetadata autoConfigurationMetadata,
		AnnotationMetadata annotationMetadata) {
	if (!isEnabled(annotationMetadata)) {
		return EMPTY_ENTRY;
	}
	// 获取 @EnableAutoConfiguration 注解中的属性 exclude、excludeName等。
	AnnotationAttributes attributes = getAttributes(annotationMetadata);
	// 获取所有自动装配的配置类
	List<String> configurations = getCandidateConfigurations(annotationMetadata,
			attributes);
	// 去除重复的配置项
	configurations = removeDuplicates(configurations);
	// 根据 @EnableAutoConfiguration 注解的 exclude 属性,把不需要自动装配的类移除
	Set<String> exclusions = getExclusions(annotationMetadata, attributes);
	checkExcludedClasses(configurations, exclusions);
	configurations.removeAll(exclusions);
	configurations = filter(configurations, autoConfigurationMetadata);
	fireAutoConfigurationImportEvents(configurations, exclusions);
	return new AutoConfigurationEntry(configurations, exclusions);
}

在这里插入图片描述
注意:AutoConfigurationImportSelector 中不执行 selectImports 方法,而是通过 ConfigurationClassPostProcessor 中的 processConfigBeanDefinitions 方法来扫描和注册所有配置类的 Bean, 最终还是会调用 getAutoConfigurationEntry 方法获得所有需要自动装配的配置类。

分析 getCandidateConfigurations 方法:

protected List<String> getCandidateConfigurations(AnnotationMetadata metadata,
		AnnotationAttributes attributes) {
	List<String> configurations = SpringFactoriesLoader.loadFactoryNames(
			getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader());
	Assert.notEmpty(configurations,
			"No auto configuration classes found in META-INF/spring.factories. If you "
					+ "are using a custom packaging, make sure that file is correct.");
	return configurations;
}

在这里插入图片描述
SpringFactoriesLoader 扫描 classpath 下的META-INF/spring.factories文件。
spring-boot-autoconfigure-x.x.x.x.jar里就有一个这样的spring.factories文件。
在这里插入图片描述
这个spring.factories文件是key=value的形式,其中一个key是EnableAutoConfiguration类的全类名,而它的value是一个xxxxAutoConfiguration的类名的列表,这些类名以逗号分隔,每一个xxxAutoConfiguration类都是容器中的一个组件,并都加入到容器中。如下图所示:
在这里插入图片描述
总结:
这个@EnableAutoConfiguration注解通过@SpringBootApplication被间接的标记在了SpringBoot的启动类上。在SpringApplication.run(...)的内部就会执行selectImports()方法,找到所有JavaConfig自动配置类的全限定名对应的class,然后将所有自动配置类加载到Spring容器中。

自动配置生效

每一个xxxxAutoConfiguration自动配置类都是在某些条件之下才会生效的,这些条件的限制在SpringBoot中以注解的形式体现,常见的条件注解有如下几项:

  • @ConditionalOnBean:当容器里有指定的bean的条件下。
  • @ConditionalOnMissingBean:当容器里不存在指定bean的条件下。
  • @ConditionalOnClass:当类路径下有指定类的条件下。
  • @ConditionalOnMissingClass:当类路径下不存在指定类的条件下。
  • @ConditionalOnProperty:指定的属性是否有指定的值,比如@ConditionalOnProperties(prefix=”xxx.xxx”, value=”enable”, matchIfMissing=true),代表当xxx.xxx为enable时条件的布尔值为true,如果没有设置的情况下也为true。

ServletWebServerFactoryAutoConfiguration配置类为例,解释一下全局配置文件中的属性如何生效,比如:server.port=8081,是如何生效的。

ServletWebServerFactoryAutoConfiguration类上,有一个@EnableConfigurationProperties注解:开启配置属性,而它后面的参数是一个ServerProperties类,这就是习惯优于配置的最终落地点。
在这里插入图片描述
在这个类上有一个@ConfigurationProperties注解,它的作用就是从配置文件中绑定属性到对应的bean上,而@EnableConfigurationProperties负责将bean加载到spring容器中。那么这个类相关的属性都可以在全局配置文件中定义,也就是说,真正“限制”我们可以在全局配置文件中配置哪些属性的类就是这些XxxxProperties类,它与配置文件中定义的prefix关键字开头的一组属性是唯一对应的。
在这里插入图片描述
总结:
全局配置的属性如:server.port等,通过@ConfigurationProperties注解,绑定到对应的XxxxProperties配置实体类上封装为一个bean,然后再通过@EnableConfigurationProperties注解导入到Spring容器中。

而其他的XxxxAutoConfiguration自动配置类,就是Spring容器的JavaConfig形式,作用就是为Spring容器导入bean,而所有导入的bean所需要的属性都通过xxxxPropertiesbean来获得。

再举一个例子:
RedisAutoConfiguration会生成RedisTemplate对象并加载到了Spring容器,程序中就可以注入这个对象了。
在这里插入图片描述

RestTemplateAutoConfiguration 源码解析

@Configuration
@ConditionalOnClass({ RabbitTemplate.class, Channel.class })
@EnableConfigurationProperties(RabbitProperties.class)
@Import(RabbitAnnotationDrivenConfiguration.class)
public class RabbitAutoConfiguration {

除了基本的 @Configuration 注解,还有 @ConditionalOnClass 注解。
@ConditionalOnClass 注解意思是判断 calsspath 下是否存在 RabbitTemplateChannel 类,如果是,把当前配置类注册到 IoC 容器。
@EnableConfigurationProperties 属性配置,按照约定在 application.properties 中配置 RabbitMQ 参数,这些配置会加载到 RabbitProperties 中。

自动配置原理总结

SpringBoot启动的时候会通过@EnableAutoConfiguration注解找到META-INF/spring.factories配置文件中的所有自动配置类,并对其进行加载,而这些自动配置类都是以AutoConfiguration结尾来命名的,它实际上就是一个JavaConfig形式的Spring容器配置类,它能通过以Properties结尾命名的类中取得在全局配置文件中配置的属性如:server.port,而XxxxProperties类是通过@ConfigurationProperties注解与全局配置文件中对应的属性进行绑定的。

XxxxProperties类的含义是:封装配置文件中相关属性;
XxxxAutoConfiguration类的含义是:自动配置类,目的是给容器中添加组件。

  • @EnableAutoConfiguration 开启自动配置类。
  • 通过 @Import(AutoConfigurationImportSelector) 实现配置类的导入。
  • AutoConfigurationImportSelector 类实现了 ImportSelector 接口,重写了方法 selectImports,用于批量配置类的装配。
  • 扫描 classpath 路径下的 META-INF/spring.factories 文件,读取需要实现自动装配的配置类。

自定义starter

SpringBoot为我们提供了自动化装配的功能,简单方便。可以像使用插件一样,对各个组件自由组合装配。只需引入定义好的 starter 即可。starter方式实现了模块化完全解耦,实现热插拔功能。

自定义一个自动化装配的实现,自定义starter。

第一步:
定义一个配置类模块:

import com.demo.entity.SimpleBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@ConditionalOnProperty(name = "enabled.autoConfituration", matchIfMissing = true)
public class MyAutoConfiguration {
    static {
        System.out.println("myAutoConfiguration init...");
    }

    @Bean
    public SimpleBean simpleBean(){
        return new SimpleBean();
    }
}

第二步:
新建一个项目作为starter模块,里面无需任何代码,pom也无需任何依赖,只需在META-INF下面建一个 spring.factories文件,添加如下配置:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
spring.study.startup.bean.MyAutoConfiguration

在这里插入图片描述
第三歩:
在启动类项目中引入我们的 starter 模块即可。

<dependency>
    <groupId>com.demo</groupId>
    <artifactId>springboot-starter</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

最终在AutoConfigurationImportSelector解析spring.factories文件:
在这里插入图片描述
SpringBoot提供了配置类有180多个,现实中不可能会全部引入。所以在自动装配的时候,会去ClassPath下面寻找,是否有对应的配置类。如果有配置类,则按条件注解 @Conditional或者@ConditionalOnProperty等相关注解进行判断,决定是否需要装配。如果ClassPath下面没有对应的字节码,则不进行任何处理。

上面自定义的配置类也是以相同的逻辑进行装配,指定了以下注解:

@ConditionalOnProperty(name = "enabled.autoConfituration", matchIfMissing = true)

默认为 true,所以自定义的starter成功执行。

自定义starter案例2

Starter 的核心就是条件注解 @Conditional ,当 classpath 下存在某一个 Class 时,某个配置才会生效。
1、创建一个普通的Maven项目,并导入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-autoconfigure</artifactId>
    <version>2.1.3.RELEASE</version>
</dependency>

2、创建一个 HelloProperties 类,用来接受 application.properties 中注入的值,并提供默认值

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "hello")
public class HelloProperties {

    private static final String DEFAULT_NAME = "James";
    private static final String DEFAULT_MSG = "hello world";
    private String name = DEFAULT_NAME;
    private String msg = DEFAULT_MSG;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getMsg() {
        return msg;
    }
    public void setMsg(String msg) {
        this.msg = msg;
    }
}

@ConfigurationProperties 表示类型安全的属性注入,即将 application.properties 文件中前缀为 hello 的属性注入到这个类对应的属性上。
application.properties 中的配置文件如下:

hello.name=zhangsan
hello.msg=java

3、创建一个 HelloService 类

public class HelloService {
    private String msg;
    private String name;
    public String sayHello() {
        return name + " say " + msg + " !";
    }
    public String getMsg() {
        return msg;
    }
    public void setMsg(String msg) {
        this.msg = msg;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}

4、创建一个自动配置类

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableConfigurationProperties(HelloProperties.class)
@ConditionalOnClass(HelloService.class)
public class HelloServiceAutoConfiguration {

    @Autowired
    HelloProperties helloProperties;

    @Bean
    HelloService helloService() {
        HelloService helloService = new HelloService();
        helloService.setName(helloProperties.getName());
        helloService.setMsg(helloProperties.getMsg());
        return helloService;
    }
}

自动配置类中首先注入 HelloProperties ,这个实例中含有我们在 application.properties 中配置的相关数据。
提供一个 HelloService 的实例,将 HelloProperties 中的值注入进去。

注解解析:

  • @Configuration 注解表明这是一个配置类。
  • @EnableConfigurationProperties 注解是使我们之前配置的 @ConfigurationProperties 生效,让配置的属性成功的进入 Bean 中。
  • @ConditionalOnClass 表示当项目当前 classpath 下存在 HelloService 时,后面的配置才生效。

5、创建一个 spring.factories 文件
为什么需要 spring.factories 文件?
SpringBoot 项目的启动类都有一个 @SpringBootApplication 注解,这个注解是一个组合注解,其中包含一个 @EnableAutoConfiguration 注解。
@EnableAutoConfiguration 表示启用 Spring 应用程序上下文的自动配置,该注解会自动导入一个名为 AutoConfigurationImportSelector 的类,而这个类会去读取一个名为 spring.factories 的文件,在 spring.factories 中定义了需要加载的自动化配置类,打开任意一个框架的 Starter ,都能看到它有一个 spring.factories 文件,例如 MyBatis 的 Starter 如下:
在这里插入图片描述

那么我们就在 Maven 项目的 resources 目录下创建一个名为 META-INF 的文件夹,然后在文件夹中创建一个名为 spring.factories 的文件,文件内容如下:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.demo.starter.HelloServiceAutoConfiguration

在这里指定自动化配置类的路径即可。

6、使用上面自定义的Starter
首先将上面的Maven打包,并引入。

在引入了上面自定义的 Starter 后,现在项目中就有一个默认的 HelloService 实例可以使用了,而且关于这个实例的数据,也可以在 application.properties 中进行配置。

hello.name=Tom
hello.msg=Hadoop

直接在单元测试方法中注入 HelloSerivce 实例来使用。

import com.demo.starter.HelloService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class ApplicationTests {

    @Autowired
    HelloService helloService;

    @Test
    public void contextLoads() {
        System.out.println(helloService.sayHello());
    }
}

模拟自动装配

在这里插入图片描述
自定义配置:

rest.max = 50
rest.min = 10

读取自定义配置:

public class RestTemplateProperties {
    @Value("${rest.max}")
    private Integer max;
    @Value("${rest.min}")
    private Integer min;
	// setter/getter
}

导入组件配置类:

public class UleImportSelector implements ImportSelector {

    private Class<?> getSpringFactoriesLoaderFactoryClass() {
        return UleEnableAutoConfig.class;
    }

    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        List<String> configurations = getCandidateConfigurations();
        System.out.println(configurations);
        return StringUtils.toStringArray(configurations);
    }

    protected List<String> getCandidateConfigurations() {
        List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
                UleImportSelector.class.getClassLoader());
        Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
                + "are using a custom packaging, make sure that file is correct.");
        return configurations;
    }
}

通过 @Import 导入组件:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(UleImportSelector.class)
public @interface UleEnableAutoConfig {
}

配置类:

@Configurable
@PropertySource(value = {"classpath:restTemplate.properties"})
public class RestTemplateConfig {

    @Bean
    public RestTemplateProperties jdbcProperties(){
        RestTemplateProperties restTemplateProperties = new RestTemplateProperties();
        return restTemplateProperties;
    }
    @Bean
    public RestTemplate restTemplate(@Autowired RestTemplateProperties restTemplateProperties) {
        System.out.println(restTemplateProperties.toString());
        return new RestTemplate();
    } 
}

导入组件配置类读取的类全路径名:

com.example.config.UleEnableAutoConfig=\
  com.example.config.RestTemplateConfig
@Configurable
@UleEnableAutoConfig
public class SpringConfig {
}

加载 SpringConfig 配置类就可以获取 RestTemplate类了:

public class App {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
        Object bean = context.getBean("restTemplate");
        RestTemplateProperties restTemplateProperties = context.getBean(RestTemplateProperties.class);
        System.out.println(restTemplateProperties.toString());
        System.out.println(bean);
    }
}

SpringBoot启动原理

创建 SpringApplication

1、进入 SpringApplication.run 方法
2、new SpringApplication(primarySources):创建 SpringApplication
2.1、判断当前应用环境

  • Reactive 环境(全NIO),有 WebFlux 的类但没有 WebMvc 的类
  • Servlet(WebMvc) 环境
  • 非Web环境,没有有跟 Servlet 相关的类

3.2、 setInitializers:设置初始化器

  • 将一组类型为 ApplicationContextInitializer 的初始化器放入 SpringApplication 中。
  • 在容器刷新之前调用 ApplicationContextInitializer 实现类的 initialize 方法,并将 ConfigurableApplicationContext 类的实例传递给该方法。
  • SpringFactoriesLoader.loadFactoryNames 从 spring.factories 文件加载

3.3、 setListeners:设置监听器

  • ApplicationListener 实现类

总结:
1、SpringApplication 的创建和运行是两个不同的步骤。
2、SpringBoot 会根据当前classpath下的类来决定Web应用类型。
3、SpringBoot 的应用中包含两个关键组件:ApplicationContextInitializer 和 ApplicationListener ,分别是初始化器和监听器,它们都在构建 SpringApplication 时注册。

run():启动SpringApplication

public ConfigurableApplicationContext run(String... args) {
    // 4.1 创建StopWatch对象
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    // 4.2 创建空的IOC容器,和一组异常报告器
    ConfigurableApplicationContext context = null;
    Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
    // 4.3 配置与awt相关的信息
    configureHeadlessProperty();
    // 4.4 获取SpringApplicationRunListeners,并调用starting方法(回调机制)
    SpringApplicationRunListeners listeners = getRunListeners(args);
    // 【回调】首次启动run方法时立即调用。可用于非常早期的初始化(准备运行时环境之前)。
    listeners.starting();
    try {
        // 将main方法的args参数封装到一个对象中
        ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
        // 4.5 准备运行时环境
        ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
        //.............
    }

1、 创建StopWatch对象
2、 创建空的IOC容器,和一组异常报告器
3、 配置与awt相关的信息
4、 获取SpringApplicationRunListeners,并调用starting方法(回调机制)
5、 准备运行环境

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
	this.resourceLoader = resourceLoader;
	Assert.notNull(primarySources, "PrimarySources must not be null");
	this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
	this.webApplicationType = WebApplicationType.deduceFromClasspath();
	setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
	setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
	this.mainApplicationClass = deduceMainApplicationClass();
}

总结:
1、获取启动类
2、获取 web 应用类型
3、读取了对外扩展的 ApplicationContextInitializer,ApplicationListener
4、根据 main 推算出所在的类
就是初始化了一些信息。

public ConfigurableApplicationContext run(String... args) {
	// 用来记录 SpringBoot 启动耗时
	StopWatch stopWatch = new StopWatch();
	stopWatch.start();
	ConfigurableApplicationContext context = null;
	Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
	configureHeadlessProperty();
	SpringApplicationRunListeners listeners = getRunListeners(args);
	listeners.starting();
	try {
		ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
		ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
		configureIgnoreBeanInfo(environment);
		Banner printedBanner = printBanner(environment);
		context = createApplicationContext();
		exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
				new Class[] { ConfigurableApplicationContext.class }, context);
		prepareContext(context, environment, listeners, applicationArguments, printedBanner);
		refreshContext(context);
		afterRefresh(context, applicationArguments);
		stopWatch.stop();
		if (this.logStartupInfo) {
			new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
		}
		listeners.started(context);
		callRunners(context, applicationArguments);
	}
	catch (Throwable ex) {
		handleRunFailure(context, ex, exceptionReporters, listeners);
		throw new IllegalStateException(ex);
	}

	try {
		listeners.running(context);
	}
	catch (Throwable ex) {
		handleRunFailure(context, ex, exceptionReporters, null);
		throw new IllegalStateException(ex);
	}
	return context;
}

参考:
springboot自动配置以及原理分析

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值