Spring源码分析---容器实现(BeanFactory,ApplicationContext )02

来源:Spring

容器实现

2.1 BeanFactory 的实现
现有如下类,尝试将 Config 添加到 Bean 工厂中:

@Configuration
static class Config {
    @Bean
    public Bean1 bean1() {
        return new Bean1();
    }

    @Bean
    public Bean2 bean2() {
        return new Bean2();
    }
}
@Slf4j
static class Bean1 {
    public Bean1() {
        log.debug("构造 Bean1()");
    }

    @Autowired
    private Bean2 bean2;

    public Bean2 getBean2() {
        return bean2;
    }
}
@Slf4j
static class Bean2 {
    public Bean2() {
        log.debug("构造 Bean2()");
    }
}

需要使用到 BeanFactory 的一个实现类: DefaultListableBeanFactory。有了 Bean 工厂,还需要定义 Bean,之后再把定义的 Bean 注册到工厂即可。

public static void main(String[] args) {
    DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
    // bean 的定义(class,scope,初始化,销毁)
    AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition(Config.class)
            .setScope("singleton")
            .getBeanDefinition();
    beanFactory.registerBeanDefinition("config", beanDefinition);

    // 只有 config
    Arrays.stream(beanFactory.getBeanDefinitionNames()).forEach(System.out::println);
}

输出:

config

现在 Bean 工厂中 有且仅有一个 名为 config 的 Bean。

解析配置类

根据对 @Configuration 和 @Bean 两个注解的认识可知,Bean 工厂中应该还存在 bean1 和 bean2,那为什么现在没有呢?

很明显是现在的 BeanFactory 缺少了解析 @Configuration 和 @Bean 两个注解的能力。

public static void main(String[] args) {
    // --snip--

    // 给 BeanFactory 添加一些常用的后置处理器
    AnnotationConfigUtils.registerAnnotationConfigProcessors(beanFactory);
    Arrays.stream(beanFactory.getBeanDefinitionNames()).forEach(System.out::println);
}

输出:

config
org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory

根据打印出的信息,可以看到有一个名为 org.springframework.context.annotation.internalConfigurationAnnotationProcessor 的 Bean,根据其所含的 ConfigurationAnnotationProcessor 字样,可以知道这个 Bean 就是用来处理 @Configuration 和 @Bean 注解的,将配置类中定义的 Bean 信息补充到 BeanFactory 中。

那为什么在 Bean 工厂中依旧没有 bean1 和 bean2 呢?

现在仅仅是将处理器添加到了 Bean 工厂,还没有使用处理器。

使用处理器很简单,先获取到处理器,然后再使用即可。像 internalConfigurationAnnotationProcessor 这样的 Bean,都有一个共同的类型,名为 BeanFactoryPostProcessor,因此可以:

public static void main(String[] args) {
    // --snip--

    // 使用后置处理器
    // BeanFactoryPostProcessor 补充了一些 Bean 的定义
    beanFactory.getBeansOfType(BeanFactoryPostProcessor.class).values().forEach(i -> i.postProcessBeanFactory(beanFactory));
    Arrays.stream(beanFactory.getBeanDefinitionNames()).forEach(System.out::println);
}

输出:

config
org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
bean1
bean2

依赖注入

bean1 和 bean2 已经被添加到 Bean 工厂中,尝试获取 bean1 中的 bean2,查看 bean2 是否成功注入到 bean1 中:

public static void main(String[] args) {
    // --snip--
    System.out.println(beanFactory.getBean(Bean1.class).getBean2());
}

输出:

null

bean2 没有成功被注入到 bean1 中。

在先前添加到 BeanFactory 中的后置处理器里,有名为 internalAutowiredAnnotationProcessor 和 internalCommonAnnotationProcessor 的两个后置处理器。前者用于解析 @Autowired 注解,后者用于解析 @Resource 注解,它们都有一个共同的类型 BeanPostProcessor,因此可以:

public static void main(String[] args) {
    // --snip--
    System.out.println("---------------------------------------------");
    // Bean 后置处理器,对 Bean 的生命周期的各个阶段提供拓展,例如 @AutoWired @Resource...
    beanFactory.getBeansOfType(BeanPostProcessor.class).values().forEach(beanFactory::addBeanPostProcessor);
    System.out.println(beanFactory.getBean(Bean1.class).getBean2());
}

输出:

config
org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
bean1
bean2

依赖注入

bean1 和 bean2 已经被添加到 Bean 工厂中,尝试获取 bean1 中的 bean2,查看 bean2 是否成功注入到 bean1 中:

public static void main(String[] args) {
    // --snip--
    System.out.println(beanFactory.getBean(Bean1.class).getBean2());
}
null

bean2 没有成功被注入到 bean1 中。

在先前添加到 BeanFactory 中的后置处理器里,有名为 internalAutowiredAnnotationProcessor 和 internalCommonAnnotationProcessor 的两个后置处理器。前者用于解析 @Autowired 注解,后者用于解析 @Resource 注解,它们都有一个共同的类型 BeanPostProcessor,因此可以:

public static void main(String[] args) {
    // --snip--
    System.out.println("---------------------------------------------");
    // Bean 后置处理器,对 Bean 的生命周期的各个阶段提供拓展,例如 @AutoWired @Resource...
    beanFactory.getBeansOfType(BeanPostProcessor.class).values().forEach(beanFactory::addBeanPostProcessor);
    System.out.println(beanFactory.getBean(Bean1.class).getBean2());
}
----------------------------------------
[main] DEBUG test.bean.a02.TestBeanFactory$Bean1 - 构造 Bean1()
[main] DEBUG test.bean.a02.TestBeanFactory$Bean2 - 构造 Bean2()
test.bean.a02.TestBeanFactory$Bean2@6ee12bac

建立 BeanPostProcessor 和 BeanFactory 的关系后,bean2 被成功注入到 bean1 中了。

除此之外还可以发现:当需要使用 Bean 时,Bean 才会被创建,即按需加载。那有没有什么办法预先就初始化好单例对象呢?

public static void main(String[] args) {
    // --snip--

    // 预先初始化单例对象(完成依赖注入和初始化流程)
    beanFactory.preInstantiateSingletons();
    System.out.println("---------------------------------------------");
    beanFactory.getBeansOfType(BeanPostProcessor.class).values().forEach(beanFactory::addBeanPostProcessor);
    System.out.println(beanFactory.getBean(Bean1.class).getBean2());
}
[main] DEBUG test.bean.a02.TestBeanFactory$Bean1 - 构造 Bean1()
[main] DEBUG test.bean.a02.TestBeanFactory$Bean2 - 构造 Bean2()
----------------------------------------
test.bean.a02.TestBeanFactory$Bean2@6ee12bac

目前可以知道,BeanFactory 不会 :

主动调用 BeanFactory 后置处理器;
主动添加 Bean 后置处理器;
主动初始化单例对象;
解析 ${} 和 #{}

后置处理器的排序

在最初给出的类信息中进行补充:

@Configuration
static class Config {
    // --snip--

    @Bean
    public Bean3 bean3() {
        return new Bean3();
    }

    @Bean
    public Bean4 bean4() {
        return new Bean4();
    }
}

interface Inter {

}
@Slf4j
static class Bean3 implements Inter {
    public Bean3() {
        log.debug("构造 Bean3()");
    }
}
@Slf4j
static class Bean4 implements Inter {
    public Bean4() {
        log.debug("构造 Bean4()");
    }
}
@Slf4j
static class Bean1 {
    // --snip--

    @Autowired
    @Resource(name = "bean4")
    private Inter bean3;

    private Inter getInter() {
        return bean3;
    }
}

向 Bean 工厂中添加了 bean3 和 bean4,并且计划在 bean1 中注入 Inter 类型的 Bean。

现在 Bean 工厂中 Inter 类型的 Bean 有两个,分别是 bean3、bean4,那么会注入哪一个呢?

如果只使用 @Autowired,首先会按照类型注入,如果同种类型的 Bean 有多个,再按照变量名称注入,如果再注入失败,就报错;如果只使用 @Resource,也会采取与 @Autowired 一样的注入策略,只不过 @Resource 注解还可以指定需要注入 Bean 的 id(使用 name 属性进行指定),如果指定了需要注入 Bean 的 id,就直接按照指定的 id 进行注入,如果失败就报错。

那如果即使用 @Autowired 又使用 @Resource(name = “bean4”) 呢?

public static void main(String[] args) {
    // --snip--
    System.out.println(beanFactory.getBean(Bean1.class).getInter());
}
test.bean.a02.TestBeanFactory$Bean3@8e0379d

根据打印的结果可知,@Autowired 先生效了,这是因为 internalAutowiredAnnotationProcessor 排在 internalCommonAnnotationProcessor 之前。可以查看它们的先后关系:

public static void main(String[] args) {
    // --snip--
    beanFactory.getBeansOfType(BeanPostProcessor.class).values().forEach(i -> {
        System.out.println(">>>> " + i);
        beanFactory.addBeanPostProcessor(i);
    });
}
>>>> org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor@6385cb26
>>>> org.springframework.context.annotation.CommonAnnotationBeanPostProcessor@38364841

也可以改变它们的顺序,然后再查看注入的是 bean3 还是 bean4:

public static void main(String[] args) {
    // --snip--
    beanFactory.getBeansOfType(BeanPostProcessor.class).values().stream()
        .sorted(Objects.requireNonNull(beanFactory.getDependencyComparator()))
        .forEach(i -> {
            System.out.println(">>>> " + i);
            beanFactory.addBeanPostProcessor(i);
        });
    System.out.println(beanFactory.getBean(Bean1.class).getInter());
}
>>>> org.springframework.context.annotation.CommonAnnotationBeanPostProcessor@6385cb26
>>>> org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor@38364841
test.bean.a02.TestBeanFactory$Bean4@52e677af

改变 BeanPostProcessor 的先后顺序后,@Resource(name = “bean4”) 生效了,成功注入了 bean4。

为什么使用 beanFactory.getDependencyComparator() 后就改变了 BeanPostProcessor 的先后顺序呢?

在调用的 AnnotationConfigUtils.registerAnnotationConfigProcessors(beanFactory); 方法源码中有:

public static void registerAnnotationConfigProcessors(BeanDefinitionRegistry registry) {
    registerAnnotationConfigProcessors(registry, null);
}

public static Set<BeanDefinitionHolder> registerAnnotationConfigProcessors(
    BeanDefinitionRegistry registry, @Nullable Object source) {

    DefaultListableBeanFactory beanFactory = unwrapDefaultListableBeanFactory(registry);
    if (beanFactory != null) {
        if (!(beanFactory.getDependencyComparator() instanceof AnnotationAwareOrderComparator)) {
            // 设置比较器
            beanFactory.setDependencyComparator(AnnotationAwareOrderComparator.INSTANCE);
        }
        if (!(beanFactory.getAutowireCandidateResolver() instanceof ContextAnnotationAutowireCandidateResolver)) {
            beanFactory.setAutowireCandidateResolver(new ContextAnnotationAutowireCandidateResolver());
        }
    }
    
    // --snip--
}

设置的 AnnotationAwareOrderComparator 比较器会根据设置的 order 信息进行比较。

AutowiredAnnotationBeanPostProcessor 设置的 order 是:

private int order = Ordered.LOWEST_PRECEDENCE - 2;
CommonAnnotationBeanPostProcessor 设置的 order 是:

public CommonAnnotationBeanPostProcessor() {
    setOrder(Ordered.LOWEST_PRECEDENCE - 3);
    // --snip--
}

值越小,优先级越大,就排在更前面,因此当设置了 AnnotationAwareOrderComparator 比较器后,CommonAnnotationBeanPostProcessor 排在更前面,@Resource 就先生效。

2.2 ApplicationContext 的实现

@Slf4j
public class A02Application {

    static class Bean1 {

    }

    static class Bean2 {
        @Getter
        @Setter
        private Bean1 bean1;
    }
}

ClassPathXmlApplicationContext

较为经典的容器,基于 classpath 下的 xml 格式的配置文件来创建。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="bean1" class="test.bean.a02.A02Application.Bean1"/>

    <bean id="bean2" class="test.bean.a02.A02Application.Bean2">
        <property name="bean1" ref="bean1" />
    </bean>
</beans>
private static void testClassPathXmlApplicationContext() {
    ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("b01.xml");
    Arrays.stream(context.getBeanDefinitionNames()).forEach(System.out::println);
    System.out.println(context.getBean(Bean2.class).getBean1());
}
bean1
bean2
test.bean.a02.A02Application$Bean1@2db7a79b

FileSystemXmlApplicationContext

与 ClassPathXmlApplicationContext 相比,FileSystemXmlApplicationContext 是基于磁盘路径下 xml 格式的配置文件来创建。

private static void testFileSystemXmlApplicationContext() {
    // 使用相对路径时,以模块为起点(IDEA 中需要设置 Working directory),也支持绝对路径
    FileSystemXmlApplicationContext context = new FileSystemXmlApplicationContext("bean\\src\\main\\resources\\b01.xml");
    Arrays.stream(context.getBeanDefinitionNames()).forEach(System.out::println);
    System.out.println(context.getBean(Bean2.class).getBean1());
}
bean1
bean2
test.bean.a02.A02Application$Bean1@2db7a79b

从 XML 文件中读取 Bean 的定义

ClassPathXmlApplicationContext 和 FileSystemXmlApplicationContext 都依赖于从 XML 文件中读取 Bean 的信息,而这都利用了 XmlBeanDefinitionReader 进行读取。

private static void testXmlBeanDefinitionReader() {
        DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
        System.out.println("读取之前...");
        Arrays.stream(beanFactory.getBeanDefinitionNames()).forEach(System.out::println);
        System.out.println("读取之后...");
        XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory);
//        reader.loadBeanDefinitions(new ClassPathResource("b01.xml"));
        reader.loadBeanDefinitions(new FileSystemResource("bean\\src\\main\\resources\\b01.xml"));
        Arrays.stream(beanFactory.getBeanDefinitionNames()).forEach(System.out::println);
    }
读取之前...
读取之后...
bean1
bean2

AnnotationConfigApplicationContext

基于 Java 配置类来创建。首先定义配置类:

@Configuration
static class Config {
    @Bean
    public Bean1 bean1() {
        return new Bean1();
    }

    @Bean
    public Bean2 bean2(Bean1 bean1) {
        Bean2 bean2 = new Bean2();
        bean2.setBean1(bean1);
        return bean2;
    }
}
private static void testAnnotationConfigApplicationContext() {
    AnnotationConfigApplicationContext context =
            new AnnotationConfigApplicationContext(Config.class);
    Arrays.stream(context.getBeanDefinitionNames()).forEach(System.out::println);
    System.out.println(context.getBean(Bean2.class).getBean1());
}
org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
a02Application.Config
bean1
bean2
test.bean.a02.A02Application$Bean1@1f0f1111

与前面两种基于 XML 创建 ApplicationContext 的方式相比,使用 AnnotationConfigApplicationContext 后,使得容器中多了一些后置处理器相关的 Bean。

如果要在先前的两种方式中也添加上这些 Bean,可以在 XML 进行配置:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
    <bean id="bean1" class="test.bean.a02.A02Application.Bean1"/>

    <bean id="bean2" class="test.bean.a02.A02Application.Bean2">
        <property name="bean1" ref="bean1" />
    </bean>

    <!--  添加后置处理器  -->
    <context:annotation-config />
</beans>

AnnotationConfigServletWebServerApplicationContext

基于 Java 配置类来创建,用于 web 环境。首先定义配置类:

@Configuration
static class WebConfig {
    @Bean
    public ServletWebServerFactory servletWebServerFactory() {
        // 提供内嵌的 Web 容器
        return new TomcatServletWebServerFactory();
    }

    @Bean
    public DispatcherServlet dispatcherServlet() {
        // 添加前控制器
        return new DispatcherServlet();
    }

    @Bean
    public DispatcherServletRegistrationBean registrationBean(DispatcherServlet dispatcherServlet) {
        // 将 DispatcherServlet 注册到 Tomcat 服务器
        return new DispatcherServletRegistrationBean(dispatcherServlet, "/");
    }

    // 如果 bean 以 '/' 开头,将 '/' 后的 bean 的名称作为访问路径
    @Bean("/hello")
    public Controller controller1() {
        // 添加控制器,用于展示
        return (request, response) -> {
            response.getWriter().println("hello");
            return null;
        };
    }
}

调用方法:

private static void testAnnotationConfigServletWebServerApplicationContext() {
    AnnotationConfigServletWebServerApplicationContext context =
            new AnnotationConfigServletWebServerApplicationContext(WebConfig.class);
}

运行代码,在浏览器中访问 http://localhost:8080/hello 路径则会显示出 hello 字样:

另外:

2.3 BeanFactory 接口体系
BeanFactory 其实就是 Spring IoC 容器,它本身是一个接口,提供了一系列获取 Bean 的方式。

基于它也有众多子接口:

ListableBeanFactory:提供获取 Bean 集合的能力,比如一个接口可能有多个实现,通过该接口下的方法就能获取某种类型的所有 Bean;
HierarchicalBeanFactory:Hierarchical 意为 “层次化”,通常表示一种具有层级结构的概念或组织方式,这种层次化结构可以通过父子关系来表示对象之间的关联,比如树、图、文件系统、组织架构等。根据该接口下的方法可知,能够获取到父容器,说明 BeanFactory 有父子容器概念;
AutowireCapableBeanFactory:提供了创建 Bean、自动装配 Bean、属性填充、Bean 初始化、依赖注入等能力,比如 @Autowired 注解的底层实现就依赖于该接口的 resolveDependency() 方法;
ConfigurableBeanFactory:该接口并未直接继承至 BeanFactory,而是继承了 HierarchicalBeanFactory。Configurable 意为 “可配置的”,就是说该接口用于对 BeanFactory 进行一些配置,比如设置类型转换器。
2.4 读取 BeanDefinition
BeanDefinition 也是一个接口,它封装了 Bean 的定义,Spring 根据 Bean 的定义,就能创建出符合要求的 Bean。

读取 BeanDefinition 可以通过下列两种类完成:

BeanDefinitionReader
ClassPathBeanDefinitionScanner
BeanDefinitionReader

该接口中对 loadBeanDefinitions() 方法进行了多种重载,支持传入一个或多个 Resource 对象、资源位置来加载 BeanDefinition。

它有一系列相关实现,比如:

XmlBeanDefinitionReader:通过读取 XML 文件来加载;
PropertiesBeanDefinitionReader:通过读取 properties 文件来加载,此类已经被 @Deprecated 注解标记;
除此之外,还有一个 AnnotatedBeanDefinitionReader,尽管它并不是 BeanDefinition 的子类,但它们俩长得很像,根据其类注释可知:它能够通过编程的方式对 Bean 进行注册,是 ClassPathBeanDefinitionScanner 的替代方案,能读取通过注解定义的 Bean。

ClassPathBeanDefinitionScanner

通过扫描指定包路径下的 @Component 及其派生注解来注册 Bean,是 @ComponentScan 注解的底层实现。

比如 MyBatis 通过继承 ClassPathBeanDefinitionScanner 实现通过 @MapperScan 注解来扫描指定包下的 Mapper 接口。

BeanDefinitionRegistry

AnnotatedBeanDefinitionReader 和 ClassPathBeanDefinitionScanner 中都有一个 BeanDefinitionRegistry 类型的成员变量,它是一个接口,提供了 BeanDefinition 的增加、删除和查找功能。

注册与获取 Bean

根据前面的补充,现在可以这样注册并获取 Bean:

public class DefaultListableBeanFactoryTest {
    static class MyBean {
    }

    public static void main(String[] args) {
        // 既实现了 BeanFactory,又实现了 BeanDefinitionRegistry
        DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
        // ClassPathBeanDefinitionScanner 的一种替代,编程式显式注册 bean
        AnnotatedBeanDefinitionReader reader = new AnnotatedBeanDefinitionReader(beanFactory);
        reader.registerBean(MyBean.class);
        MyBean bean = beanFactory.getBean(MyBean.class);
        System.out.println(bean);
    }
}

2.5 ApplicationContext 接口体系

public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory, MessageSource, ApplicationEventPublisher, ResourcePatternResolver {
}

ApplicationContext 接口继承了许多接口,,其中:
EnvironmentCapable:提供获取 Environment 的能力
ListableBeanFactory:提供了获取某种类型的 Bean 集合的能力
HierarchicalBeanFactory:提供了获取父容器的能力
MessageSource:提供了对国际化信息进行处理的能力
ApplicationEventPublisher:提供了事件发布能力
ResourcePatternResolver:提供了通过通配符获取多个资源的能力

虽然 ApplicationContext 继承了很多接口,但这些能力的实现是通过一种委派(Delegate)的方式实现的,这种方式也被叫做委派模式,但它并不属于 GoF 的 23 种设计模式中的一种,是一种面向对象的设计模式。什么是委派呢?

public class MyApplicationContext implements ApplicationContext {

    private final ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();

    @Override
    public Resource[] getResources(String locationPattern) throws IOException {
        return resourcePatternResolver.getResources(locationPattern);
    }

}

实现获取资源的方式并不是由实现类自身完成,而是交给其内部的一个成员变量完成,这样的方式就是委派(这和对象适配器模式很相似)。

在日常编码遇到这样的实现逻辑时,类名可以以 Delegate 结尾。

ConfigurableApplicationContext

ApplicationContext 有一个子接口 ConfigurableApplicationContext,从类名就可以看出,它提供了对 ApplicationContext 进行配置的能力,浏览其内部方法可知,提供了诸如设置父容器、设置 Environment 等能力。

AbstractApplicationContext

ApplicationContext 有一个非常重要的抽象实现 AbstractApplicationContext,其他具体实现都会继承这个抽象实现,在其内部通过委派的方式实现了一些接口的能力,除此之外还有一个与 Spring Bean 的生命周期息息相关的方法:refresh()。

  • 27
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值