Spring IOC原理


一、bean组件:
    1. BeanFactory: listableBeanFactory & hierarchicalBeanFactory & AutoWireCapableBeanFactory
        最终实现:defaultListableBeanFactory

    2. Bean: 最终BeanDefinition,所有操作对象基于此。

    3. Bean解析:XmlBeanDefinitionReader完成。

二、context组件:
    1. 功能:spring运行环境,保存各对象关系,创建bean,捕获事件。
    2. 继承beanFactory ,说明spring主要是bean。
       继承ResourceLoader,可访问所有外部资源。

三、Core组件:
    1. Resource定义了资源的访问方式
        Resource封装了多种资源类型:
        即,所有资源都可通过getInputStream()获取,返回InputStream。同时满足了资源提供者、资源使用者的方便。
    2. 通过ResourceLoader加载资源:
        Resource resource = ResourceLoader.getResource(String location);
    3. Context资源加载、解析和描述通过ResourcePatternResolver完成。

 

Spring IOC(一)

IOC:Inversion of Control(控制反转)。IOC它所体现的并不是一种技术,而是一种思想,一种将设计好的对象交给容器来管理的思想。IOC的核心思想就体现在控制、反转这两个词上面,要理解就必须要理解几个问题:

1、谁控制谁?在传统的开发工作中,我们一般都是主动去new一个对象,这个是主动控制依赖对象。但是对于IOC而已,控制权会被移交给容器,所以应该是IOC容器控制对象。

2、控制什么?既然是IOC容器控制对象,那控制什么呢?IOC容器除了负责控制对象的生成还包括外部资源的获取。

3、为何是反转?对象主动生成依赖对象,我们称之为“正转”,但是现在有IOC来负责了,所以反转则是IOC容器来负责对象的生成和注入过程。

4、那些地方反转?依赖对象的获取被反转了。

对于IOC而言,它强调是将主动变为被动,由IOC容器来负责依赖对象的创建和查找,由IOC容器来进行注入组合对象。将原来的强联系、高耦合转变为了弱关系、松耦合。IOC,它能指导我们如何设计出松耦合、更加优良的程序,把应用程序从原来需要维护依赖对象之间关系中彻底解放出来而更加专注于业务逻辑,这样会使得程序的整个体系机构变得非常灵活。

其实IoC对编程带来的最大改变不是从代码上,而是从思想上,发生了“主从换位”的变化。应用程序原本是老大,要获取什么资源都是主动出击,但是在IoC/DI思想中,应用程序就变成被动的了,被动的等待IoC容器来创建并注入它所需要的资源了。

有了IOC就有必要提到DI了。DI,Dependency Injection,即“依赖注入”。其实IOC和DI本就是同一个概念的两种不同的表述,DI所描述的是由容器动态地将某个依赖关系注入到主键当中去,其需要理解如下几个概念:

1、谁依赖谁?应用程序依赖IOC容器。

2、依赖什么?因为应用程序不再主动去创建对象,由IOC容器来向应用程序注入,所以应该是应用程序依赖IOC容器来提供的外部资源。

3、谁注入谁?由IOC容器向应用程序注入。

4、注入什么?注入的某个对象所依赖的外部资源。

通俗点将就是IOC就是容器控制应用程序所需要外部资源的创建和管理,然后将其反转给应用程序;而DI是应用程序依赖容器提供的外部对象,容器将其依赖的外部资源在运行期注入到应用程序中。两者表达的意思都是容器负责应用程序的创建和管理,应用程序只需要在需要它们的时候等待容器将其所依赖的外部资源提供就行,至于来自哪里,怎么来的应用程序都不需要知道。

具体的IOC理解我就不多阐述了,网上实在是太多了,这里推荐几篇博客:

1、谈谈对Spring IOC的理解

2、【第二章】 IoC 之 2.1 IoC基础 ——跟我学Spring3

3、Spring的IOC原理[通俗解释一下]

4、spring ioc原理(看完后大家可以自己写一个spring)

IOC结构体系

IOC作为一个容器,它里面放得都是bean、bean与bean之间的对应关系,而bean之间的对应关系我们开始都是通过xml配置文件来体现的。那么这里就反馈了如下几个问题:

1、对应与对象之间的关系是通过xml配置文件来描述的(当然也可以是properties等文件)。

2、描述的文件存放位置在那里,一般来说我们都是放在classpath目录下的,但是也可是是URL、fileSystem。

3、文件的解析。

4、Bean在容器中的表现形式,也就是它的数据结构。

对于Spring而言,它用Resource、BeanDefinition、BeanDefinitionReader、BeanFactory、ApplicationContext五个组件来实现以上问题,而同时这5个接口定义了 spring ioc 容器的基本代码组件结构。下面我们逐一了解这五个结构

Resource

Resource,对资源的抽象,它的每一个实现类都代表了一种资源的访问策略,如ClasspathResource 、 URLResource ,FileSystemResource 等。

20151217100002

 

BeanDefinition

用来描述和抽象一个具体的Bean对象,它是描述Bean对象的基本数据结构。

BeanDefinitionReader

外部资源所表达的语义需要统一转化为统一的内部数据结构BeanDefinition,这个时候BeanDefinitionReader就起到统一解析的作用力了。对应不同的描述需要有不同的 Reader 。如 XmlBeanDefinitionReader 用来读取xml 描述配置的 bean 对象。

20151217100001

BeanFactory

BeanFactory是一个非常纯粹的bean容器,它是IOC必备的数据结构,其中BeanDefinition是她的基本结构,它内部维护着一个BeanDefinition map,并可根据BeanDefinition 的描述进行 bean 的创建和管理。

 

20151217100003

 

ApplicationContext

这个就是大名鼎鼎的Spring容器,它叫做应用上下文,与我们应用息息相关,她继承BeanFactory,所以它是BeanFactory的扩展升级版,如果BeanFactory是屌丝的话,那么ApplicationContext则是名副其实的高富帅。由于ApplicationContext的结构就决定了它与BeanFactory的不同,其主要区别有:

1、继承MessageSource,提供国际化的标准访问策略。

2、继承ApplicationEventPublisher,提供强大的事件机制。

3、扩展ResourceLoader,可以用来加载多个Resource,可以灵活访问不同的资源。

4、对Web应用的支持。

下图是上面组合关系图(以ClasspathXmlApplicationContext 为例)

20151217100004

以上图片均来自:啃啃老菜: Spring IOC核心源码学习(一)

 

refresh() 方法

ClassPathXmlApplicationContext的refresh() 方法负责完成了整个容器的初始化。

为 什么叫refresh?也就是说其实是刷新的意思,该IOC容器里面维护了一个单例的BeanFactory,如果bean的配置有修改,也可以直接调用 refresh方法,它将销毁之前的BeanFactory,重新创建一个BeanFactory。所以叫refresh也是能理解的。

以下是Refresh的基本步骤:
1.把配置xml文件转换成resource。resource的转换是先通过ResourcePatternResolver来解析可识别格式的配置文件的路径(如"classpath*:"等),如果没有指定格式,默认会按照类路径的资源来处理。 
2.利用XmlBeanDefinitionReader完成对xml的解析,将xml Resource里定义的bean对象转换成统一的BeanDefinition。
3.将BeanDefinition注册到BeanFactory,完成对BeanFactory的初始化。BeanFactory里将会维护一个BeanDefinition的Map。


当getBean的时候就会根据调用BeanFactory,根据bean的BeanDifinition来实例化一个bean。当然根据bean的lazy-init、protetype等属性设置不同以上过程略有差别。

 

refresh()代码如下:

public void refresh() throws BeansException, IllegalStateException {  
    synchronized (this.startupShutdownMonitor) {  
        // Prepare this context for refreshing.  
        prepareRefresh();  
  
        // Tell the subclass to refresh the internal bean factory.  
        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.  
            beanFactory.destroySingletons();  
  
            // Reset 'active' flag.  
            cancelRefresh(ex);  
  
            // Propagate exception to caller.  
            throw ex;  
        }  
    }  
}  

 

 以上的obtainFreshBeanFactory是很关键的一个方法,里面会调用loadBeanDefinition方法,如下:

protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws IOException {  
    // Create a new XmlBeanDefinitionReader for the given BeanFactory.  
    XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory);  
  
    // Configure the bean definition reader with this context's  
    // resource loading environment.  
    beanDefinitionReader.setResourceLoader(this);  
    beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this));  
  
    // Allow a subclass to provide custom initialization of the reader,  
    // then proceed with actually loading the bean definitions.  
    initBeanDefinitionReader(beanDefinitionReader);  
    loadBeanDefinitions(beanDefinitionReader);  
}  

 LoadBeanDifinition方法很关键,这里特定于整个IOC容器,实例化了一个XmlBeanDefinitionReader来解析Resource文件。关于Resource文件如何初始化和xml文件如何解析都在

loadBeanDefinitions(beanDefinitionReader);  

里面的层层调用完成,这里不在累述。

 

下面LZ将尽全力阐述IOC的初始化过程和在该过程中涉及的重要组件,这系列博客是也是LZ学习、研究Spring机制和源码的学习笔记,其中难免会参考别人的博客,如有雷同,纯属借鉴。同时也避免不了错误之处,博文中的错误望各位博友指出,不胜感激!!!

参考资料

1、啃啃老菜: Spring IOC核心源码学习(一)

 

Spring IOC(二):初始化过程—简介

首先我们先来看看如下一段代码

ClassPathResource resource = new ClassPathResource("bean.xml");  
        DefaultListableBeanFactory factory = new DefaultListableBeanFactory();  
        XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);  
        reader.loadBeanDefinitions(resource);  

 

博友是否对这段简单代码记忆犹新呢? 这段代码是编程式使用IOC容器,通过这个简单的程序我们初步判定IOC容器的使用过程:

1、创建IOC配置文件的抽闲资源,也就是Resource接口。

2、创建BeanFactory,DefaultListtableBeanFactory是BeanFactory模式实现类。

3、创建一个BeanDefinitionReader对象,该对象为BeanDefinition的读取器。xml文件就使用XMLBeanDefinitionReader。

4、使用Reader来装载配置文件。loadBeanDefinitions就包括了资源文件的解析和注入过程。

通过上面四个步骤我们就可以轻松地使用IOC容器了,在整个过程可以剖析为三个步骤,这三个步骤也是IOC容器的初始化过程:Resource定位、载入、注册。如下:

Resource定位

我们一般使用外部资源来描述Bean对象,所以IOC容器第一步就是需要定位Resource外部资源。Resource的定位其实就是BeanDefinition的资源定位,它是由ResourceLoader通过统一的Resource接口来完成的,这个Resource对各种形式的BeanDefinition的使用都提供了统一接口。

载入

第二个过程就是BeanDefinition的载入。BeanDefinitionReader读取、解析Resource定位的资源,也就是将用户定义好的Bean表示成IOC容器的内部数据结构也就是BeanDefinition。在IOC容器内部维护着一个BeanDefinition Map的数据结构,通过这样的数据结构,IOC容器能够对Bean进行更好的管理。

在配置文件中每一个<bean>都对应着一个BeanDefinition对象。

注册

第三个过程则是注册,即向IOC容器注册这些BeanDefinition,这个过程是通过BeanDefinitionRegistery接口来实现的。在IOC容器内部其实是将第二个过程解析得到的BeanDefinition注入到一个HashMap容器中,IOC容器就是通过这个HashMap来维护这些BeanDefinition的。在这里需要注意的一点是这个过程并没有完成依赖注入,依赖注册是发生在应用第一次调用getBean向容器所要Bean时。当然我们可以通过设置预处理,即对某个Bean设置lazyinit属性,那么这个Bean的依赖注入就会在容器初始化的时候完成。

 

经过这三个步骤,IOC容器的初始化过程就已经完成了,后面LZ会结合源代码详细阐述这三个过程的实现。下面来看看与IOC容器相关的体系结构图,以ClassPathXmlApplicationContext为例(图片来自:【Spring】IOC核心源码学习(二):容器初始化过程

左边黄色部分是 ApplicationContext 体系继承结构,右边是 BeanFactory 的结构体系。

 

Spring IOC(三):初始化过程—Resource定位

 

我们知道Spring的IoC起到了一个容器的作用,其中装得都是各种各样的Bean。同时在我们刚刚开始学习Spring的时候都是通过xml文件来定义Bean,Spring会某种方式加载这些xml文件,然后根据这些信息绑定整个系统的对象,最终组装成一个可用的基于轻量级容器的应用系统。

Spring IoC容器整体可以划分为两个阶段,容器启动阶段,Bean实例化阶段。其中容器启动阶段蛀牙包括加载配置信息、解析配置信息,装备到BeanDefinition中以及其他后置处理,而Bean实例化阶段主要包括实例化对象,装配依赖,生命周期管理已经注册回调。下面LZ先介绍容器的启动阶段的第一步,即定位配置文件。

我们使用编程方式使用DefaultListableBeanFactory时,首先是需要定义一个Resource来定位容器使用的BeanDefinition。

ClassPathResource resource = new ClassPathResource("bean.xml");  

 

通过这个代码,就意味着Spring会在类路径中去寻找bean.xml并解析为BeanDefinition信息。当然这个Resource并不能直接被使用,他需要被BeanDefinitionReader进行解析处理(这是后面的内容了)。

对于各种applicationContext,如FileSystemXmlApplicationContext、ClassPathXmlApplicationContext等等,我们从这些类名就可以看到他们提供了那些Resource的读入功能。下面我们以FileSystemXmlApplicationContext为例来阐述Spring IoC容器的Resource定位。

先看FileSystemXmlApplicationContext继承体系结构:

从图中可以看出FileSystemXMLApplicationContext继承了DefaultResourceLoader,具备了Resource定义的BeanDefinition的能力,其源代码如下:

public class FileSystemXmlApplicationContext extends AbstractXmlApplicationContext {  
    /** 
     * 默认构造函数 
     */  
    public FileSystemXmlApplicationContext() {  
    }  
  
    public FileSystemXmlApplicationContext(ApplicationContext parent) {  
        super(parent);  
    }  
  
    public FileSystemXmlApplicationContext(String configLocation) throws BeansException {  
        this(new String[] {configLocation}, true, null);  
    }  
  
    public FileSystemXmlApplicationContext(String... configLocations) throws BeansException {  
        this(configLocations, true, null);  
    }  
  
    public FileSystemXmlApplicationContext(String[] configLocations, ApplicationContext parent) throws BeansException {  
        this(configLocations, true, parent);  
    }  
  
    public FileSystemXmlApplicationContext(String[] configLocations, boolean refresh) throws BeansException {  
        this(configLocations, refresh, null);  
    }  
      
    //核心构造器  
    public FileSystemXmlApplicationContext(String[] configLocations, boolean refresh, ApplicationContext parent)  
            throws BeansException {  
  
        super(parent);  
        setConfigLocations(configLocations);  
        if (refresh) {  
            refresh();  
        }  
    }  
  
    //通过构造一个FileSystemResource对象来得到一个在文件系统中定位的BeanDefinition  
    //采用模板方法设计模式,具体的实现用子类来完成  
    protected Resource getResourceByPath(String path) {  
        if (path != null && path.startsWith("/")) {  
            path = path.substring(1);  
        }  
        return new FileSystemResource(path);  
    }  
  
}  

 

AbstractApplicationContext中的refresh()方法是IoC容器初始化的入口,也就是说IoC容器的初始化是通过refresh()方法来完成整个调用过程的。在核心构造器中就对refresh进行调用,通过它来启动IoC容器的初始化工作。getResourceByPath为一个模板方法,通过构造一个FileSystemResource对象来得到一个在文件系统中定位的BeanDEfinition。getResourceByPath的调用关系如下(部分):

refresh为初始化IoC容器的入口,但是具体的资源定位还是在XmlBeanDefinitionReader读入BeanDefinition时完成,loadBeanDefinitions() 加载BeanDefinition的载入。

protected final void refreshBeanFactory() throws BeansException {  
        //判断是否已经创建了BeanFactory,如果创建了则销毁关闭该BeanFactory  
        if (hasBeanFactory()) {  
            destroyBeans();  
            closeBeanFactory();  
        }  
        try {  
            //创建DefaultListableBeanFactory实例对象  
            DefaultListableBeanFactory beanFactory = createBeanFactory();  
            beanFactory.setSerializationId(getId());  
            customizeBeanFactory(beanFactory);  
            //加载BeanDefinition信息  
            loadBeanDefinitions(beanFactory);  
            synchronized (this.beanFactoryMonitor) {  
                this.beanFactory = beanFactory;  
            }  
        }  
        catch (IOException ex) {  
            throw new ApplicationContextException("I/O error parsing bean definition source for " + getDisplayName(), ex);  
        }  
    }  

 

DefaultListableBeanFactory作为BeanFactory默认实现类,其重要性不言而喻,而createBeanFactory()则返回该实例对象。

protected DefaultListableBeanFactory createBeanFactory() {  
        return new DefaultListableBeanFactory(getInternalParentBeanFactory());  
    }  

 

loadBeanDefinition方法加载BeanDefinition信息,BeanDefinition就是在这里定义的。AbstractRefreshableApplicationContext对loadBeanDefinitions仅仅只是定义了一个抽象的方法,真正的实现类为其子类AbstractXmlApplicationContext来实现:

AbstractRefreshableApplicationContext:

protected abstract void loadBeanDefinitions(DefaultListableBeanFactory beanFactory)  
            throws BeansException, IOException;  

 

AbstractXmlApplicationContext:

protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException {  
        //创建bean的读取器(Reader),即XmlBeanDefinitionReader,并通过回调设置到容器中  
        XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory);  
  
        //  
        beanDefinitionReader.setEnvironment(getEnvironment());  
        //为Bean读取器设置Spring资源加载器  
        beanDefinitionReader.setResourceLoader(this);  
        //为Bean读取器设置SAX xml解析器  
        beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this));  
  
        //  
        initBeanDefinitionReader(beanDefinitionReader);  
        //Bean读取器真正实现的地方  
        loadBeanDefinitions(beanDefinitionReader);  
    }  

 

程序首先首先创建一个Reader,在前面就提到过,每一类资源都对应着一个BeanDefinitionReader,BeanDefinitionReader提供统一的转换规则;然后设置Reader,最后调用loadBeanDefinition,该loadBeanDefinition才是读取器真正实现的地方:

protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws BeansException, IOException {  
        //获取Bean定义资源的定位  
        Resource[] configResources = getConfigResources();  
        if (configResources != null) {  
            reader.loadBeanDefinitions(configResources);  
        }  
        //获取Bean定义资源的路径。在FileSystemXMLApplicationContext中通过setConfigLocations可以配置Bean资源定位的路径  
        String[] configLocations = getConfigLocations();  
        if (configLocations != null) {  
            reader.loadBeanDefinitions(configLocations);  
        }  
    }  

 

首先通过getConfigResources()获取Bean定义的资源定位,如果不为null则调用loadBeanDefinitions方法来读取Bean定义资源的定位。

loadBeanDefinitions是中的方法:

public int loadBeanDefinitions(String... locations) throws BeanDefinitionStoreException {  
        Assert.notNull(locations, "Location array must not be null");  
        int counter = 0;  
        for (String location : locations) {  
            counter += loadBeanDefinitions(location);  
        }  
        return counter;  
    }  

继续:

public int loadBeanDefinitions(String location) throws BeanDefinitionStoreException {  
        return loadBeanDefinitions(location, null);  
    }  

 

再继续:

public int loadBeanDefinitions(String location, Set<Resource> actualResources) throws BeanDefinitionStoreException {  
        //获取ResourceLoader资源加载器  
        ResourceLoader resourceLoader = getResourceLoader();  
        if (resourceLoader == null) {  
            throw new BeanDefinitionStoreException(  
                    "Cannot import bean definitions from location [" + location + "]: no ResourceLoader available");  
        }  
  
        //  
        if (resourceLoader instanceof ResourcePatternResolver) {  
            try {  
                //调用DefaultResourceLoader的getResourceByPath完成具体的Resource定位  
                Resource[] resources = ((ResourcePatternResolver) resourceLoader).getResources(location);  
                int loadCount = loadBeanDefinitions(resources);  
                if (actualResources != null) {  
                    for (Resource resource : resources) {  
                        actualResources.add(resource);  
                    }  
                }  
                if (logger.isDebugEnabled()) {  
                    logger.debug("Loaded " + loadCount + " bean definitions from location pattern [" + location + "]");  
                }  
                return loadCount;  
            }  
            catch (IOException ex) {  
                throw new BeanDefinitionStoreException(  
                        "Could not resolve bean definition resource pattern [" + location + "]", ex);  
            }  
        }  
        else {  
            //调用DefaultResourceLoader的getResourceByPath完成具体的Resource定位  
            Resource resource = resourceLoader.getResource(location);  
            int loadCount = loadBeanDefinitions(resource);  
            if (actualResources != null) {  
                actualResources.add(resource);  
            }  
            if (logger.isDebugEnabled()) {  
                logger.debug("Loaded " + loadCount + " bean definitions from location [" + location + "]");  
            }  
            return loadCount;  
        }  
    }  

 

在这段源代码中通过调用DefaultResourceLoader的getResource方法:

public Resource getResource(String location) {  
        Assert.notNull(location, "Location must not be null");  
        if (location.startsWith("/")) {  
            return getResourceByPath(location);  
        }  
        //处理带有classPath标识的Resource  
        else if (location.startsWith(CLASSPATH_URL_PREFIX)) {  
            return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());  
        }  
        else {  
            try {  
                //处理URL资源  
                URL url = new URL(location);  
                return new UrlResource(url);  
            }  
            catch (MalformedURLException ex) {  
                return getResourceByPath(location);  
            }  
        }  
    }  

 

在getResource方法中我们可以清晰地看到Resource资源的定位。这里可以清晰地看到getResourceByPath方法的调用,getResourceByPath方法的具体实现有子类来完成,在FileSystemXmlApplicationContext实现如下:

protected Resource getResourceByPath(String path) {  
        if (path != null && path.startsWith("/")) {  
            path = path.substring(1);  
        }  
        return new FileSystemResource(path);  
    }  

 

这样代码就回到了博客开初的FileSystemXmlApplicationContext 中来了,它提供了FileSystemResource 来完成从文件系统得到配置文件的资源定义。当然这仅仅只是Spring IoC容器定位资源的一种逻辑,我们可以根据这个步骤来查看Spring提供的各种资源的定位,如ClassPathResource、URLResource等等。下图是ResourceLoader的继承关系:

这里就差不多分析了Spring IoC容器初始化过程资源的定位,在BeanDefinition定位完成的基础上,就可以通过返回的Resource对象来进行BeanDefinition的载入、解析了。

下篇博客将探索Spring IoC容器初始化过程的解析,Spring Resource体系结构会在后面详细讲解。

转载于:https://my.oschina.net/u/3572551/blog/1486404

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值