10. Spring 配置元信息

10.1 Spring 配置元信息

  • Spring Bean 配置元信息——BeanDefinition

  • Spring Bean 属性元信息——PropertyValues

  • Spring 容器配置元信息

  • Spring 外部化配置元信息+PropertySource

  • Spring Profile 元信息——@Profile

10.2 Spring Bean 配置元信息

  • GenericBeanDefinition:通用型 BeanDefinition

  • RootBeanDefinition:无 Parent 的 BeanDefinition 或者合并后的 BeanDefinition

  • AnnotatedBeanDefinition:注解标注的 BeanDefinition

GenericBeanDefinition 和 RootBeanDefinition 都继承了 AbstractBeanDefinition,AbstractBeanDefinition 提供了对于 Definiton 的写操作,GenericBeanDefinition 简单重写了 setParent 方法,RootBeanDefinition 则增加了许多字段(IoC 中 BeanDefitnition 通常需要 merge,merge 后的 BeanDefinition 就是 RootBeanDefinition,也叫做 mbd),这些字段用于创建 Bean 时提供一些辅助性操作(提升性能等)

DefaultListableBeanFactory 中存储所有 BeanDefinition,AbstractBeanDefinition 中存储所有 mergedBeanDefinition。

public interface AnnotatedBeanDefinition extends BeanDefinition {
​
    /**
     * Obtain the annotation metadata (as well as basic class metadata)
     * for this bean definition's bean class.
     * @return the annotation metadata object (never {@code null})
     */
    AnnotationMetadata getMetadata();
​
    /**
     * Obtain metadata for this bean definition's factory method, if any.
     * @return the factory method metadata, or {@code null} if none
     * @since 4.1.1
     */
    @Nullable
    MethodMetadata getFactoryMethodMetadata();
​
}

AnnotatedBeanDefinition 可以通过反射或者 ASM 方式获取 AnnotationMetadata,Spring 5.2 及以后 ASM 方式标记过时,新增 SimpleAnnotationMetadataReadingVisitor 类使用

getFactoryMethodMetadata 元数据是否来自某个方法,比如 Factory 中方法,可有可无,因为并不是每个 Bean 都是通过 Factory method 来生成的

10.3 Spring Bean 属性元信息

  • Bean 属性元信息——PropertyValues

    • 可修改实现——MutablePropertyValues

    • 元素成员——PropertyValue

  • Bean 属性上下文存储——AttributeAccessor

  • Bean 元信息元素——BeanMetadataElement

属性元信息中后两个属性上下文存储以及 Bean 元信息元素两种元信息都是起辅助作用的,上下文信息可以在创建 Bean 的过程中进行使用,元信息元素可以标记 BeanDefinition 的来源,具体使用可参照下例:

public class BeanConfigurationMetadataDemo {
    public static void main(String[] args) {
        BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(User.class);
        builder.addPropertyValue("name", "bar");
​
        // 获取 AbstractBeanDefinition
        AbstractBeanDefinition definition = builder.getBeanDefinition();
        // 附加属性(不影响 Bean populate、initialize)
        definition.setAttribute("name", "foo");
        // 当前 BeanDefinition 来自哪里,辅助作用
        definition.setSource(BeanConfigurationMetadataDemo.class);
​
        DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
        factory.registerBeanDefinition("user", definition);
        factory.addBeanPostProcessor(new BeanPostProcessor() {
            @Override
            public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
                if (bean instanceof User && beanName.equals("user")) {
                    User user = (User) bean;
                    BeanDefinition beanDefinition = factory.getBeanDefinition("user");
                    if (beanDefinition.getSource() == BeanConfigurationMetadataDemo.class) {
                        String name = (String) beanDefinition.getAttribute("name");
                        user.setName(name);
                        return user;
                    }
                }
                return bean;
            }
        });
​
        System.out.println(factory.getBean("user", User.class));
    }
}
​
// 运行结果:
User{name='foo', age=0}

10.4 Spring 容器配置元信息

Spring XML 配置元信息——beans 元素相关

beans 元素属性默认值使用场景
profilenullSpring Profile 配置值
default-lazy-initdefault当 outter beans "default-lazy-init" 属性存在时,继承该值,否则为"false"
default-mergedeafult当 outter beans "default-merge" 属性存在时,继承该值,否则为"false"
default-autowiredefault当 outter beans "default-autowire" 属性存在时,继承该值,否则为"false"
default-autowire-candidatesnull默认 Spring Beans 名称 pattern
default-init-methodnull默认 Spring Beans 自定义初始化方法
default-destroy-methodnull默认 Spring Beans 自定义销毁方法

上面这些属性,都是在 XML 的 beans 标签中设置的

Spring XML 配置元信息——应用上下文相关

XML 元素使用场景
<context:annotation-config/>激活 Spring 注解驱动
<context:component-scan/>Spring @Component 以及自定义注解扫描
<context:load-time-weaver/>激活 Spring LoadTimeWeaver
<context:mbean-export/>暴露 Spring Beans 作为 JMX Beans
<context:mbean-server/>将当前平台作为 MBeanServer
<context:property-placeholder/>加载外部化配置资源作为 Spring 属性配置
<context:property-override/>利用外部化配置资源覆盖 Spring 属性值

可以在 BeanDefinitionParserDelegate 中查看其解析规则

10.5 基于 XML 文件装载 Spring Bean 配置元信息

Spring Bean 配置元信息

XML 元素使用场景
<beans:beans>单 XML 资源下的多个 Spring Beans 配置
<beans:bean>单个 Spring Bean 定义(BeanDefinition)配置
<beans:alias>为 Spring Bean 定义(BeanDefinition)映射别名
<beans:import>加载外部 Spring XML 资源配置

底层实现:XmlBeanDefinitionReader

public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
    // ...
    // 从输入流注册 BeanDefinition
    return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
    // ...
}
​
protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
            throws BeanDefinitionStoreException {
    // ...
    // 获取 DOM 操作需要的 Document 元素
    Document doc = doLoadDocument(inputSource, resource);
    // 执行注册
    int count = registerBeanDefinitions(doc, resource);
    // ...
}
​
public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
    // 创建 DocumentReader
    BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
    // 保存现有 BeanDefinition 数量
    int countBefore = getRegistry().getBeanDefinitionCount();
    // 注册 BeanDefinitions
    documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
    // 返回注册 BeanDefinition 数量
    return getRegistry().getBeanDefinitionCount() - countBefore;
}
​
​
@Override
public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {
    this.readerContext = readerContext;
    doRegisterBeanDefinitions(doc.getDocumentElement());
}
​
protected void doRegisterBeanDefinitions(Element root) {
    // Any nested <beans> elements will cause recursion in this method. In
    // order to propagate and preserve <beans> default-* attributes correctly,
    // keep track of the current (parent) delegate, which may be null. Create
    // the new (child) delegate with a reference to the parent for fallback purposes,
    // then ultimately reset this.delegate back to its original (parent) reference.
    // this behavior emulates a stack of delegates without actually necessitating one.
    BeanDefinitionParserDelegate parent = this.delegate;
    this.delegate = createDelegate(getReaderContext(), root, parent);
​
    if (this.delegate.isDefaultNamespace(root)) {
        // 获取设置的 profiles 跟 环境变量中 profile 进行比较,相同才往后执行,不同就注解返回不解析
        String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
        if (StringUtils.hasText(profileSpec)) {
            String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
                profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
            // We cannot use Profiles.of(...) since profile expressions are not supported
            // in XML config. See SPR-12458 for details.
            if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Skipped XML bean definition file due to specified profiles [" + profileSpec +
                                 "] not matching: " + getReaderContext().getResource());
                }
                return;
            }
        }
    }
​
    preProcessXml(root);
    // 真正解析的地方
    parseBeanDefinitions(root, this.delegate);
    postProcessXml(root);
​
    this.delegate = parent;
}
​
// XML -> BeanDefinition
protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
    if (delegate.isDefaultNamespace(root)) {
        NodeList nl = root.getChildNodes();
        for (int i = 0; i < nl.getLength(); i++) {
            Node node = nl.item(i);
            if (node instanceof Element) {
                Element ele = (Element) node;
                if (delegate.isDefaultNamespace(ele)) {
                    parseDefaultElement(ele, delegate);
                }
                else {
                    delegate.parseCustomElement(ele);
                }
            }
        }
    }
    else {
        delegate.parseCustomElement(root);
    }
}
​
// Spring 默认解析的元素
private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {
    if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) {
        importBeanDefinitionResource(ele);
    }
    else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) {
        processAliasRegistration(ele);
    }
    else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {
        processBeanDefinition(ele, delegate);
    }
    else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) {
        // recurse
        doRegisterBeanDefinitions(ele);
    }
}
​
// 自定义 XML 解析
@Nullable
public BeanDefinition parseCustomElement(Element ele) {
    return parseCustomElement(ele, null);
}

10.6 基于 Properties 文件装载 Spring Bean 配置元信息(不推荐)

Spring Bean 配置元信息

Properties 属性名使用场景
(class)Bean 类全限定名
(abstract)是否为抽象的 BeanDefinition
(parent)指定 parent BeanDefinition 名称
(lazy-init)是否为延迟初始化
(ref)引用其他 Bean 的名称
(scope)设置 Bean 的 scope 属性
$nn 表示第 n+1 个构造器参数

底层实现:PropertiesBeanDefinitionReader

不推荐使用,配置很复杂并且局限性很大

10.7 基于 Java 注解装载 Spring Bean 配置元信息

Spring 模式注解

Spring 注解场景说明起始版本
@Repository数据仓储模式注解2.0
@Component通用组件模式注解2.5
@Service服务模式注解2.5
@ControllerWeb 控制器模式注解2.5
@Configuration配置类模式注解3.0

Spring Bean 依赖注入注解

Spring 注解场景说明起始版本
@AutowiredBean 依赖注入,支持多种依赖查找方式2.5
@Qualifier细粒度的 @Autowired 依赖查找2.5
Java 注解场景说明起始版本
@Resource类似于 @Autowired2.5
@Inject类似于 @Autowired2.5

AutowiredAnnotationBeanPostProcessor 处理 @Autowired、@Value、@Inject 注解

CommonAnnotationBeanPostProcessor 处理 @Resource 注解,javax.xml.ws.WebServiceRef注解、javax.ejb.EJB,主要处理 j2se 或者 Java EE 相关的注解或者 API

Spring Bean 条件装配注解

Spring 注解场景说明起始版本
@Profile配置化条件装配3.1
@Conditional编程条件装配4.0

@Profile 通过 @Conditional 实现

实现 Condition 接口

class ProfileCondition implements Condition {
	@Override
	public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        // 读取注解所有的信息
		MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
		if (attrs != null) {
			for (Object value : attrs.get("value")) {
				if (context.getEnvironment().acceptsProfiles(Profiles.of((String[]) value))) {
					return true;
				}
			}
			return false;
		}
		return true;
	}
}

Spring Bean 生命周期回调注解

Spring 注解场景说明起始版本
@PostConstructor替换 XML 元素 <bean init-method="..."/> 或 InitializingBean2.5
@PreDestroy替换 XML 元素 <bean destroy-method="..."/> 或 DisposableBean2.5

CommonAnnotationBeanPostProcessor 中处理这两个注解

10.8 Spring Bean 配置元信息底层实现

Spring BeanDefinition 解析与注册

实现场景实现类起始版本
XML 资源XmlBeanDefinitionReader1.0
Properties 资源PropertiesBeanDefinitionReader1.0
Java 注解AnnotatedBeanDefinitionReader3.0

Spring 注解驱动主要开始于 Spring 2.5 版本

Java 注解和类相关,并不和资源相关,因此 AnnotatedBeanDefinitionReader 和 BeanDefinitionReader 这个接口是没有关系的,因为 BeanDenitionReader 是基于资源(Resource)的,XmlBeanDefinitionReader 和 PropertiesBeanDefinitionReader 都属于 BeanDenitionReader 实现

10.8.1 Spring XML 资源 BeanDefinition 解析与注册

核心 API——XmlBeanDefinitionReader

  • 资源——Resource

  • 底层——BeanDefinitionDocumentReader

    • XML 解析——Java DOM Level 3 API

    • BeanDefinition 解析——BeanDefinitionParserDelegate

    • BeanDefinition 注册——BeanDefinitionRegistrar

10.8.2 Spring Properties 资源 BeanDefinition 解析与注册

核心 API——PropertiesBeanDefinitionReader

  • 资源

    • 字节流——Resource

    • 字符流——EncodedResource

  • 底层

    • 存储——java.util.Properties

    • BeanDefinition 解析——API 内部实现

    • BeanDefinition 注册——BeanDefinitionRegistry

10.8.3 Spring Java 注册 BeanDefinition 解析与注册

核心 API——AnnotatedBeanDefinitionReader

  • 资源——类对象 java.lang.Class

  • 底层

    • 条件评估——ConditionEvaluattor

    • Bean 范围解析——ScopeMetadataResolver

    • BeanDefinition 解析——内部 API 实现

    • BeanDefinition 处理——AnnotationConfigUtils.processCommonDefinitionAnnotations

    • BeanDefinition 注册——BeanDefinitionRegistry

public class AnnotatedBeanDefinitionReader {

    // Bean 注册容器
    private final BeanDefinitionRegistry registry;

    // Bean 名称生成器
    private BeanNameGenerator beanNameGenerator = AnnotationBeanNameGenerator.INSTANCE;

    // Scope 解析器,是否单例以及代理模式
    private ScopeMetadataResolver scopeMetadataResolver = new AnnotationScopeMetadataResolver();

    // 评估器,评估是否需要跳过注册
    private ConditionEvaluator conditionEvaluator;
    // ...
}

10.9 基于 XML 文件装载 Spring IoC 容器配置元信息

spring.handlers 文件中配置解析命名空间的 handler

http\://www.springframework.org/schema/util=org.springframework.beans.factory.xml.UtilNamespaceHandler

spring.schema 文件中配置命名空间对应的 xsd 文件

http\://www.springframework.org/schema/beans/spring-beans.xsd=org/springframework/beans/factory/xml/spring-beans.xsd

handler 继承 NamespaceHandlerSupport,重写 init 方法,init 方法内部注册具体的解析类

public class UtilNamespaceHandler extends NamespaceHandlerSupport {

	private static final String SCOPE_ATTRIBUTE = "scope";


	@Override
	public void init() {
        // 注册解析 list、map 等的 parser 类
		registerBeanDefinitionParser("constant", new ConstantBeanDefinitionParser());
		registerBeanDefinitionParser("property-path", new PropertyPathBeanDefinitionParser());
		registerBeanDefinitionParser("list", new ListBeanDefinitionParser());
		registerBeanDefinitionParser("set", new SetBeanDefinitionParser());
		registerBeanDefinitionParser("map", new MapBeanDefinitionParser());
		registerBeanDefinitionParser("properties", new PropertiesBeanDefinitionParser());
	}
    // ...
}

解析类继承 AbstractSingleBeanDefinitionParser,重写 doParse 方法

private static class ListBeanDefinitionParser extends AbstractSingleBeanDefinitionParser {

		@Override
		protected Class<?> getBeanClass(Element element) {
			return ListFactoryBean.class;
		}

		@Override
		protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) {
			List<Object> parsedList = parserContext.getDelegate().parseListElement(element, builder.getRawBeanDefinition());
			builder.addPropertyValue("sourceList", parsedList);

			String listClass = element.getAttribute("list-class");
			if (StringUtils.hasText(listClass)) {
				builder.addPropertyValue("targetListClass", listClass);
			}

			String scope = element.getAttribute(SCOPE_ATTRIBUTE);
			if (StringUtils.hasLength(scope)) {
				builder.setScope(scope);
			}
		}
	}

此处解析完成,BeanDefinitionBuilder 属性也设置完成,返回 AbstractBeanDefinitionParser#parse 方法将结果封装成 BeanDefinitionHolder 进行注册。整体流程如下:

XmlBeanDefinitionReader#loadBeanDefinition(Resource) -> doLoadBeanDefinitions() -> doLoadDocument() -> registerBeanDefinitions() -> 
DefaultBeanDefinitionDocumentReader#registerBeanDefinitions() -> doRegisterBeanDefinitions() -> parseBeanDefinitions() -> 
BeanDefinitionParserDelegate#parseCustomElement() -> 根据 namespaceUri 获取 handler -> 
NamespaceHandlerSupport#parse -> 查找 parser -> 
BeanDefinitionParser#parse()(此处将parseInternal 解析完成的 BeanDefinition 封装成 BeanDefinitionHolder 进行注册) -> parseInternal() -> doParse()(子类重写的方法,解析逻辑在此实现)

parseCustonElement 流程:

  • 获取 namespace

  • 通过 namespace 获取 handler

  • 构造 ParserContext

  • 解析元素,获取 BeanDefinition

10.10 基于 Java 注解装载 Spring IoC 容器配置元信息

10.10.1 Spring IoC 容器装配注解

Spring 注解场景说明起始版本
@ImportResource替换 XML 元素 <import>3.0
@Import导入 Configuration Class3.0
@ComponentScan扫描指定 package 下标注 Spring 模式注解的类3.1

@ImportResource 导入 xml 文件,@Import 导入 Java 类(配置类或者普通类)

10.10.2 Spring IoC 属性配置注解

Spring 注解场景说明起始版本
@PropertySource配置属性抽象 PropertySource3.1
@PropertySources@PropertySource 集合注解4.0

从 Java 8 开始,一个类上支持同时使用多个 @PropertySource 注解

10.11 基于 Extensible XML authoring 扩展原理

扩展步骤:

  • 编写 XML Schema 文件:定义 XML 结构

  • 自定义 NamespaceHandler 实现:命名空间绑定

  • 自定义 BeanDefinitionParser 实现:XML 元素与 BeanDefinition 解析

  • 注册 XML 扩展:命名空间与 XML Schema 映射

10.11.1 定义 xsd

<?xml version="1.0" encoding="UTF-8" standalone="no"?>

<xsd:schema xmlns="http://gcl.com/schema/users"
            xmlns:xsd="http://www.w3.org/2001/XMLSchema"
            targetNamespace="http://gcl.com/schema/users">

    <xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>

    <xsd:complexType name="User">
        <xsd:attribute name="id" type="xsd:long" use="required"/>
        <xsd:attribute name="name" type="xsd:string" use="required"/>
        <xsd:attribute name="age" type="xsd:int"/>
    </xsd:complexType>

    <xsd:element name="user" type="User"/>
</xsd:schema>

10.11.2 定义 handler

public class CustomHandler extends NamespaceHandlerSupport {
    @Override
    public void init() {
        registerBeanDefinitionParser("user", new CustomParser());
    }
}
http\://gcl.com/schema/users=com.gcl.configuration.metadata.handler.CustomHandler

10.11.3 定义 parser

public class CustomParser extends AbstractSingleBeanDefinitionParser {
​
    @Override
    protected Class<?> getBeanClass(Element element) {
        return User.class;
    }
​
    @Override
    protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) {
        setAttribute("name", element, builder);
        setAttribute("age", element, builder);
    }
​
    private void setAttribute(String attributeName, Element element, BeanDefinitionBuilder builder) {
        String attribute = element.getAttribute(attributeName);
        if (StringUtils.hasText(attribute)) {
            builder.addPropertyValue(attributeName, attribute);
        }
    }
}

10.11.4 注册 xml 扩展

http\://gcl.com/schema/users.xsd=META-INF/com/gcl/configuration/metadata/users.xsd

10.11.5 测试

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:users="http://gcl.com/schema/users"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       https://www.springframework.org/schema/beans/spring-beans.xsd
       http://gcl.com/schema/users
       http://gcl.com/schema/users.xsd" >
    <users:user id="1" name="foo" age="18"/>
</beans>
public class CustomXmlParserDemo {
    public static void main(String[] args) {
        DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
        XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);
        reader.loadBeanDefinitions("classpath:META-INF/users.xml");
        System.out.println(factory.getBean(User.class));
    }
}
​
// 运行结果:
User{name='foo', age=18}

10.12 基于 Properties 资源装载外部化配置

  • 注解驱动

    • @org.springframework.context.annotation.PropertySource

    • @org.springframework.context.annotation.PropertySources

  • API 编程

    • org.springframework.core.env.PropertySource

    • org.springframework.core.env.PropertySources

Java 8 支持重复注解功能,也就是支持一个类上重复使用相同的注解,因此可以使用多个 @PropertySource 替换 @PropertySources 注解

10.13 基于 YAML 资源装载外部化配置

API 编程

  • org.springframework.beans.factory.config.YamlProcessor

    • org.springframework.beans.factory.config.YamlMapFactoryBean

    • org.springframework.beans.factory.config.YamlPropertiesFactoryBean

10.14 面试题

10.14.1 Spring 内建 XML Schema 常见有哪些

命名空间所属模块Schema 资源 URL
beansspring-beanshttp://www.springframework.org/schema/beans/spring-beans.xsd
contextspring-contexthttp://www.springframework.org/schema/context/spring-context.xsd
aopspring-aophttp://www.springframework.org/schema/sop/spring-aop.xsd
txspring-txhttp://www.springframework.org/schema/tx/spring-tx.xsd
utilspring-beanshttp://www.springframework.org/schema/util/spring-util.xsd
toolspring-beanshttp://www.springframework.org/schema/tool/spring-tool.xsd

10.14.2 Spring 配置元信息具体有哪些

  • Bean 配置元信息:通过媒介(如 XML、Properties等),解析 BeanDefinition

  • IoC 容器配置元信息:通过媒介(如:XML、Properties等),控制 IoC 容器行为,比如注解驱动、AOP等

  • 外部化配置:通过资源抽象(如:Properties、YAML等),控制 PropertySource

  • Spring Profile:通过外部化配置,提供条件分支流程

10.14.3 Extensible XML authoring 的缺点

  • 高复杂度:开发人员要熟悉 XML Schema,spring.handlers、spring.schemas 以及 Spring API

  • 嵌套元素支持较弱:通常需要使用方法递归或者其嵌套解析的方式处理嵌套元素

  • XML 处理性能较差:Spring XML 基于 DOM Level 3 API 实现,该 API 便于理解,然而性能较差

  • XML 框架移植性较差:很难适配高性能和便利性的 XML 框架,如 JAXB

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值