第二节 Spring框架之二IOC容器的继续研究

一. IOC容器的探索

  1. 从容器的创建来探索
    用ClassPathXMLApplicationContext来创建,在这个类中,有很多个重载的构造方法,其中目前主要用以下这四个构造器,在内部实现上,这四个构造器调用的还是同一个构造方法。真正实现容器的刷新并且启动和创建bean组件的构造方法
    这里写图片描述

在这个构造器中最重要的方法就是refresh()方法,由它来负责刷新IOC容器,启动和创建bean。这个方法的实现如下,重要的注释已经在代码中注释

public void refresh() throws BeansException, IllegalStateException {
        synchronized (this.startupShutdownMonitor) {
            // Prepare this context for refreshing.
            prepareRefresh();
            // Tell the subclass to refresh the internal bean factory.加载好了配置文件
            //工厂中在解析XML的时候,解析完成后会保存每一个要创建的bean信息,但是并没有创建
            ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
            // Prepare the bean factory for use in this context.
            prepareBeanFactory(beanFactory);
            try {
                // Allows post-processing of the bean factory in context subclasses.
                postProcessBeanFactory(beanFactory);
                // Invoke factory processors registered as beans in the context.                invokeBeanFactoryPostProcessors(beanFactory);
                // Register bean processors that intercept bean creation.   
                                              registerBeanPostProcessors(beanFactory);

                // Initialize message source for this context.
                initMessageSource();

                // Initialize event multicaster for this context.
                initApplicationEventMulticaster();

                // Initialize other special beans in specific context subclasses.给子类实现的接口
                onRefresh();

                // Check for listener beans and register them.
                registerListeners();

                // Instantiate all remaining (non-lazy-init) singletons.
                **finishBeanFactoryInitialization(beanFactory);//初始化所有的单例类**

                // Last step: publish corresponding event.
                finishRefresh();
            }

            catch (BeansException ex) {
                // Destroy already created singletons to avoid dangling resources.
                destroyBeans();

                // Reset 'active' flag.
                cancelRefresh(ex);

                // Propagate exception to caller.
                throw ex;
            }
        }
    }

在这个方法中起决定作用的是finishBeanFactoryInitialization(beanFactory)这个方法,这个方法的具体实现如下

protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
        // Initialize conversion service for this context.和类型转换有关的
        if (beanFactory.containsBean(CONVERSION_SERVICE_BEAN_NAME) &&
                beanFactory.isTypeMatch(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class)) {
            beanFactory.setConversionService(
                    beanFactory.getBean(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class));
        }

        // Initialize LoadTimeWeaverAware beans early to allow for registering their transformers early.
        String[] weaverAwareNames = beanFactory.getBeanNamesForType(LoadTimeWeaverAware.class, false, false);
        for (String weaverAwareName : weaverAwareNames) {
            getBean(weaverAwareName);
        }

        // Stop using the temporary ClassLoader for type matching.
        beanFactory.setTempClassLoader(null);

        // Allow for caching all bean definition metadata, not expecting further changes.冻结配置,bean的配置信息都在beanfactory的一个map中保存着,先把这个map冻结,防止其他的操作来对其进行修改,为了多线程安全问题
        beanFactory.freezeConfiguration();

        // Instantiate all remaining (non-lazy-init) singletons.初始化所以的懒加载的bean
        beanFactory.preInstantiateSingletons();
    }

这个方法中起决定作用的是preInstantiateSingletons()这个方法,实现是:

public void preInstantiateSingletons() throws BeansException {
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Pre-instantiating singletons in " + this);
        }
        List<String> beanNames;
//beanDefinitionMap保存了每个bean的详细的配置信息
        synchronized (this.beanDefinitionMap) {
            // Iterate over a copy to allow for init methods which in turn register new bean definitions.
            // While this may not be part of the regular factory bootstrap, it does otherwise work fine.beanDefinitionNames保存了所有的bean的id
            beanNames = new ArrayList<String>(this.beanDefinitionNames);
        }
开始循环创建,按照配置顺序进行创建的
        for (String beanName : beanNames) {
            RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);//获取当前要创建的bean的详细的配置信息

//判断要创建的bean的配置信息中,是否是不是抽象的,是否是单列的,是否不是懒加载的
            if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
//判断这个bean是否是工厂bean
                if (isFactoryBean(beanName)) {
                    final FactoryBean<?> factory = (FactoryBean<?>) getBean(FACTORY_BEAN_PREFIX + beanName);
                    boolean isEagerInit;
                    if (System.getSecurityManager() != null && factory instanceof SmartFactoryBean) {
                        isEagerInit = AccessController.doPrivileged(new PrivilegedAction<Boolean>() {
                            @Override
                            public Boolean run() {
                                return ((SmartFactoryBean<?>) factory).isEagerInit();
                            }
                        }, getAccessControlContext());
                    }
                    else {
                        isEagerInit = (factory instanceof SmartFactoryBean &&
                                ((SmartFactoryBean<?>) factory).isEagerInit());
                    }
                    if (isEagerInit) {
                        getBean(beanName);
                    }
                }
                else {
//是一个多功能方法,如果这个bean没有被创建,则这个方法就去创建bean,如果这个bean创建了,则就去获取这个bean,是在AbstractBeanFactory这个工厂类中
                    getBean(beanName);
                }
            }
        }
    }

在这个方法中实现起功能的是getBean这个方法,这个方法是在AbstractBeanFactory工厂类中,在这个工厂类中有多个这个方法的重载方法如下:

public Object getBean(String name) throws BeansException {
        return doGetBean(name, null, null, false);
    }

    @Override
    public <T> T getBean(String name, Class<T> requiredType) throws BeansException {
        return doGetBean(name, requiredType, null, false);
    }

    @Override
    public Object getBean(String name, Object... args) throws BeansException {
        return doGetBean(name, null, args, false);
    }

    public <T> T getBean(String name, Class<T> requiredType, Object... args) throws BeansException {
        return doGetBean(name, requiredType, args, false);
    }

在这几个重载的方法中都调用了doGetBean方法,其实现为:

protected <T> T doGetBean(
            final String name, final Class<T> requiredType, final Object[] args, boolean typeCheckOnly)
            throws BeansException {

        final String beanName = transformedBeanName(name);
        Object bean;

        // Eagerly check singleton cache for manually registered singletons.获取单实例bean,紧接着如果bean为null,则开始创建,方法往下走
        Object sharedInstance = getSingleton(beanName);
        if (sharedInstance != null && args == null) {
            if (logger.isDebugEnabled()) {
                if (isSingletonCurrentlyInCreation(beanName)) {
                    logger.debug("Returning eagerly cached instance of singleton bean '" + beanName +
                            "' that is not fully initialized yet - a consequence of a circular reference");
                }
                else {
                    logger.debug("Returning cached instance of singleton bean '" + beanName + "'");
                }
            }
            bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
        }

        else {
            // Fail if we're already creating this bean instance:
            // We're assumably within a circular reference.
            if (isPrototypeCurrentlyInCreation(beanName)) {
                throw new BeanCurrentlyInCreationException(beanName);
            }

            // Check if bean definition exists in this factory.
            BeanFactory parentBeanFactory = getParentBeanFactory();
            if (parentBeanFactory != null && !containsBeanDefinition(beanName)) {
                // Not found -> check parent.
                String nameToLookup = originalBeanName(name);
                if (args != null) {
                    // Delegation to parent with explicit args.
                    return (T) parentBeanFactory.getBean(nameToLookup, args);
                }
                else {
                    // No args -> delegate to standard getBean method.
                    return parentBeanFactory.getBean(nameToLookup, requiredType);
                }
            }
没创建之前先标记一下bean已经被创建了,防止多个线程同时要创建这个bean
            if (!typeCheckOnly) {
                markBeanAsCreated(beanName);\\[ protected void markBeanAsCreated(String beanName) {
        this.alreadyCreated.add(beanName);
    }]
            }
创建bean
            try {
//拿到bean的定义信息
                final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
                checkMergedBeanDefinition(mbd, beanName, args);

                // Guarantee initialization of beans that the current bean depends on.、
//优先创建有依赖的bean
                String[] dependsOn = mbd.getDependsOn();
                if (dependsOn != null) {
                    for (String dependsOnBean : dependsOn) {
                        if (isDependent(beanName, dependsOnBean)) {
                            throw new BeanCreationException("Circular depends-on relationship between '" +
                                    beanName + "' and '" + dependsOnBean + "'");
                        }
                        registerDependentBean(dependsOnBean, beanName);
                        getBean(dependsOnBean);
                    }
                }

                // Create bean instance.
                if (mbd.isSingleton()) {

                    sharedInstance = getSingleton(beanName, new ObjectFactory<Object>() {
                        @Override
                        public Object getObject() throws BeansException {
                            try {
                                return createBean(beanName, mbd, args);
                            }
                            catch (BeansException ex) {
                                // Explicitly remove instance from singleton cache: It might have been put there
                                // eagerly by the creation process, to allow for circular reference resolution.
                                // Also remove any beans that received a temporary reference to the bean.
                                destroySingleton(beanName);
                                throw ex;
                            }
                        }
                    });
                    bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
                }

                else if (mbd.isPrototype()) {
                    // It's a prototype -> create a new instance.
                    Object prototypeInstance = null;
                    try {
                        beforePrototypeCreation(beanName);
                        prototypeInstance = createBean(beanName, mbd, args);
                    }
                    finally {
                        afterPrototypeCreation(beanName);
                    }
                    bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
                }

                else {
                    String scopeName = mbd.getScope();
                    final Scope scope = this.scopes.get(scopeName);
                    if (scope == null) {
                        throw new IllegalStateException("No Scope registered for scope '" + scopeName + "'");
                    }
                    try {
                        Object scopedInstance = scope.get(beanName, new ObjectFactory<Object>() {
                            @Override
                            public Object getObject() throws BeansException {
                                beforePrototypeCreation(beanName);
                                try {
                                    return createBean(beanName, mbd, args);
                                }
                                finally {
                                    afterPrototypeCreation(beanName);
                                }
                            }
                        });
                        bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
                    }
                    catch (IllegalStateException ex) {
                        throw new BeanCreationException(beanName,
                                "Scope '" + scopeName + "' is not active for the current thread; " +
                                "consider defining a scoped proxy for this bean if you intend to refer to it from a singleton",
                                ex);
                    }
                }
            }
            catch (BeansException ex) {
                cleanupAfterBeanCreationFailure(beanName);
                throw ex;
            }
        }

        // Check if required type matches the type of the actual bean instance.
        if (requiredType != null && bean != null && !requiredType.isAssignableFrom(bean.getClass())) {
            try {
                return getTypeConverter().convertIfNecessary(bean, requiredType);
            }
            catch (TypeMismatchException ex) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Failed to convert bean '" + name + "' to required type [" +
                            ClassUtils.getQualifiedName(requiredType) + "]", ex);
                }
                throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());
            }
        }
        return (T) bean;
    }

接下来就是getSingleton这个方法,其实现为

public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
        Assert.notNull(beanName, "'beanName' must not be null");
//singletonObjects IOC容器创建的所有单实例都在这个map里
        synchronized (this.singletonObjects) {
            Object singletonObject = this.singletonObjects.get(beanName);
            if (singletonObject == null) {
                if (this.singletonsCurrentlyInDestruction) {
                    throw new BeanCreationNotAllowedException(beanName,
                            "Singleton bean creation not allowed while the singletons of this factory are in destruction " +
                            "(Do not request a bean from a BeanFactory in a destroy method implementation!)");
                }
                if (logger.isDebugEnabled()) {
                    logger.debug("Creating shared instance of singleton bean '" + beanName + "'");
                }
                beforeSingletonCreation(beanName);
                boolean recordSuppressedExceptions = (this.suppressedExceptions == null);
                if (recordSuppressedExceptions) {
                    this.suppressedExceptions = new LinkedHashSet<Exception>();
                }
                try {
//创建了bean
                    singletonObject = singletonFactory.getObject();
                }
                catch (BeanCreationException ex) {
                    if (recordSuppressedExceptions) {
                        for (Exception suppressedException : this.suppressedExceptions) {
                            ex.addRelatedCause(suppressedException);
                        }
                    }
                    throw ex;
                }
                finally {
                    if (recordSuppressedExceptions) {
                        this.suppressedExceptions = null;
                    }
                    afterSingletonCreation(beanName);
                }
                addSingleton(beanName, singletonObject);
            }
            return (singletonObject != NULL_OBJECT ? singletonObject : null);
        }
    }

将创建好的bean组件放到singletonObjects中

    protected void addSingleton(String beanName, Object singletonObject) {
        synchronized (this.singletonObjects) {
            this.singletonObjects.put(beanName, (singletonObject != null ? singletonObject : NULL_OBJECT));
            this.singletonFactories.remove(beanName);
            this.earlySingletonObjects.remove(beanName);
            this.registeredSingletons.add(beanName);
        }
    }

总结一下,IOC容器最底层的实现就是一个Map集合
这些核心方法的底层调用顺序为:
这里写图片描述

  1. 继续研究起IOC容器的用法
    1) 给bean的级联属性赋值,一个类的属性是另一个类的对象,即就是这个作为属性的类的对象的属性的值
        <property name="bookName" value="红楼梦"></property>
        <property name="price" value="998"></property>
    </bean>

    <bean id="person01" class="com.fgy.bean.Person" p:lastName="王五" p:gender="女" p:salary="19999">
        <property name="myBook" ref="book01"></property>
        <!-- 会改掉外部全局的book bean的属性值,因为ref是一个严格的引用 -->
        <property name="myBook.price" value="1111"></property>
        <!-- 不想改外部的bean,可以自己在内部创建bean -->
    </bean>

2)静态工厂和实例工厂
静态工厂:调用工厂方法,获取对象。不需要创建工厂对象,即就是创建一个静态方法
实例工厂:调用工厂方法,获取对象。需要创建工厂对象

<!--class 静态工厂的全类名
        factory-method 静态工厂的工厂方法
        constructor-arg标签设置工厂方法的参数
      -->
    <bean id="myBookStaticFactory01" class="com.fgy.factory.MyBookStaticFactory" factory-method="getBook" >
        <constructor-arg value="发恼天空"></constructor-arg>

    </bean>

    <!--
        1.先把实例工厂的bean创建进来
      -->
    <bean id="myBookInstanceFactory" class="com.fgy.factory.MyBookInstanceFactory" ></bean>
    <!--2.创建book的bean,告诉bean用的是那个工厂,调用的是哪个工厂方法  -->
    <bean id="book03" class="com.fgy.bean.Book" factory-bean="myBookInstanceFactory" factory-method="getBook">
        <constructor-arg value="孙子兵法"></constructor-arg>
    </bean>

3)配置factoryBean
实现factoryBean的实现类

package com.fgy.factory;
import org.springframework.beans.factory.FactoryBean;
import com.fgy.bean.Book;
public class MyBookImplFactoryBean implements FactoryBean<Book> {
    //获取对象,工厂用来创建对象的方法
    public Book getObject() throws Exception {
        Book book = new Book();
        book.setBookName("国志");
        book.setAuthor("佚名");
        book.setPrice(99.8);        
        return book;
    }
//工厂返回创建对象的类型
    public Class<?> getObjectType() {
        return Book.class;
    }
//创建的对象是否是单例对象
    public boolean isSingleton() {
        return false;
    }
}
<!--1.创建实现了factoryBean接口的类
        1).实现了factoryBean接口的所类,都被Spring认识的工厂类
        2).这个接口就是Spring配置的
        3).Spring创建什么对象都是照这个类指定的
        4).这个工厂是一个单例的
        2.让Spring知道这样的一个工厂类,即注册
         id是用来获取创建对象的
        3.正常来说IOC容器启动时会创建所的单实例对象,但是对于实现factoryBean接口的单实例类,却不会在IOC容器启动时,创建和获取
      -->
      <bean id="myBookImplFactoryBean" class="com.fgy.factory.MyBookImplFactoryBean"></bean>

4.<!--通过继承实现bean配置信息的重用 -->
      <bean id="book04" class="com.fgy.bean.Book" p:bookName="亮剑" p:author="小海" p:price="998"></bean>       
      <bean id="book05" class="com.fgy.bean.Book" p:price="9999" parent="book04"></bean>

4)通过abstract属性创建一个模板bean,当abstract属性被赋值为true时,这个bean组件只能被继承,不能获取对象

<bean id="book04" class="com.fgy.bean.Book" p:bookName="亮剑" p:author="小海" p:price="998" abstract="true"></bean>     
      <bean id="book05" class="com.fgy.bean.Book" p:price="9999" parent="book04"></bean>

5)bean之间的依赖关系,作用:决定bean执行的顺序

bean默认的创建顺序,按照配置优先的原则如
      <bean id="dog01" class="com.fgy.bean.Dog"></bean>
      <bean id="cat01" class="com.fgy.bean.Cat"></bean>
顺序:先创建了dog,然后创建了cat
自定义创建顺序依赖于depends-on这个属性
      <bean id="dog01" class="com.fgy.bean.Dog" depends-on="cat01"></bean>
      <bean id="cat01" class="com.fgy.bean.Cat"></bean>

6)测试bean的作用域,分别创建单实例和多实例的bean
作用域:指的就是bean是否是单实例,作用域默认是单实例的,整个容器中,只有一个bean,
修改:scope属性
scope属性值:prototype:多实例:容器启动时不会创建对象,每次获取的时候,采取创建对象,我们可以在获取的时候指定值
singleton:默认的就是,单实例bean
request:在Web环境下,同一个request创建一个对象
session:在Web环境下,同一个session创建一个对象
7)创建带有生命周期方法的bean
生命周期:bean的生命周期是受IOC容器管理的,IOC容器决定了Bean的生命周期
单实例:
创建:容器启动时
初始化:指定初始化方法后,在容器启动后,就初始化了
销毁:容器销毁的时候调用,ConfigurableApplicationContext接口有close()方法
我们可以自己去定义一个方法,告诉Spring,哪些方法是用来初始化,哪些用来做销毁方法,这些方法不能有参数,bean的属性都设置好了,单实例情况下
多实例:
创建:获取的时候,创建对象
初始化:对象创建完成后,初始化
销毁:不会被调用。因为容器底层不会保存创建多实例的对象,容器关闭时只会销毁保存的对象,即容器启动时创建的单实例对象
8)bean的后置处理器
作用:就是在bean初始化前后可以进行一些预处理
实际上bean的后置处理器就是Spring的一个接口BeanPostProcessor,在这个接口中有两个方法
/**
* bean 通过构造器刚创建好的对象,也是我们要初始化的对象
* beanName bean的名字,就是在XML配置中的bean的id值
*/
Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException;
在初始化之前做一些操作
初始化方法
/**
* bean 通过初始化后的对象,对象的内容可能已经发生了改变
* beanName bean的名字,就是在XML配置中的bean的id值
*/
Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException;
在初始化之后做一些操作
9)引用外部属性文件
①这是在Spring配置文件的XML文件中直接写的,这样写的缺点就是把数据都写死了,没有体现动态性

<!--举例数据库连接池,用Spring来管理数据库连接池  -->
    <bean id="comboPooledDataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="user" value="root"></property>
        <property name="password" value="123456"></property>
        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/test"></property>
        <property name="driverClass" value="com.mysql.jdbc.Driver"></property>
    </bean>
测试举例:
    DataSource dataSource = (DataSource) ioc.getBean("comboPooledDataSource");

②所以我们要引用外部属性文件,体现数据的动态性
首先我们要用到context命名空间

<!--context命名空间,注册外部的properties文件,并设置访问路径,classpath是类目录下  -->
    <context:property-placeholder location="classpath:JDBCConfig.properties"/>

    <!--举例数据库连接池,用Spring来管理数据库连接池  -->
    <bean id="comboPooledDataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="user" value="${jdbc.user}"></property>
        <property name="password" value="${password}"></property>
        <property name="jdbcUrl" value="${jdbcUrl}"></property>
        <property name="driverClass" value="${driverClass}"></property>

注意点:为了区分配置文件中的值和Spring自己属性的值,一般建议给配置文件中的每个属性都加一个前缀
10)基于XML的自动装配
装配:解释过来就是给属性赋值的过程
①手动装配:在容器启动时,每个组件的属性值都为null,所以要为每个属性手动的在配置文件中配置值

<bean id="bookService" class="com.fgy.service.BookService">
        <property name="bookDao" ref="bookDao"></property>
    </bean>
    <bean id="bookDao" class="com.fgy.dao.BookDao"></bean>

②自动装配:自动装配对基本数据类型无作用

autowire="default"、autowire="no":都是设置为不自动装配
autowire="byName":按照属性名作为bean的id,去容器中查找,并且赋值
        注意:bean的id是按什么确定的呢?不是按照类中的属性名,而是按照类中属性的setter方法中set后面的单词,当找到的时候,对容器组件属性赋值,否则赋值为null
    <bean id="bookService" class="com.fgy.service.BookService" autowire="byName"></bean>
    <bean id="bookDao" class="com.fgy.dao.BookDao"></bean>
autowire="byType":按照属性的类型去容器中找到这个类型对于的bean,然后赋值。如果容器中有多个这个类型的bean,则这种方法就不能进行自动装配了

    <bean id="bookService" class="com.fgy.service.BookService" autowire="byType"></bean>
    <bean id="bookDao" class="com.fgy.dao.BookDao"></bean>
autowire="constructor":按照构造器进行装配
                1.先去看构造器中参数的类型,按照类型进行装配
                2.如果这个参数类型有多个组件,参数的变量名就会作为id继续查找
                3.都找不到,则装配null

    <bean id="bookService" class="com.fgy.service.BookService" autowire="constructor"></bean>
    <bean id="bookDao" class="com.fgy.dao.BookDao"></bean>

11)SpEL

<bean id="person" class="com.atguigu.beans.Person">
    <!--[SpEL测试I]在SpEL中使用字面量  -->
    <property name="lastName" value="#{'张三'}"></property>
    <!--[SpEL测试VI]在SpEL中使用运算符 -->
    <property name="salary" value="#{9879.98*14}"></property>
    <!--[SpEL测试II]在SpEL中引用其他bean #{对象的id}获取这个对象 -->
    <property name="myBook" value="#{book01}"></property>
    <!--[SpEL测试III]在SpEL中引用其他bean的某个属性值  -->
    <property name="email" value="#{book01.author}"></property>
    <!--[SpEL测试IV]在SpEL中调用非静态方法  -->
    <property name="myMap" value="#{helloMap.getMap('key01')}"></property>
    <!--[SpEL测试V]在SpEL中调用静态方法  
    #{T(全类名).方法名(参数列表)}
    -->
    <property name="list" value="#{T(com.atguigu.beans.HelloMap).getList(3)}"></property>
  </bean>
  <bean id="helloMap" class="com.atguigu.beans.HelloMap"></bean>
  <bean id="book01" class="com.atguigu.beans.Book">
    <property name="bookName" value="西游记"></property>
    <property name="author" value="吴承恩@atguigu.com"></property>
  </bean>
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值