[读书笔记]Spring中IOC容器中FileSystemXmlApplicationContext的初始化详解

接上文[读书笔记]Spring中IOC容器中XmlBeanFactory的初始化详解,我们这里尝试看一下FileSystemXmlApplicationContext这种ApplicationContext的初始化。

FileSystemXmlApplicationContext是独立的 XML application context,从文件系统或URL得到上下文定义文件。URL可以是绝对或者相对路径。普通路径将始终被解释为相对于当前VM工作目录,即使它们以斜杠/开头。(这与Servlet容器中的语义一致。) 使用明确的file:前缀强制执行绝对文件路径。

配置文件路径可以是一个具体的文件比如/myfiles/context.xml也可以是带有通配符的如/myfiles/*-context.xml,其可以通过getConfigLocations方法被覆盖。

在多个配置位置的情况下,后面的bean定义将覆盖前面加载的文件中定义的bean。这可以通过一个额外的XML文件故意覆盖某些bean定义。

这是一个简单的一站式便利应用程序上下文,考虑使用GenericApplicationContextXmlBeanDefinitionReader以便更灵活的上下文配置。

按照惯例,我们先看一下其类继承家族树,如下所示其根据ListableBeanFactoryHierarchicalBeanFactory保持与BeanFactory的关联。
在这里插入图片描述

FileSystemXmlApplicationContext 源码

public class FileSystemXmlApplicationContext extends AbstractXmlApplicationContext {

	public FileSystemXmlApplicationContext() {
	}

	public FileSystemXmlApplicationContext(ApplicationContext parent) {
		super(parent);
	}

	 // 这个构造函数的configLocation包含的是BeanDefinition所在的文件路径
	 // 从给定的xml 文件中加载BeanDefinition,然后自动刷新context
	public FileSystemXmlApplicationContext(String configLocation) throws BeansException {
		this(new String[] {configLocation}, true, null);
	}

	 // 这个构造函数允许configLocations包含多个BeanDefinition的文件路径
	public FileSystemXmlApplicationContext(String... configLocations) throws BeansException {
		this(configLocations, true, null);
	}
 // 这个构造函数在允许configLocation包含多个BeanDefinition的文件路径同时,还允许指定自己的双亲IOC容器
	public FileSystemXmlApplicationContext(String[] configLocations, ApplicationContext parent) throws BeansException {
		this(configLocations, true, parent);
	}
	 //这个构造函数在允许configLocation包含多个BeanDefinition的文件路径同时,还允许指定是否刷新context
	public FileSystemXmlApplicationContext(String[] configLocations, boolean refresh) throws BeansException {
		this(configLocations, refresh, null);
	}

	 //  允许多个路径、允许指定是否刷新、允许指定自己的双亲IOC容器
	public FileSystemXmlApplicationContext(
			String[] configLocations, boolean refresh, @Nullable ApplicationContext parent)
			throws BeansException {

		super(parent);
		setConfigLocations(configLocations);
		if (refresh) {
			refresh();
		}
	}


	 //将资源路径解析为文件系统路径 即使一个文件路径以/ 开头,它也会被视作为一个相对当前VM工作路径的相对路径
	@Override
	protected Resource getResourceByPath(String path) {
		if (path.startsWith("/")) {
			path = path.substring(1);
		}
		return new FileSystemResource(path);
	}

}

这个getResourceByPath是一个模板方法,其是在BeanDefinitionReader的loadBeanDefinition中被调用的。

【1】 FileSystemXmlApplicationContext

我们以如下代码入口分析。

FileSystemXmlApplicationContext pplicationContext =
new FileSystemXmlApplicationContext(xmlPath);

传入一个配置文件路径,实例化FileSystemXmlApplicationContext并刷新context。

public FileSystemXmlApplicationContext(String configLocation) throws BeansException {
	this(new String[] {configLocation}, true, null);
}

其将会调用如下构造函数,其中refresh是true,parent为null。

public FileSystemXmlApplicationContext(
		String[] configLocations, boolean refresh, @Nullable ApplicationContext parent)
		throws BeansException {
// 调用父类构造方法
	super(parent);
	
	// 设置配置文件加载路径
	setConfigLocations(configLocations);
	
	if (refresh) {
	// 刷新容器
		refresh();
	}
}

我们看一下super分支流程,其一路走到了顶级抽象基类AbstractApplicationContext上设置资源加载器。

//AbstractXmlApplicationContext
public AbstractXmlApplicationContext(@Nullable ApplicationContext parent) {
	super(parent);
}

// AbstractRefreshableConfigApplicationContext
public AbstractRefreshableConfigApplicationContext(@Nullable ApplicationContext parent) {
	super(parent);
}

//AbstractRefreshableApplicationContext
public AbstractRefreshableApplicationContext(@Nullable ApplicationContext parent) {
	super(parent);
}

//AbstractApplicationContext
public AbstractApplicationContext(@Nullable ApplicationContext parent) {
	this();
	setParent(parent);
}
// 这里很重要,设置资源加载器/解析器,默认是PathMatchingResourcePatternResolver
public AbstractApplicationContext() {
	this.resourcePatternResolver = getResourcePatternResolver();
}

我们再看一下其setConfigLocations(configLocations);,如下所示其调用了父类AbstractRefreshableConfigApplicationContext的setConfigLocations方法,为属性String[] configLocations赋值。

// AbstractRefreshableConfigApplicationContext
public void setConfigLocations(@Nullable String... locations) {
	if (locations != null) {
		Assert.noNullElements(locations, "Config locations must not be null");
		this.configLocations = new String[locations.length];
		for (int i = 0; i < locations.length; i++) {
			this.configLocations[i] = resolvePath(locations[i]).trim();
		}
	}
	else {
		this.configLocations = null;
	}
}

关于其父类的几点说明

在这里插入图片描述
AbstractXmlApplicationContext继承于AbstractRefreshableConfigApplicationContext,扩展支持了读取xml文件、解析并加载bean定义注册到容器中。

AbstractRefreshableApplicationContext继承于AbstractApplicationContext主要维护了容器刷新与BeanFactory生命周期的相关操作,如refreshBeanFactory(),cancelRefresh(),closeBeanFactory,hasBeanFactory(),getBeanFactory(),createBeanFactory()以及customizeBeanFactory。其留了一个抽象方法loadBeanDefinitions让子类实现。

AbstractRefreshableConfigApplicationContext中定义了configLocations属性维护上下文配置文件路径,其实现了InitializingBean接口在其afterPropertiesSet方法中对当前context进行了判断,如果非活跃状态,那么将调用refresh()方法。

通过分析FileSystemXmlApplicationContext的源代码可以知道,在创建FileSystemXmlApplicationContext容器时,构造方法做以下两项重要工作:

  • 首先,调用父类容器的构造方法(super(parent)方法)为容器设置好Bean资源加载器,默认是PathMatchingResourcePatternResolver
  • 然后,再调用父类AbstractRefreshableConfigApplicationContextsetConfigLocations(configLocations)方法设置Bean定义资源文件的定位路径。

【2】 AbstractApplicationContext

ApplicationContext接口的抽象实现。不强制要求用于配置的存储类型;简单实现常见的context功能特性。使用了模板方法设计模式,具体子类需要实现抽象方法。与普通的BeanFactory对比,一个ApplicationContext被设计用来检测具体的bean定义在它内部的bean 工厂。因此该类自动注册了在当前context被定义为bean的组件比如BeanFactoryPostProcessorsBeanPostProcessors以及ApplicationListeners

MessageSource可能会以messageSource名称作为context中的一个bean,否则 消息解析将会委派给父context。此外,ApplicationEventMulticaster(事件广播器)将会作为一个bean以名称applicationEventMulticaster存在于context,否则将会使用默认的SimpleApplicationEventMulticaster

通过扩展DefaultResourceLoader实现资源加载。因此,将非URL资源路径视为类路径资源(支持包含包路径的完整类路径资源名称,例如“mypackage/myresource.dat”),除非{getResourceByPath}方法在子类中被覆盖。

在这里插入图片描述

① 核心属性

 // 在bean 工厂中的MessageSource名字 ,如果不存在则委派给父类处理
public static final String MESSAGE_SOURCE_BEAN_NAME = "messageSource";

 //在bean 工厂中的LifecycleProcessor 名字,如果不存在则使用DefaultLifecycleProcessor 
public static final String LIFECYCLE_PROCESSOR_BEAN_NAME = "lifecycleProcessor";

 //bean 工厂中ApplicationEventMulticaster 的bean 名字,如果不存则是使用SimpleApplicationEventMulticaster 
public static final String APPLICATION_EVENT_MULTICASTER_BEAN_NAME = "applicationEventMulticaster";


static {
//为了避免应用程序在Weblogic8.1关闭时出现类加载异常加载问题,加载IoC容
//器关闭事件(ContextClosedEvent)类
	ContextClosedEvent.class.getName();
}
protected final Log logger = LogFactory.getLog(getClass());
// 唯一ID
private String id = ObjectUtils.identityToString(this);
// 展示名字
private String displayName = ObjectUtils.identityToString(this);
//父上下文
@Nullable
private ApplicationContext parent;
//当前上下文用的Environment 
@Nullable
private ConfigurableEnvironment environment;
// 在refresh时使用的BeanFactoryPostProcessors 
private final List<BeanFactoryPostProcessor> beanFactoryPostProcessors = new ArrayList<>();
// 上下文启动时间 System time in milliseconds
private long startupDate;
// 当前context是否active标识
private final AtomicBoolean active = new AtomicBoolean();
// 当前context是否被关闭标志
private final AtomicBoolean closed = new AtomicBoolean();
// refresh 和 destroy同步监视器-锁
private final Object startupShutdownMonitor = new Object();
//对JVM关闭挂钩的引用(如果已注册)
@Nullable
private Thread shutdownHook;
// 当前context使用的ResourcePatternResolver 
private ResourcePatternResolver resourcePatternResolver;
// 管理当前context中bean的生命周期
@Nullable
private LifecycleProcessor lifecycleProcessor;

/** MessageSource we delegate our implementation of this interface to. */
@Nullable
private MessageSource messageSource;

// 事件发布过程中的帮助类 ---应用事件广播器
@Nullable
private ApplicationEventMulticaster applicationEventMulticaster;

/** Statically specified listeners. */
private final Set<ApplicationListener<?>> applicationListeners = new LinkedHashSet<>();

// 早起发布的应用事件
@Nullable
private Set<ApplicationEvent> earlyApplicationEvents;

一些默认组件:

ResourcePatternResolver=new PathMatchingResourcePatternResolver(this);
environment=new StandardEnvironment()

// 三个动态获取
//如果messageSource不存在则使用如下值
messageSource=new DelegatingMessageSource();
// 如果applicationEventMulticaster不存在,则使用如下对象
applicationEventMulticaster=new SimpleApplicationEventMulticaster(beanFactory);
//如果lifecycleProcessor不存在,则使用如下对象
lifecycleProcessor=new DefaultLifecycleProcessor();

两个构造方法:

//无参构造,实例化resourcePatternResolver 
public AbstractApplicationContext() {
	this.resourcePatternResolver = getResourcePatternResolver();
}

 // 使用给定的父容器创建容器
public AbstractApplicationContext(@Nullable ApplicationContext parent) {
	this();
	setParent(parent);
}
//AbstractApplicationContext继承DefaultResourceLoader,也是一个
//Spring资源加载器,其getResource(String location)方法用于载入资源
protected ResourcePatternResolver getResourcePatternResolver() {
	return new PathMatchingResourcePatternResolver(this);
}

在设置容器的资源加载器之后,接下来FileSystemXmlApplicationContet执行setConfigLocations方法。通过调用其父类AbstractRefreshableConfigApplicationContext的方法进行对Bean定义资源文件的定位,该方法的源码如下:

public void setConfigLocations(@Nullable String... locations) {
	if (locations != null) {
		Assert.noNullElements(locations, "Config locations must not be null");
		this.configLocations = new String[locations.length];
		for (int i = 0; i < locations.length; i++) {
		// resolvePath为同一个类中将字符串解析为路径的方法
			this.configLocations[i] = resolvePath(locations[i]).trim();
		}
	}
	else {
		this.configLocations = null;
	}
}

通过这两个方法的源码我们可以看出,我们既可以使用一个字符串来配置多个Spring Bean定义资源文件,也可以使用字符串数组,即下面两种方式都是可以的:

  • ClasspathResource res = new ClasspathResource(“a.xml,b.xml,……”);多个资源文件路径之间可以是用” ,; /t/n”等分隔。
  • ClasspathResource res = new ClasspathResource(newString[]{“a.xml”,”b.xml”,……});

至此,Spring IoC容器在初始化时将配置的Bean定义资源文件定位为Spring封装的Resource。

② 核心方法

我们可以分为这样几个系列:与BeanFactory相关的、与XXXApplicationContext相关的、与生命周期相关的、与Message相关的以及自身提供的。

① 与BeanFactory相关

// Implementation of BeanFactory interface
public boolean containsBean(String name)
public boolean isSingleton(String name) throws NoSuchBeanDefinitionException
public boolean isPrototype(String name) throws NoSuchBeanDefinitionException
public boolean isTypeMatch(String name, ResolvableType typeToMatch) throws NoSuchBeanDefinitionException
public boolean isTypeMatch(String name, Class<?> typeToMatch) throws NoSuchBeanDefinitionException
public Class<?> getType(String name) throws NoSuchBeanDefinitionException
public String[] getAliases(String name)

// Implementation of ListableBeanFactory interface
public boolean containsBeanDefinition(String beanName)
public int getBeanDefinitionCount()
public String[] getBeanDefinitionNames()
public String[] getBeanNamesForType(ResolvableType type)
public String[] getBeanNamesForType(@Nullable Class<?> type)
public String[] getBeanNamesForType(@Nullable Class<?> type, boolean includeNonSingletons, boolean allowEagerInit)
public <T> Map<String, T> getBeansOfType(@Nullable Class<T> type) throws BeansException
public <T> Map<String, T> getBeansOfType(@Nullable Class<T> type, boolean includeNonSingletons, boolean allowEagerInit)
public String[] getBeanNamesForAnnotation(Class<? extends Annotation> annotationType)
public Map<String, Object> getBeansWithAnnotation(Class<? extends Annotation> annotationType)
public <A extends Annotation> A findAnnotationOnBean(String beanName, Class<A> annotationType)

//Implementation of HierarchicalBeanFactory interface
public BeanFactory getParentBeanFactory()
public boolean containsLocalBean(String name)
protected BeanFactory getInternalParentBeanFactory()

② 与MessageSource相关

//Implementation of MessageSource interface
public String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale)
public String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException
public String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException
private MessageSource getMessageSource() throws IllegalStateException
protected MessageSource getInternalParentMessageSource()

与ResourcePatternResolver相关的:

@Override
public Resource[] getResources(String locationPattern) throws IOException {
	return this.resourcePatternResolver.getResources(locationPattern);
}

③ 与生命周期相关的

public void start()
public void stop()
public boolean isRunning()

④ 让子类实现的抽象方法

protected abstract void refreshBeanFactory() throws BeansException, IllegalStateException;

protected abstract void closeBeanFactory();

public abstract ConfigurableListableBeanFactory getBeanFactory() throws IllegalStateException;

⑤ ApplicationContext相关

public void setId(String id)
public String getId()
public String getApplicationName()
public void setDisplayName(String displayName)
public String getDisplayName()
public ApplicationContext getParent()
public long getStartupDate()
public void destroy()
public void close()

与当前context环境相关的:

public void setEnvironment(ConfigurableEnvironment environment)
public ConfigurableEnvironment getEnvironment()
protected ConfigurableEnvironment createEnvironment()

获取当前context内部bean factory:

public AutowireCapableBeanFactory getAutowireCapableBeanFactory() throws IllegalStateException {
	return getBeanFactory();
}
// 抽象方法让子类实现
public abstract ConfigurableListableBeanFactory getBeanFactory() throws IllegalStateException;

⑥ 容器初始化过程中的核心方法

// 事件、监听、发布、广播
public void publishEvent(Object event)
protected void publishEvent(Object event, @Nullable ResolvableType eventType)
ApplicationEventMulticaster getApplicationEventMulticaster() throws IllegalStateException
protected void initApplicationEventMulticaster()
public void addApplicationListener(ApplicationListener<?> listener)
public Collection<ApplicationListener<?>> getApplicationListeners()
protected void registerListeners()


// 预刷新、刷新
public void refresh() throws BeansException, IllegalStateException
protected void prepareRefresh()
protected void onRefresh() throws BeansException
protected void finishRefresh()
protected void cancelRefresh(BeansException ex)

// BeanFactory 的处理
protected ConfigurableListableBeanFactory obtainFreshBeanFactory()
protected void prepareBeanFactory(ConfigurableListableBeanFactory beanFactory)
protected void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory)
protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory)

【3】BeanDefinition的Resource定位

从下图可以看到,这个FileSystemXmlApplicationContext已经通过继承AbstractApplicationContext具备了ResourceLoader读入以Resource定义的BeanDefinition的能力,因为AbstractApplicationContext的基类是DefaultResourceLoader。

在这里插入图片描述

public FileSystemXmlApplicationContext(
		String[] configLocations, boolean refresh, @Nullable ApplicationContext parent)
		throws BeansException {

	super(parent);
	setConfigLocations(configLocations);
	if (refresh) {
		refresh();
	}
}

在FileSystemXmlApplicationContext中,我们可以看到在构造函数中,实现了对configuration进行处理的功能,让所有配置在文件系统中的,以xml文件方式存在的BeanDefinition都能够得到有效的处理,比如,实现了getResourceByPath方法,这个方法是一个模板方法,是为读取Resource服务的。

在当前案例分析中,这个对BeanDefinition资源定位的过程,最初是由refresh来触发的,这个refresh的调用是在FileSystemXmlApplicationContext的构造函数中启动的。

构造函数.refresh ->
AbstractApplicationContext#refresh ->
AbstractApplicationContext#obtainFreshBeanFactory ->
AbstractRefreshableApplicationContext#refreshBeanFactory ->

AbstractRefreshableApplicationContext#refreshBeanFactory

@Override
protected final void refreshBeanFactory() throws BeansException {
//如果已经建立了BeanFactory,则销毁并关闭该BeanFactory
	if (hasBeanFactory()) {
		destroyBeans();
		closeBeanFactory();
	}
	try {
	//创建并设置DefaultListableBeanFactory ,然后调用loadBeanDefinitions载入BeanDefinition信息
		DefaultListableBeanFactory beanFactory = createBeanFactory();
		beanFactory.setSerializationId(getId());
		customizeBeanFactory(beanFactory);
		loadBeanDefinitions(beanFactory);
		this.beanFactory = beanFactory;
	}
	catch (IOException ex) {
		throw new ApplicationContextException("I/O error parsing bean definition source for " + getDisplayName(), ex);
	}
}

这个refreshBeanFactory会被FileSystemXmlApplicationContext构造函数中的refresh方法调用。在这个方法中,通过createBeanFActory构建了一个IOC容器供ApplicationContext使用。这个IOC容器就是我们前面提到过得DefaultListableBeanFactory,同时,它启动了loadBeanDefinitions来载入BeanDefinition,这个过程和前面以编程式的方法来使用IOC容器过程非常类似。

总结来说在初始化FileSystemXmlApplicationContext的过程中,通过IOC容器的初始化的而防人是来启动整个调用,使用的IOC容器是DefaultListableBeanFactory。具体的资源载入在XmlBeanDefinitionReader读入BeanDefinition时完成,在XmlBeanDefinitionReader的基类AbstractBeanDefinitionReader中可以看到这个载入过程的具体实现。

AbstractRefreshableApplicationContextloadBeanDefinitions是个抽象方法让其他子类实现。

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

AbstractBeanDefinitionReader#loadBeanDefinitions

假设这里我们定了了configLocation,指向的是一些xml配置文件。那么loadBeanDefinitions会走到下面这个方法。这个方法的作用就是解析configLocation中定义的一个个location得到Resource。

public int loadBeanDefinitions(String location, @Nullable Set<Resource> actualResources) throws BeanDefinitionStoreException {
// 默认是DefaultResourceLoader
		ResourceLoader resourceLoader = getResourceLoader();
		if (resourceLoader == null) {
			throw new BeanDefinitionStoreException(
					"Cannot load bean definitions from location [" + location + "]: no ResourceLoader available");
		}
// 这里对Resource的路径模式进行解析,比如我们设定的各种Ant格式的路径定义,得到resources集合。
		if (resourceLoader instanceof ResourcePatternResolver) {
			// Resource pattern matching available.
			try {
				Resource[] resources = ((ResourcePatternResolver) resourceLoader).getResources(location);
				int count = loadBeanDefinitions(resources);
				if (actualResources != null) {
					Collections.addAll(actualResources, resources);
				}
				if (logger.isTraceEnabled()) {
					logger.trace("Loaded " + count + " bean definitions from location pattern [" + location + "]");
				}
				return count;
			}
			catch (IOException ex) {
				throw new BeanDefinitionStoreException(
						"Could not resolve bean definition resource pattern [" + location + "]", ex);
			}
		}
		else {
			// Can only load single resources by absolute URL.
			Resource resource = resourceLoader.getResource(location);
			int count = loadBeanDefinitions(resource);
			if (actualResources != null) {
				actualResources.add(resource);
			}
			if (logger.isTraceEnabled()) {
				logger.trace("Loaded " + count + " bean definitions from location [" + location + "]");
			}
			return count;
		}
	}

对于取得Resource的具体过程,我们可以看看DefaultResourceLoader是怎样完成的。

@Override
public Resource getResource(String location) {
	Assert.notNull(location, "Location must not be null");

// 先尝试使用各种协议解析器处理location得到resource,默认为空
	for (ProtocolResolver protocolResolver : getProtocolResolvers()) {
		Resource resource = protocolResolver.resolve(location, this);
		if (resource != null) {
			return resource;
		}
	}
// new ClassPathContextResource(path, getClassLoader())
	if (location.startsWith("/")) {
		return getResourceByPath(location);
	}
	else if (location.startsWith(CLASSPATH_URL_PREFIX)) {
	// 处理带有classpath标识的Resource
		return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());
	}
	else {
		try {
			// Try to parse the location as a URL... 尝试作为URL去解析
			URL url = new URL(location);
			return (ResourceUtils.isFileURL(url) ? new FileUrlResource(url) : new UrlResource(url));
		}
		catch (MalformedURLException ex) {
			// No URL -> resolve as resource path.
			// new ClassPathContextResource(path, getClassLoader());
			return getResourceByPath(location);
		}
	}
}

前面我们看到的getResourceByPath会被子类FileSystemXmlApplicationContext实现,这个方法返回的是一个FileSystemResource对象。通过这个对象,Spring可以进行相关的IO操作,完成BeanDefinition的定位。

// 解析path,生成一个FileSystemResource返回
protected Resource getResourceByPath(String path) {
	if (path.startsWith("/")) {
		path = path.substring(1);
	}
	return new FileSystemResource(path);
}

如果是其他的ApplicationContext,那么会对应生成其他种类的Resource,比如ClassPathResource、ServletContextResource等。关于Spring中Resource的种类,我们可以在如下继承图中了解。
在这里插入图片描述

那么在BeanDefinition定位完成的基础上,就可以通过返回的Resource对象来进行BeanDefinition的载入了。在定位过程完成以后,为BeanDefinition的载入创造了IO操作的条件,但是具体的数据还没有开始读入。这些数据的读入将在BeanDefinition的载入和解析中完成。

参考博文:

[读书笔记]FileSystemXmlApplicationContext容器初始化之BeanDefinition的载入和解析

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

流烟默

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值