Spring 实例化 构造方法 工厂方法

本文详细解释了Spring框架中Bean实例化过程中,构造方法和工厂方法的创建流程,包括构造方法的选择策略、工厂方法的筛选机制以及自动装配的处理方式。
摘要由CSDN通过智能技术生成

博文目录


内容总结

Spring 创建 Bean, 在实例化阶段, 有两种方式, 分别是构造方法方式和工厂方法方式, 在扫描 BeanDefinition 的时候, 如果类中有 @Bean 方法(实例/静态), 则方法名称被扫描成 BeanName, 收集信息并创建对应的 BeanDefinition, 方法名称作为 factory-method-name 被设置到 BeanDefinition 中, 实例化时, 根据是否设置了 factory-method-name 走不同的创建流程

构造方法创建流程

一个类可以有多个构造方法, Spring 根据一些规则选择其中之一, 简单来说是经过两个步骤的筛选

  • 初步筛选, 返回 null 的会直接调无参构造函数完成实例化
    • 类中没有 @Autowired 注解的构造方法
      • 有多个构造方法, 返回 null
      • 只有一个无参构造方法, 返回 null
      • 只有一个有参构造方法, 返回该构造方法
    • 类中存在 @Autowired 注解的构造方法
      • 有多个 required=true 的构造方法, 异常. @Autowired(required=true) 的构造方法只能有一个, required 默认值就是 true
      • 有一个 required=true 和多个 required=false 的构造方法, 异常
      • 只有一个 required=true 的构造方法, 返回该构造方法
      • 没有 required=true 的构造方法, 返回所有 required=false 的构造方法以及无参构造方法
  • 二次推断, 初步筛选返回非 null 非异常的会进入二次推断 (默认情况下, 其他三个条件基本不会使用到)
    • 默认情况下, 初步筛选有内容才会进入二次推断, 默认会开启构造方法参数自动注入功能
    • 给候选项排序, 公开的在前面, 然后参数越多的越靠前
    • 通常选择参数最多的那个构造方法, 除非该方法自动注入时没有找全参数而报错
  • 根据找到的构造方法和参数, 完成 Bean 生命周期中的实例化步骤

工厂方法创建流程

和构造方法创建流程类似, 相对较为简单, 支持多个工厂方法存在, 会从中选一个合适的来执行实例化

机制了解

	public static void main(String[] args) {
//		AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Application.class);
		AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
		context.register(Application.class);

		AbstractBeanDefinition definition = BeanDefinitionBuilder.genericBeanDefinition().getBeanDefinition();
		definition.setBeanClass(User.class);
		// 自动决定使用哪个构造方法, 需要的参数也自己找, 默认是 AUTOWIRE_NO
		definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR); // 允许构造方法的参数, 可以使用依赖注入, 不设置的话只能使用传入的参数, 不能自动找其他参数
		definition.getConstructorArgumentValues().addGenericArgumentValue(new Object());
		definition.getConstructorArgumentValues().addGenericArgumentValue(new RuntimeBeanNameReference("object"));
		definition.getConstructorArgumentValues().addGenericArgumentValue(new RuntimeBeanReference("object"));
		definition.getConstructorArgumentValues().addGenericArgumentValue(new RuntimeBeanReference(Object.class));
		definition.getConstructorArgumentValues().addIndexedArgumentValue(0, new Object());
		definition.getConstructorArgumentValues().addIndexedArgumentValue(1, new RuntimeBeanNameReference("object"));
		definition.getConstructorArgumentValues().addIndexedArgumentValue(2, new RuntimeBeanReference("object"));
		definition.getConstructorArgumentValues().addIndexedArgumentValue(3, new RuntimeBeanReference(Object.class));
		context.registerBeanDefinition("user", definition);

		context.refresh();

		System.out.println(context.getBean("user"));
		System.out.println(context.getBean(User.class));
		System.out.println(context.getBean("user", new Object(), new Object()));
		
	}

Spring 支持下面方式为实例化传入构造方法参数

  • getBean(BeanName, 参数一, 参数二, 参数三, …)
  • BeanDefinition.getConstructorArgumentValues().addGenericArgumentValue(参数一)
  • BeanDefinition.getConstructorArgumentValues().addGenericArgumentValue(参数二)
  • BeanDefinition.getConstructorArgumentValues().addIndexedArgumentValue(参数位置1, 参数三)
  • BeanDefinition.getConstructorArgumentValues().addIndexedArgumentValue(参数位置2, 参数四)

Spring 支持下面的参数类型

  • 普通参数如 new OrderService()
  • 引用参数, 如 RuntimeBeanNameReference(“object”) / RuntimeBeanReference(“object”) / RuntimeBeanReference(Object.class) 等, 引用参数在解析的时候, 会从容器中找对应的 Bean

Spring 支持通过 BeanDefinition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR) 启用构造方法参数的依赖注入, 不设置的话只能使用传入的参数, 不一定能从容器中自动找构造方法缺少的其他参数

通过构造方法完成实例化的流程

实例化的入口在 AbstractAutowireCapableBeanFactory.doCreateBean 开始的 AbstractAutowireCapableBeanFactory.createBeanInstance

大致流程

  • 解析 Bean 类型, 确保该 Class 已经被 JVM 加载过了
  • BeanDefinition.setInstanceSupplier() 处理, 如果通过这种方式设置了实例提供者, 则截断创建流程并返回提供者生成的实例, 一个小机制
  • 处理类中的 @Bean 标注的方法, @Bean 方法会按照 factory-method 的方式处理, 分 static 和 non-static 两种情况
    • static: @Bean 方法会被解析成为 factory-bean 和 factory-method 来处理
    • non-static: @Bean 方法会被解析成为 class 和 factory-method 来处理
  • 读取缓存判断构造方法和工厂方法是否被解析过了, 解析过的话直接拿到构造方法和对应参数去调用实例化即可, 分有无参数两种情况
    • 有参数: 调用 AbstractAutowireCapableBeanFactory.autowireConstructor 根据有参构造方法和对应参数做实例化并返回
    • 无参数: 调用 AbstractAutowireCapableBeanFactory.instantiateBean 根据无参构造方法做实例化并返回
  • 接下来就是没有缓存的情况, 开始解析构造方法和参数的逻辑了
  • 调用 AbstractAutowireCapableBeanFactory.determineConstructorsFromBeanPostProcessors 从类中筛选所有符合条件的候选构造方法, 调用的是 SmartInstantiationAwareBeanPostProcessor.determineCandidateConstructors, 目前这个接口只有处理 @Autowired 的 AutowiredAnnotationBeanPostProcessor 一个实现, 可以通过 @Autowired 来指定 Spring 使用哪个构造方法来实例化 Bean
  • 判断 4 个条件任意一个满足, 即进入自动推断构造方法的流程, 找到最恰当的一个构造方法, 然后执行实例化 Bean. 常规使用时基本都是使用无参构造方法
    • 是否有找到任意候选构造方法, 有即可进入流程
    • BeanDefinition 是否被设置了 autowireMode 属性为 AUTOWIRE_CONSTRUCTOR, 表示启用了构造方法自动装配, 即构造方法的参数可以通过容器做依赖注入
    • BeanDefinition.hasConstructorArgumentValues(), 是否被设置了构造方法参数, 有的话执行实例化时会使用到
    • getBean 时是否传入了构造方法参数, 有的话执行实例化时会用到
  • 如果上面条件都不满足, 则直接调用 AbstractAutowireCapableBeanFactory.instantiateBean 执行无参构造方法生成实例, 没有无参构造方法的话则报错

筛选候选构造方法 determineConstructorsFromBeanPostProcessors

推断构造方法不同情况总结

设计思路

@Autowired(required=false) 注解的构造方法是可选构造方法, @Autowired(required=true) 注解的构造方法是必选构造方法, 一旦有必选就不能存在任何可选, 否则报错

结果

  • 类中没有 @Autowired 注解的构造方法
    • 有多个构造方法, 返回 null
    • 只有一个无参构造方法, 返回 null
    • 只有一个有参构造方法, 返回该构造方法
  • 类中存在 @Autowired 注解的构造方法
    • 有多个 required=true 的构造方法, 异常. @Autowired(required=true) 的构造方法只能有一个, required 默认值就是 true
    • 有一个 required=true 和多个 required=false 的构造方法, 异常
    • 只有一个 required=true 的构造方法, 返回该构造方法
    • 没有 required=true 的构造方法, 返回所有 required=false 的构造方法以及无参构造方法

调用 AutowiredAnnotationBeanPostProcessor.determineCandidateConstructors 方法

  • 检查 @LookUp 注解, 目前已经不用这种方式了, 推荐使用更为灵活和强大的依赖查找功能
    • @Lookup(BeanName) 注解可以加在某方法上, 会忽略方法内容, 每次调用该方法都相当于执行 getBean(BeanName) 并返回该 Bean, 然后转为方法返回值指定的类型. 这种机制通常用于获取原型 Bean
    • 解析出 @LookUp 方法, 转换为 LookupOverride, 添加到 BeanDefinition 的 methodOverrides 中
    • 在后续推断构造方法实例化 (autowireConstructor) 和默认无参构造方法实例化 (instantiateBean) 的最后实例化时, 通过默认的 CglibSubclassingInstantiationStrategy 策略执行实例化时, 通过判断 BeanDefinition 的 methodOverrides 是否有内容, 即是否有 @LookUp 注解的方法, 如果有则走 CGLib 代理对象创建逻辑
    • 创建代理对象时, 有一个 LookupOverrideMethodInterceptor 拦截器, 后续执行该代理对象的方法时, 就会走该拦截器的 intercept 方法, 会判断当前执行的方法是否有对应的 LookupOverride, 即是否是 @LookUp 注解过的方法, 是的话直接通过 BeanFactory 调用 getBean 获取对象并返回, 完全忽略方法体本身的内容
  • 从缓存中获取该类的候选构造方法, 有则返回, 无则解析
    • candidates: 存放候选构造方法, 加了 @Autowired 的构造方法就是候选构造方法, 不管 required 是否 true / false
    • requiredConstructor: 用于存放唯一一个 required=true 的加了 @Autowired 注解的构造方法, 一个类中只能有一个 required=true 的构造方法
    • defaultConstructor: 用于存放没有加 @Autowired 注解的无参构造方法, 加了的会放在 candidates 中而不是放在这里
  • 拿到所有构造方法并遍历
    • 判断当前正在遍历的构造方法是否有加 @Autowired 注解. 当前类可能是一个 CGLib 代理类, 这种情况会找实际类的相同参数类型的那个构造方法是否有 @Autowired 注解
      • 当前遍历的构造方法有 @Autowired 注解, 且已经存在了一个加 @Autowired(required=true) 的构造方法, 则在这里报错
      • 当前遍历的构造方法是 @Autowired(required=true) 的, 且已经有其他 required=false 的构造方法, 则在这里报错, 不报错时则给 requiredConstructor 赋值
      • 加了 @Autowired 注解的构造方法都加入到候选构造方法 candidates 中
    • 如果没有加 @Autowired 且参数个数是 0, 说明是无参构造方法, 赋值给 defaultConstructor
  • 遍历结束, 判断是否有找到候选构造方法BeanDefinition
    • 有候选, 可能是 一个必选 或 N 个可选
      • 如果是 1 个必选, 则把该必选转成候选数组
      • 如果是 N 个可选, 则把 没有 @Autowired 注解的无参构造方法也加到候选里, 然后把候选转为候选数组
    • 没候选, 且类中只有一个有参构造方法, 则把该构造方法转为候选数组
    • 没候选, 且类中只有一个无参构造方法或多个构造方法(不管有参无参), 则构造一个空的候选数组
  • 把候选结果加入缓存
  • 判断候选数组元素个数, 大于 0 的返回勾选构造方法, 否则返回 null

推断构造方法 autowireConstructor

Spring推断构造方法底层原理

调用的是 ConstructorResolver.autowireConstructor, Spring 优先考虑公开且参数最多的构造函数, 当都公开且参数个数相同时, 会将差异最小的那个作为选中的构造函数来使用

定义变量

  • constructorToUse: 存放最终选择好的要用来实例化的那个构造方法
  • argsToUse: 存放最终要使用的构造方法的参数

流程

  • 判断是否通过 getBean 传入了构造方法参数
    • 是, 则直接使用该参数, Spring 不再找了
    • 否, 从缓存里读已经解析过的构造方法和参数, 以及解析预准备的参数 (暂不清楚到底是什么)
  • 如果 constructorToUse 和 argsTuUse 任何一个是 null, 就走推断构造方法流程
  • 将传入的候选构造方法作为候选, 如果候选是 null, 则重新从类里获取所有构造方法, 作为新的候选
  • 如果候选只有一个, 且没有通过 getBean 和 BeanDefinition 传入参数, 且该候选是无参构造方法, 则将该构造方法作为推断结果, 假如缓存, 并执行初始化, 生成对象
  • 定义变量 autowiring, 表示是否启用构造方法参数自动依赖注入功能, 有两个条件, 一个是传入的候选有值, 一个 BeanDefinition 设置了 autowireMode 为 AUTOWIRE_CONSTRUCTOR, 两个条件任一满足即可启用该功能
  • 定义变量 resolvedValues, 存放用户设置过的构造方法参数被解析后的结果, 包括普通参数和引用参数等
    • resolvedValues 只包含用户设置的参数. 如构造方法有两个参数, 而用户只给第二个参数赋值了, 那么最后第二个参数肯定来自用户赋值, 而第一个参数可能是 Spring 从容器中找到的, 也可能因为没有开启自动注入导致报错
  • 确定构造方法的最少参数个数 minNrOfArgs, 在后面用于过滤构造方法
    • 如果通过 getBean 传如了参数, 则取其参数个数
    • 否则通过 BeanDefinition.getConstructorArgumentValues 获取到 ConstructorArgumentValues 对象, 调用 getArgumentCount 方法得到默认参数个数, 遍历其 genericArgumentValues 和 indexedArgumentValues, 计算合适的 minNrOfArgs 值
      • 比如 genericArgumentValues 有 3 个参数, 则 minNrOfArgs 至少是 3
      • 比如 indexedArgumentValues 有 1 个参数, 是 (1, Object), 意味着给索引为 1 的参数设置了值, 而索引 0 的参数没有设置值, 则 minNrOfArgs 至少是 2
    • 如果未主动通过 BeanDefinition 设置参数, 则得到的 minNrOfArgs 是 0
    • 获取 minNrOfArgs 的同时, 也会把 ConstructorArgumentValues 里面的所有参数都解析一遍, 包括普通参数和引用参数, 然后加入到 resolvedValues 中
      • 明确参数, 如 addGenericArgumentValue(new OrderService()) / addIndexedArgumentValue(0, new Object()) 等
      • 引用参数, 如 RuntimeBeanNameReference(“object”) / RuntimeBeanReference(“object”) / RuntimeBeanReference(Object.class) 等, 这种对象提供了 Bean 的名称或类型, 解析时会到容器里找到匹配的 Bean 并重新设置到传入的那个对象中
  • 给候选项排序, 公开的在前面, 然后参数越多的越靠前
  • 设置一个变量 minTypeDiffWeight = Integer.MAX_VALUE, 默认是最大值, 用于记录差异权重最小的那个构造方法的差异权重, 遍历的过程中, 会判断当前构造方法的差异权重是否小于最小权重, 是的话, 就会更新最小权重值, 并将当前构造方法以及对应参数赋值给 constructorToUse 和 argsToUse 等. 因为该值默认是最大值, 所以首次遍历就会满足条件并赋值, 后续则判断更小的才会更新
  • 遍历候选项
    • 如果 constructorToUse 和 argsToUse 都不是 null 且 argsToUse 的参数个数大于当前遍历的构造方法的参数个数, 则跳出循环
      • 候选项是有顺序的, 参数个数越多的越靠前, 假如第一次遍历的是一个 3 参构造函数, 第二次遍历的是一个 2 参构造函数, 这时判断参数个数少了, 就直接跳出遍历. 换句话说, Spring 优先考虑公开且参数最多的构造函数, 当都公开且参数个数相同时, 会遍历并计算匹配差异值, 将差异最小的那个作为选中的构造函数来使用
    • 当前构造方法参数个数小于构造方法参数个数限制 minNrOfArgs 值的, 直接跳过本次循环
    • 定义变量 argsHolder, 存放从 resolvedValues 和 容器 中找到的匹配当前构造方法的参数, 参数被包装成 ArgumentsHolder 对象形式
    • 判断是否通过 getBean 传入了参数
      • 是, 如果当前构造方法参数个数不等于传入参数个数, 跳过本次循环, 否则将参数打包成 argsHolder
      • 否, 拿到当前构造方法的 参数名称, 参数类型, resolvedValues 等, 一起传入 ConstructorResolver.createArgumentArray 方法. 遍历该构造方法的参数, 从 resolvedValues 或者 容器 中拿到合适的值, 最终将对应该构造方法的所有参数打包成 ArgumentHolder 并返回, 赋值给 argHolder 变量
        • 参数名称通过 @ConstructorProperties 注解指定, 或通过 参数名称发现器 获取
    • 计算解析出来的参数 argHolder 和 当前正在遍历的构造函数的参数类型 之间的差异权重, 根据是否宽松模式(默认是)选不同的计算方法. 该值越低, 说明找出来的各参数与该构造方法越匹配
    • 判断当前差异权重值与最小差异权重值
      • 如果当前的值更小, 说明当前构造方法和找出来的参数更合适用来初始化对象, 将 constructorToUse 和 argsToUse 等替换为当前构造方法和参数, 更新最小差异权重值
      • 如果两个值相同, 则说明之前遍历出来的最小差异权重值对应的那个构造方法和当前正在遍历的构造方法具有相同的优先级, Spring 无法做决策来选择使用哪一个, 会把这两个构造方法都加入到冲突构造方法列表内, 待后续判断是否还有差异更小的构造方法, 还有的话就放弃这两个, 没有的话, 只能报错了
  • 遍历结束, 判断 constructorToUse 是否为 null, 即是否有找到满意的构造方法
    • 如果没找到, 则报错
    • 如果有找到, 但是同时也存在与之差异权重值相同的冲突构造方法, 且当前不是宽松模式, 则同样报错
    • 如果有找到, 不管是否冲突, 只要是宽松模式, 就使用 constructorToUse 里的那个, 即首次遍历到的那个, 作为最终选择的构造方法
  • 用找到的构造方法和参数来实例化 Bean 对象

组装合适的参数 createArgumentArray

根据用户通过 BeanDefinition 传入的参数的解析结果 resolvedValues, 以及是否启用自动查找和注入功能 autowiring, 当前正在遍历的构造方法的 参数名称和参数类型 等条件, 从 resolvedValues 和 容器 中找到匹配的参数对象, 最终组装成为 ArgumentsHolder 并返回

  • 根据参数类型遍历
  • 尝试从 resolvedValues 中找到该位置的参数的值, 如果找到了则再做一些处理
  • 如果没有找到, 则看 autowiring, false 的话就报错, true 的话就调用 BeanFactory.resolveDependency 到容器中找

通过工厂方法完成实例化的流程 (@Bean)

理解

@Bean 方法, 可以在 @Configuration 配置类里, 也可以在 @Component 普通类里, 且不管是否为 static, 都会被解析到并生成 Bean

@Bean 这种创建 Bean 的方法和构造方法创建 Bean 非常类似, 同样接受通过 getBean 和 BeanDefinition 传递的参数, 以及设置 autowireMode, 同样有筛选的流程, 且筛选的倾向也一致

@ComponentScan
@Configuration
public class Application {
	@Bean
	public A a() {
		return new A();
	}
	public static void main(String[] args) {
		AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Application.class);
		context.getBean("a");
	}
}

容器刷新时, 除了加 @Component 注解的类会被创建成为 Bean 外, Application 类也会被创建成 Bean, Application 里面通过 @Bean 注解标注的 a 方法的返回值 A 类, 也会被创建成名称为 a 的 Bean

Spring 在启动时会扫描到所有符合条件的类, 生成 BeanName 以及对应的 BeanDefinition, 其中就包括 a. 在 Spring 遍历调用 getBean 生成 Bean 的时候, 在实例化阶段, 就会判断对应的 BeanDefinition 是否有解析到 factory-method-name, 有的话, 说明这个 Bean 要使用指定的工厂方法创建, 而不是由 Spring 自动选择构造方法创建

@Bean 方法的名称, 被解析成为 factory-method-name

  • 无 static: 解析 @Bean 方法所在的 Bean 为 factory-bean, 通过调用该 Bean 的实例方法创建指定的 Bean
  • 有 static: 解析 @Bean 方法所在的类为 beanClass, 通过调用该类的静态方法创建指定的 Bean

如果有多个名称不同的 @Bean 方法, 则每个方法名称会被解析成为一个 BeanName, 分别对应不同的 BeanDefinition

如果有多个名称相同的 @Bean 方法, 因为是同一个 BeanName, 所以 BeanDefinitio 也只有一个. 解析第一个 @Bean 方法的时候会生成 BeanDefinition, 其 isFactoryMethodUnique 字段的值为 true, 解析第二个 @Bean 方法的时候, 因为 BeanDefinition 已经存在, 不会再次创建, 而是修改其 isFactoryMethodUnique 字段的值为 false

如果有多个名称相同的 @Bean 方法, 但是是在不同的类中, 则会使用首次解析到的那个类, 后续解析到的同名 @Bean 方法会被直接忽略? 我猜的

isFactoryMethodUnique 为 true 时, Spring 直接使用唯一的 factory-method 执行实例化, 为 false 时, Spring 会对多个 factory-method 走一遍筛选流程, 和推断构造方法流程类似, 最终决定要使用的唯一一个方法, 执行实例化

Spring 筛选 @Bean 方法有如下倾向

  • 优先使用 factory-bean 实例方法来实例化, 只要有实例方法, 则不再考虑静态方法
  • 优先使用 公开的 参数多的 方法来实例化

大致流程

入口在 AbstractAutowireCapableBeanFactory.createBeanInstance 的开始, 判断是否有解析到 factory-method-name

if (mbd.getFactoryMethodName() != null) {
	return instantiateUsingFactoryMethod(beanName, mbd, args);
}

具体入口在 AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod, 即 ConstructorResolver.instantiateUsingFactoryMethod

  • 判断 BeanDefinitio 中是否解析到 factory-bean-name
    • 有, 确定本次筛选只找实例方法, isStatic=false
    • 无, 确定本次筛选只找静态方法, isStatic=true
  • 判断是否通过 getBean 传入了参数, 有则使用该参数, 无则根据缓存判断是否解析过方法和参数
  • 方法和参数有任何一个为 null, 就进入解析流程
  • 如果 isFactoryMethodUnique 是 true, 说明是唯一的, 方法加入候选, 这里不一定能找得到
  • 如果候选是空的, 则拿到所有方法, 将其中 isStatic 和名称匹配的方法拿出来, 加入候选
  • 如果候选只有一个, 且没有通过 getBean 或 BeanDefinition 传入参数, 则直接使用该方法实例化
  • 如果候选不止一个, 则排序, 公开的在前, 参数多的在前
  • 定义变量 resolvedValues, 存通过 BeanDefinition 传入的参数的解析结果
  • 定义变量 autowiring, 条件只有一个, BeanDefinition 是否设置了 autowireMode 为 AUTOWIRE_CONSTRUCTOR, 是就开启自动注入
  • 定义变量 minNrOfArgs, 用于筛选方法的参数个数, 个数少于该值的方法直接跳过
    • 如果通过 getBean 传入了参数, 则 minNrOfArgs 为传入的参数个数
    • 如果通过 BeanDefinition 传入了参数, 则解析这些参数(如 RuntimeBeanReference(“object”)), 并返回至少要包含的参数个数, 并赋值给 minNrOfArgs
    • 如果没有传入参数, 则 minNrOfArgs 为 0
  • 遍历候选
    • 参数个数少于 minNrOfArgs 的直接跳过
    • 根据当前遍历的工厂方法, 以及传入的参数, 以及是否开启自动注入, 从传入的参数和容器中找到匹配该方法的参数
    • 计算找到的参数和方法参数列表的差异权重, 差异权重相同的加入到冲突列表中, 如果没有更低的差异权重来覆盖, 则最后报错
    • 遍历一圈, 拿到差异权重最小的那个方法和参数, 调用方法, 完成实例化
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值