spring注解之ConfigurationProperties

EnableConfigurationProperties

启用ConfigurationProperties注解,EnableConfigurationProperties注解通过import导入EnableConfigurationPropertiesImportSelector选择器

属性

value:类型为class数组,如果指定了value属性,则EnableConfigurationPropertiesImportSelector选择器会额外注册一个ConfigurationPropertiesBeanRegistrar bean。否则仅注册ConfigurationPropertiesBindingPostProcessorRegistrar bean

ConfigurationPropertiesBeanRegistrar

作用就跟它的名字一毛一样,注册一个ConfigurationPropertiesBean的定义至上下文,如果value属性指定的bean存在ConfigurationProperties注解,则注册至上下文的bean名称命名规则为ConfigurationProperties前缀+type.getName;否则直接取类型名称作为bean的名称。问题:为什么要注册value中指定的BeanDefinition至上下文?暂不讨论继续往下看。

ConfigurationPropertiesBindingPostProcessorRegistrar

作用也跟名称一毛一样,但会额外做件事就是将ConfigurationBeanFactoryMetaData也注册至上下文,当且也把ConfigurationPropertiesBindingPostProcessor注册至上下文

ConfigurationBeanFactoryMetaData

该类实现了BeanFactoryPostProcessor接口,也就是说在工厂创建完成后会回调它的postProcessBeanFactory执行一些后置动作处理。
缓存了存在工厂方法的bean。问题:缓存存在工厂方法的bean定义的用处是什么?

ConfigurationPropertiesBindingPostProcessor

自身初始化

该类实现了InitializingBean接口,自身初始化完成时回调,如果工厂存在PropertySourcesPlaceholderConfigurer bean则使用该配置类的配置源getAppliedPropertySources;否则使用当前环境中的配置源getPropertySources。如果validator为空则从工厂中获取configurationPropertiesValidator bean作为配置校验类;如果conversionService为空则获取工厂中的conversionService bean作为转换类
自身属性的输入校验:ConfigurationPropertiesBinding,注入的该类型bean必须由ConfigurationProperties注解配置

@Autowired(required = false)
@ConfigurationPropertiesBinding
public void setConverters(List<Converter<?, ?>> converters) {
	this.converters = converters;
}

@Autowired(required = false)
@ConfigurationPropertiesBinding
public void setGenericConverters(List<GenericConverter> converters) {
	this.genericConverters = converters;
}

bean初始化的前置后置动作

该类同时实现了BeanPostProcessor接口即在上下文bean初始化的前后增加了回调动作,在bean初始化后是空实现什么都没干,在bean初始化前的对bean进行前置的一些加工

  1. 如果bean存在ConfigurationProperties注解则使用该注解进行前置处理postProcessBeforeInitialization
  2. 如果ConfigurationBeanFactoryMetaData中存在当前bean名称的工厂方法定义并且存在ConfigurationProperties注解,则使用该注解进行前置处理

postProcessBeforeInitialization前置动作

  1. 创建配置工厂:PropertiesConfigurationFactory,目标target=bean
  2. 为配置工厂设置配置源、上下文、support支持当前bean的校验器Validator、转换服务。如果当前默认的转换服务(defaultConversionService)为空则初始化默认的转换服务,调用autowireBean注入依赖converters、genericConverters添加至defaultConversionService。设置其他注解属性、命名前缀(工厂的targeName属性)至工厂
// 默认转换器
converterRegistry.addConverterFactory(new NumberToNumberConverterFactory());
converterRegistry.addConverterFactory(new StringToNumberConverterFactory());
converterRegistry.addConverter(Number.class, String.class, new ObjectToStringConverter());
converterRegistry.addConverter(new StringToCharacterConverter());
converterRegistry.addConverter(Character.class, String.class, new ObjectToStringConverter());
converterRegistry.addConverter(new NumberToCharacterConverter());
converterRegistry.addConverterFactory(new CharacterToNumberFactory());
converterRegistry.addConverter(new StringToBooleanConverter());
converterRegistry.addConverter(Boolean.class, String.class, new ObjectToStringConverter());
converterRegistry.addConverterFactory(new StringToEnumConverterFactory());
converterRegistry.addConverter(new EnumToStringConverter((ConversionService) converterRegistry));
converterRegistry.addConverterFactory(new IntegerToEnumConverterFactory());
converterRegistry.addConverter(new EnumToIntegerConverter((ConversionService) converterRegistry));
converterRegistry.addConverter(new StringToLocaleConverter());
converterRegistry.addConverter(Locale.class, String.class, new ObjectToStringConverter());
converterRegistry.addConverter(new StringToCharsetConverter());
converterRegistry.addConverter(Charset.class, String.class, new ObjectToStringConverter());
converterRegistry.addConverter(new StringToCurrencyConverter());
converterRegistry.addConverter(Currency.class, String.class, new ObjectToStringConverter());
converterRegistry.addConverter(new StringToPropertiesConverter());
converterRegistry.addConverter(new PropertiesToStringConverter());
converterRegistry.addConverter(new StringToUUIDConverter());
converterRegistry.addConverter(UUID.class, String.class, new ObjectToStringConverter());
converterRegistry.addConverter(new ArrayToCollectionConverter(conversionService));
converterRegistry.addConverter(new CollectionToArrayConverter(conversionService));
converterRegistry.addConverter(new ArrayToArrayConverter(conversionService));
converterRegistry.addConverter(new CollectionToCollectionConverter(conversionService));
converterRegistry.addConverter(new MapToMapConverter(conversionService));
converterRegistry.addConverter(new ArrayToStringConverter(conversionService));
converterRegistry.addConverter(new StringToArrayConverter(conversionService));
converterRegistry.addConverter(new ArrayToObjectConverter(conversionService));
converterRegistry.addConverter(new ObjectToArrayConverter(conversionService));
converterRegistry.addConverter(new CollectionToStringConverter(conversionService));
converterRegistry.addConverter(new StringToCollectionConverter(conversionService));
converterRegistry.addConverter(new CollectionToObjectConverter(conversionService));
converterRegistry.addConverter(new ObjectToCollectionConverter(conversionService));
if (streamAvailable) {
	converterRegistry.addConverter(new StreamConverter(conversionService));
}
converterRegistry.addConverter(new ByteBufferConverter((ConversionService) converterRegistry));
if (jsr310Available) {
	Jsr310ConverterRegistrar.registerJsr310Converters(converterRegistry);
}

converterRegistry.addConverter(new ObjectToObjectConverter());
converterRegistry.addConverter(new IdToEntityConverter((ConversionService) converterRegistry));
converterRegistry.addConverter(new FallbackObjectToStringConverter());
if (javaUtilOptionalClassAvailable) {
	converterRegistry.addConverter(new ObjectToOptionalConverter((ConversionService) converterRegistry));
}
  1. 执行绑定配置至目标bean:bindPropertiesToTarget
  2. 创建数据绑定器:RelaxedDataBinder
  3. 配置数据绑定器
  4. 回调自定义钩子方法:customizeBinder
  5. 如果上下文不为空则注册自定义编辑器:ResourceEditorRegistrar.registerCustomEditors
// registry:RelaxedDataBinder
public void registerCustomEditors(PropertyEditorRegistry registry) {
	ResourceEditor baseEditor = new ResourceEditor(this.resourceLoader, this.propertyResolver);
	doRegisterEditor(registry, Resource.class, baseEditor);
	doRegisterEditor(registry, ContextResource.class, baseEditor);
	doRegisterEditor(registry, InputStream.class, new InputStreamEditor(baseEditor));
	doRegisterEditor(registry, InputSource.class, new InputSourceEditor(baseEditor));
	doRegisterEditor(registry, File.class, new FileEditor(baseEditor));
	if (pathClass != null) {
		doRegisterEditor(registry, pathClass, new PathEditor(baseEditor));
	}
	doRegisterEditor(registry, Reader.class, new ReaderEditor(baseEditor));
	doRegisterEditor(registry, URL.class, new URLEditor(baseEditor));

	ClassLoader classLoader = this.resourceLoader.getClassLoader();
	doRegisterEditor(registry, URI.class, new URIEditor(classLoader));
	doRegisterEditor(registry, Class.class, new ClassEditor(classLoader));
	doRegisterEditor(registry, Class[].class, new ClassArrayEditor(classLoader));

	if (this.resourceLoader instanceof ResourcePatternResolver) {
		doRegisterEditor(registry, Resource[].class,
				new ResourceArrayPropertyEditor((ResourcePatternResolver) this.resourceLoader, this.propertyResolver));
	}
}
  1. 根据targetName创建RelaxedNames实例
  2. 遍历bean属性描述符PropertyDescriptor,根据relaxNames(即:prefixes)获取所有属性名称,将驼峰命名初始化为“-”横线分隔的全小写名称。然后按照后面两种规则拼接后返回所有场景的名称集合:前缀+"."+relaxedName,前缀+"_"+relaxedName。
  3. relaxedName场景如下
1. 初始值
2. HYPHEN_TO_UNDERSCORE:横线转下划线
3. UNDERSCORE_TO_PERIOD:下划线转点(.4. PERIOD_TO_UNDERSCORE:点转下划线
5. CAMELCASE_TO_UNDERSCORE:驼峰转下划线
6. CAMELCASE_TO_HYPHEN:驼峰转横线
7. SEPARATED_TO_CAMELCASE:横线、点、下划线转驼峰
8. CASE_INSENSITIVE_SEPARATED_TO_CAMELCASE:横线、点、下划线转驼峰(忽略大小写,全部转为小写)
  1. 基于relaxedName拆分重组后的names案例如下
0 = "rocketmq.consume-check-type"
1 = "rocketmq_consume-check-type"
2 = "rocketmq.consume_check_type"
3 = "rocketmq_consume_check_type"
4 = "rocketmq.consumeCheckType"
5 = "rocketmq_consumeCheckType"
6 = "rocketmq.consumechecktype"
7 = "rocketmq_consumechecktype"
8 = "rocketmq.CONSUME-CHECK-TYPE"
9 = "rocketmq_CONSUME-CHECK-TYPE"
10 = "rocketmq.CONSUME_CHECK_TYPE"
11 = "rocketmq_CONSUME_CHECK_TYPE"
12 = "rocketmq.CONSUMECHECKTYPE"
13 = "rocketmq_CONSUMECHECKTYPE"
14 = "ROCKETMQ.consume-check-type"
15 = "ROCKETMQ_consume-check-type"
16 = "ROCKETMQ.consume_check_type"
17 = "ROCKETMQ_consume_check_type"
18 = "ROCKETMQ.consumeCheckType"
19 = "ROCKETMQ_consumeCheckType"
20 = "ROCKETMQ.consumechecktype"
21 = "ROCKETMQ_consumechecktype"
22 = "ROCKETMQ.CONSUME-CHECK-TYPE"
23 = "ROCKETMQ_CONSUME-CHECK-TYPE"
24 = "ROCKETMQ.CONSUME_CHECK_TYPE"
25 = "ROCKETMQ_CONSUME_CHECK_TYPE"
26 = "ROCKETMQ.CONSUMECHECKTYPE"
27 = "ROCKETMQ_CONSUMECHECKTYPE"
28 = "rocketmq.consume-thread-max"
29 = "rocketmq_consume-thread-max"
30 = "rocketmq.consume_thread_max"
31 = "rocketmq_consume_thread_max"
32 = "rocketmq.consumeThreadMax"
33 = "rocketmq_consumeThreadMax"
34 = "rocketmq.consumethreadmax"
35 = "rocketmq_consumethreadmax"
36 = "rocketmq.CONSUME-THREAD-MAX"
37 = "rocketmq_CONSUME-THREAD-MAX"
38 = "rocketmq.CONSUME_THREAD_MAX"
39 = "rocketmq_CONSUME_THREAD_MAX"
40 = "rocketmq.CONSUMETHREADMAX"
41 = "rocketmq_CONSUMETHREADMAX"
42 = "ROCKETMQ.consume-thread-max"
43 = "ROCKETMQ_consume-thread-max"
44 = "ROCKETMQ.consume_thread_max"
45 = "ROCKETMQ_consume_thread_max"
46 = "ROCKETMQ.consumeThreadMax"
47 = "ROCKETMQ_consumeThreadMax"
48 = "ROCKETMQ.consumethreadmax"
49 = "ROCKETMQ_consumethreadmax"
50 = "ROCKETMQ.CONSUME-THREAD-MAX"
51 = "ROCKETMQ_CONSUME-THREAD-MAX"
52 = "ROCKETMQ.CONSUME_THREAD_MAX"
53 = "ROCKETMQ_CONSUME_THREAD_MAX"
54 = "ROCKETMQ.CONSUMETHREADMAX"
55 = "ROCKETMQ_CONSUMETHREADMAX"
56 = "rocketmq.consume-thread-min"
57 = "rocketmq_consume-thread-min"
58 = "rocketmq.consume_thread_min"
59 = "rocketmq_consume_thread_min"
60 = "rocketmq.consumeThreadMin"
61 = "rocketmq_consumeThreadMin"
62 = "rocketmq.consumethreadmin"
63 = "rocketmq_consumethreadmin"
64 = "rocketmq.CONSUME-THREAD-MIN"
65 = "rocketmq_CONSUME-THREAD-MIN"
66 = "rocketmq.CONSUME_THREAD_MIN"
67 = "rocketmq_CONSUME_THREAD_MIN"
68 = "rocketmq.CONSUMETHREADMIN"
69 = "rocketmq_CONSUMETHREADMIN"
70 = "ROCKETMQ.consume-thread-min"
71 = "ROCKETMQ_consume-thread-min"
72 = "ROCKETMQ.consume_thread_min"
73 = "ROCKETMQ_consume_thread_min"
74 = "ROCKETMQ.consumeThreadMin"
75 = "ROCKETMQ_consumeThreadMin"
76 = "ROCKETMQ.consumethreadmin"
77 = "ROCKETMQ_consumethreadmin"
78 = "ROCKETMQ.CONSUME-THREAD-MIN"
79 = "ROCKETMQ_CONSUME-THREAD-MIN"
80 = "ROCKETMQ.CONSUME_THREAD_MIN"
81 = "ROCKETMQ_CONSUME_THREAD_MIN"
82 = "ROCKETMQ.CONSUMETHREADMIN"
83 = "ROCKETMQ_CONSUMETHREADMIN"
84 = "rocketmq.consumer-group-name"
85 = "rocketmq_consumer-group-name"
86 = "rocketmq.consumer_group_name"
87 = "rocketmq_consumer_group_name"
88 = "rocketmq.consumerGroupName"
89 = "rocketmq_consumerGroupName"
90 = "rocketmq.consumergroupname"
91 = "rocketmq_consumergroupname"
92 = "rocketmq.CONSUMER-GROUP-NAME"
93 = "rocketmq_CONSUMER-GROUP-NAME"
94 = "rocketmq.CONSUMER_GROUP_NAME"
95 = "rocketmq_CONSUMER_GROUP_NAME"
96 = "rocketmq.CONSUMERGROUPNAME"
97 = "rocketmq_CONSUMERGROUPNAME"
98 = "ROCKETMQ.consumer-group-name"
99 = "ROCKETMQ_consumer-group-name"
100 = "ROCKETMQ.consumer_group_name"
101 = "ROCKETMQ_consumer_group_name"
102 = "ROCKETMQ.consumerGroupName"
103 = "ROCKETMQ_consumerGroupName"
104 = "ROCKETMQ.consumergroupname"
105 = "ROCKETMQ_consumergroupname"
106 = "ROCKETMQ.CONSUMER-GROUP-NAME"
107 = "ROCKETMQ_CONSUMER-GROUP-NAME"
108 = "ROCKETMQ.CONSUMER_GROUP_NAME"
109 = "ROCKETMQ_CONSUMER_GROUP_NAME"
110 = "ROCKETMQ.CONSUMERGROUPNAME"
111 = "ROCKETMQ_CONSUMERGROUPNAME"
112 = "rocketmq.name-server-addr"
113 = "rocketmq_name-server-addr"
114 = "rocketmq.name_server_addr"
115 = "rocketmq_name_server_addr"
116 = "rocketmq.nameServerAddr"
117 = "rocketmq_nameServerAddr"
118 = "rocketmq.nameserveraddr"
119 = "rocketmq_nameserveraddr"
120 = "rocketmq.NAME-SERVER-ADDR"
121 = "rocketmq_NAME-SERVER-ADDR"
122 = "rocketmq.NAME_SERVER_ADDR"
123 = "rocketmq_NAME_SERVER_ADDR"
124 = "rocketmq.NAMESERVERADDR"
125 = "rocketmq_NAMESERVERADDR"
126 = "ROCKETMQ.name-server-addr"
127 = "ROCKETMQ_name-server-addr"
128 = "ROCKETMQ.name_server_addr"
129 = "ROCKETMQ_name_server_addr"
130 = "ROCKETMQ.nameServerAddr"
131 = "ROCKETMQ_nameServerAddr"
132 = "ROCKETMQ.nameserveraddr"
133 = "ROCKETMQ_nameserveraddr"
134 = "ROCKETMQ.NAME-SERVER-ADDR"
135 = "ROCKETMQ_NAME-SERVER-ADDR"
136 = "ROCKETMQ.NAME_SERVER_ADDR"
137 = "ROCKETMQ_NAME_SERVER_ADDR"
138 = "ROCKETMQ.NAMESERVERADDR"
139 = "ROCKETMQ_NAMESERVERADDR"
140 = "rocketmq.subscribes"
141 = "rocketmq_subscribes"
142 = "rocketmq.SUBSCRIBES"
143 = "rocketmq_SUBSCRIBES"
144 = "ROCKETMQ.subscribes"
145 = "ROCKETMQ_subscribes"
146 = "ROCKETMQ.SUBSCRIBES"
147 = "ROCKETMQ_SUBSCRIBES"
  1. 获取属性源属性值:getPropertySourcesPropertyValues,返回PropertyValues
  2. 如果ignoreUnknownFields=true并且target不是map子类使用_EXACT_DELIMITERS = { '’, ‘.’, ‘[’ }字符数组及names构建DefaultPropertyNamePatternsMatcher,否则如果relaxedTargetNames不为空则使用_TARGET_NAME_DELIMITERS = { '’, ‘.’ }字符数组及relaxedTargetNames构建匹配器,兜底为全匹配。
  3. 根据匹配器创建PropertySourcesPropertyValues

PropertySourcesPropertyValues

构造器中遍历处理配置源:processPropertySource
1

  1. 如果是组合类型的配置源则遍历处理所有组合配置源:processCompositePropertySource
  2. 如果是枚举类型的配置源则直接获取:processEnumerablePropertySource
  3. 否则:processNonEnumerablePropertySource

2
我们常用的是第二种。暂只关心第二种的处理方式

processEnumerablePropertySource

遍历source的所有属性名,即MapPropertySource.getPropertyNames,返回properties的keySet3
如果匹配器匹配到配置源的属性名则获取配置value,构建属性值并缓存

// Pattern COLLECTION_PROPERTY = Pattern.compile("\\[(\\d+)\\](\\.\\S+)?");
// COLLECTION_PROPERTY即集合类型的属性,例如:
// spring.diamonds[0].data-id=mydata0
// spring.diamonds[0].group-id=mygroup0
// spring.diamonds[1].data-id=mydata1
// spring.diamonds[1].group-id=mygroup1
private PropertyValue putIfAbsent(String propertyName, Object value,
		PropertySource<?> source) {
	if (value != null && !this.propertyValues.containsKey(propertyName)) {
		PropertySource<?> collectionOwner = this.collectionOwners.putIfAbsent(
				COLLECTION_PROPERTY.matcher(propertyName).replaceAll("[]"), source);
		if (collectionOwner == null || collectionOwner == source) {
			PropertyValue propertyValue = new OriginCapablePropertyValue(propertyName,
					value, propertyName, source);
			this.propertyValues.put(propertyName, propertyValue);
			return propertyValue;
		}
	}
	return null;
}

RelaxedDataBinder

  1. 执行绑定:RelaxedDataBinder.bind,如果PropertyValues是MutablePropertyValues类型则直接转换,否则封装为MutablePropertyValues类型
  2. 根据target创建BeanWrapperImpl,设置转换服务:RelaxedConversionService,设置setAutoGrowNestedPaths=true
  3. 修改配置RelaxedDataBinder.modifyProperties
  4. 剔除配置MutablePropertyValues前缀:getPropertyValuesForNamePrefix
  5. 属性名称排序:getSortedPropertyNames。将属性名路径封装为BeanPath,路径被切分为node缓存至路径的nodes属性。保证最左匹配,例如:‘foo.bar’ 在 'foo.bar.spam’前面
com.gallant[0].user.[id]=1;
com.gallant[1].user.[name]=superman;
上面的配置属性
6. com,gallant,user:PropertyNode类型节点
7. [0]:ArrayIndexNode类型节点
8. [id]:MapIndexNode类型节点
  1. 将target bean封装为BeanWrapperImpl,遍历配置源所有属性配置,修改属性RelaxedDataBinder.modifyProperty。返回修改后的属性值(对属性名称进行规范化处理):PropertyValue
  2. 规范化路径(即属性的全路径名称):RelaxedDataBinder.normalizePath,简单讲:如果属性匹配到target的字段则保持原名称,否则第一个段(一个配置路径被dot分隔n个段)保持原名称,其他段使用中括号包装。规范化后的值如下图:
    11.4
  3. 如果规范化后的名称与属性名不一致,则根据规范化后的名称与属性值重新创建PropertyValue,否则直接返回原属性值propertyValue
  4. 规范化排序后的匹配前缀的属性值
  5. 5
  6. 校验是否允许绑定:RelaxedDataBinder.DataBinder.checkAllowedFields
  7. 校验必须的字段:RelaxedDataBinder.DataBinder.checkRequiredFields,如果是必须字段并且没有找到匹配的配置则添加字段错误:FieldError,绑定完成后会校验异常,默认会抛出:BindException异常
  8. 执行属性绑定:RelaxedDataBinder.DataBinder.applyPropertyValues
  9. 获取属性访问器:getPropertyAccessor,调用内部绑定结果获取访问器,Bean属性绑定结果:BeanPropertyBindingResult,创建并返回bean属性访问器RelaxedDataBinder.createBeanWrapper:RelaxedBeanWrapper
  10. 注入设置属性:RelaxedBeanWrapper.setPropertyValue。调用父类AbstractNestablePropertyAccessor.setPropertyValue。如果属性值PropertyValue不存在tokens则获取getPropertyNameTokens:非中括号部分为实际属性名称、canonicalName绝对路径,keys为中括号内容,如下图
  11. 6
  12. 调用含tokens入参setPropertyValue方法,如果tokens的keys(keys为属性的中括号中的内容(含)后面的dot分隔的值,例如:com.gallant[id].user,对应的keys为id,user,对应的最终路径是com.gallant[id][user]。案例见下图,配置为:spring.ons.producer.mq-type=METAQ)为空则调用processLocalProperty
  13. 7
  14. 如果需要转换属性类型则调用转换AbstractNestablePropertyAccessor.convertForProperty,例如:StringToArrayConverter转换器,可以看到转换器中是按照逗号切分配置进行转换的
  15. 将转换后的value注入:BeanWrapperImpl.BeanPropertyHandler.setValue(this.wrappedObject, valueToApply);

8
9
10

问题

为什么要注册value中指定的BeanDefinition至上下文?

当然是为了上下文中可以直接注入配置bean使用配置喽

缓存存在工厂方法的bean定义的用处是什么?

因为对工厂方法定义的bean也支持属性配置的注入,如果工厂方法bean的定义上存在ConfigurationProperties注解,则该bean也可以直接使用配置值

总结

  1. com.gallant[0]=1,com.gallant[1]=99,默认对应gallant数组,下标0为1,下标1为99
  2. 下面案例则对应数组diamonds,数组元素为一个java对象,对象包含两个字段dataId,groupId,与之对应
spring.diamonds[0].data-id=ddd
spring.diamonds[0].group-id=111
spring.diamonds[1].data-id=eee
spring.diamonds[1].group-id=222
  1. 下面案例则对应map类型的diamonds,key=dataId对应value=d1,key=groupId对应value=g1
spring.diamonds['dataId']=d1
spring.diamonds['groupId']=g1
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值