Spring框架AOP核心源码解析之一:代理对象的创建

阅读文章时,看看流程图会更容易理解哦:
https://www.processon.com/view/link/5df5e143e4b004cc9a3068d2

在这里插入图片描述

示例准备

在分析aop源码之前,先搭建一个demo示例,演示下效果。

比如,有一个DemoService接口,里面定义一个demo方法:

public interface DemoService {
    void demo(String args);
}

给他一个简单的实现:

@Component
public class DemoServiceImpl implements DemoService{

    @Override
    public void demo(String args) {
        System.out.println("-----DemoServiceImpl.demo()-----");
//        throw new RuntimeException();
    }
}

接下来,使用一个切面类来封装切面的逻辑,比如来一个日志切面,使用@Aspect注解标明它是一个切面类:

@Component
@Aspect
public class DemoLogAspect {
    
    //切点表达式,表示要切DemoService下的所有方法
    @Pointcut("execution(* com.zyy.sc.analysis.framework.debugAopAspect.DemoService.*(..))")
    public void pointCut(){}

    //前置通知,在执行目标方法前执行
    @Before("pointCut()")
    public void beforeMethod(JoinPoint joinPoint){
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        System.out.println("拦截到目标方法:"+methodName+", 方法参数:"+args+",执行【前置通知】逻辑");
    }

    //后置通知,在执行目标方法后执行
    @After("pointCut()")
    public void afterMethod(JoinPoint joinPoint){
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        System.out.println("拦截到目标方法:"+methodName+", 方法参数:"+args+",执行【后置通知】逻辑");
    }

    //返回通知,在执行目标方法成功后执行
    @AfterReturning("pointCut()")
    public void afterReturningMethod(JoinPoint joinPoint){
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        System.out.println("拦截到目标方法:"+methodName+", 方法参数:"+args+",执行【返回通知】逻辑");
    }

    //异常通知,在执行目标方法异常后执行
    @AfterThrowing("pointCut()")
    public void afterThrowingMethod(JoinPoint joinPoint){
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        System.out.println("拦截到目标方法:"+methodName+", 方法参数:"+args+",执行【异常通知】逻辑");
    }
    
    //环绕通知,.....
}

在配置类中,注意要使用@EnableAspectJAutoProxy注解来启用aop的自动代理机制:

@Configuration
@ComponentScan
@EnableAspectJAutoProxy
public class MainConfig {

}

最后,我们使用Main方法来测试:

public class Main {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig.class);
        context.getBean(DemoService.class).demo("args");
        context.registerShutdownHook();
    }
}

控制台输出:

拦截到目标方法:demo, 方法参数:args,执行【前置通知】逻辑
-----DemoServiceImpl.demo()-----
拦截到目标方法:demo, 方法参数:args,执行【后置通知】逻辑
拦截到目标方法:demo, 方法参数:args,执行【返回通知】逻辑

@EnableAspectJAutoProxy 干了什么

在使用spring的过程中,时常会引入一些组件,如果要启用这些组件的话,通常需要我们配置下@EnableXXX,那么这个注解原理是什么,以@EnableAspectJAutoProxy为例说明一下。

进入EnableAspectJAutoProxy注解的源码:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(AspectJAutoProxyRegistrar.class)
public @interface EnableAspectJAutoProxy {

	boolean proxyTargetClass() default false;

	boolean exposeProxy() default false;
}

可以看到,其实它是用@Import注解标注的,“Spring框架IOC容器初始化核心源码解析之三:BeanDefinition注册”文章中的说明,在解析配置类(doProcessConfigurationClass方法)的过程中,会处理@Import注解,具体的处理过程这儿不重复了。

        // Process any @Import annotations
		processImports(configClass, sourceClass, getImports(sourceClass), true);

我们可以来看下getImports方法,它会调用collectImports方法,这个方法会扫描解析类的所有注解,并递归解析注解的注解,因此只要有@Import,都会被找出来,然后从里面找到value值。

private void collectImports(SourceClass sourceClass, Set<SourceClass> imports, Set<SourceClass> visited)
			throws IOException {

		if (visited.add(sourceClass)) {
			for (SourceClass annotation : sourceClass.getAnnotations()) {
				String annName = annotation.getMetadata().getClassName();
				if (!annName.startsWith("java") && !annName.equals(Import.class.getName())) {
					collectImports(annotation, imports, visited);
				}
			}
			imports.addAll(sourceClass.getAnnotationAttributes(Import.class.getName(), "value"));
		}
	}

对于@EnableAspectJAutoProxy注解,会找到AspectJAutoProxyRegistrar这个类,这个类是ImportBeanDefinitionRegistrar,spring会调用registerBeanDefinitions方法,这个方法中,它会注册一个AnnotationAwareAspectJAutoProxyCreator类的beanDefinition。

public void registerBeanDefinitions(
			AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {

        //注册AnnotationAwareAspectJAutoProxyCreator这个类的beanDefinition
		AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry);

        //设置AnnotationAwareAspectJAutoProxyCreator这个类的beanDefinition的属性
		AnnotationAttributes enableAspectJAutoProxy =
				AnnotationConfigUtils.attributesFor(importingClassMetadata, EnableAspectJAutoProxy.class);
		if (enableAspectJAutoProxy != null) {
			if (enableAspectJAutoProxy.getBoolean("proxyTargetClass")) {
				AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
			}
			if (enableAspectJAutoProxy.getBoolean("exposeProxy")) {
				AopConfigUtils.forceAutoProxyCreatorToExposeProxy(registry);
			}
		}
	}

因此,@EnableXXX的注解,通常使用了@Import注解标注了,作用是往容器中注册自己的组件。

创建代理对象准备

在createBean方法中,有这样一段逻辑:

//这儿会调用InstantiationAwareBeanPostProcessor的 postProcessBeforeInstantiation 
// Give BeanPostProcessors a chance to return a proxy instead of the target bean instance.
Object bean = resolveBeforeInstantiation(beanName, mbdToUse);
if (bean != null) {
	return bean;
}

而在前面,我们注册的 AnnotationAwareAspectJAutoProxyCreator 它就实现了InstantiationAwareBeanPostProcessor, 其postProcessBeforeInstantiation方法,注意返回值除了custom TargetSource(默认不会走这个分支)之外,都是null,因此不会阻断getBean的流程(见getBean分析)

public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException {
		//缓存key
		Object cacheKey = getCacheKey(beanClass, beanName);

		if (!StringUtils.hasLength(beanName) || !this.targetSourcedBeans.contains(beanName)) {
			if (this.advisedBeans.containsKey(cacheKey)) {
				return null;
			}
			
			//判断是否基础类型(Advice、Pointcut、Advisor、AopInfrastructureBean),基础类型不会代理
			//shouldSkip判断是否应该跳过,这里面进行了切面的解析。
			if (isInfrastructureClass(beanClass) || shouldSkip(beanClass, beanName)) {
				this.advisedBeans.put(cacheKey, Boolean.FALSE);
				return null;
			}
		}

        //custom TargetSource 的处理, 没仔细研究
		// Create proxy here if we have a custom TargetSource.
		// Suppresses unnecessary default instantiation of the target bean:
		// The TargetSource will handle target instances in a custom fashion.
		...

		return null;
	}

skip方法

protected boolean shouldSkip(Class<?> beanClass, String beanName) {
		// TODO: Consider optimization by caching the list of the aspect names
		//找到候选的advisor,先找Advisor类型的,然后构建 aspectj 的。
		List<Advisor> candidateAdvisors = findCandidateAdvisors();
		
		//切面不能对切面自己再做增强处理,应该跳过。
		for (Advisor advisor : candidateAdvisors) {
			if (advisor instanceof AspectJPointcutAdvisor &&
					((AspectJPointcutAdvisor) advisor).getAspectName().equals(beanName)) {
				return true;
			}
		}
		return super.shouldSkip(beanClass, beanName);
	}

findCandidateAdvisors方法,注意这个方法被 AnnotationAwareAspectJAutoProxyCreator 重写了的:

protected List<Advisor> findCandidateAdvisors() {
        //调用父类的方法找Advisor类型
		// Add all the Spring advisors found according to superclass rules.
		List<Advisor> advisors = super.findCandidateAdvisors();
		
		//构建所有AspectJ的Advisors
		// Build Advisors for all AspectJ aspects in the bean factory.
		if (this.aspectJAdvisorsBuilder != null) {
			advisors.addAll(this.aspectJAdvisorsBuilder.buildAspectJAdvisors());
		}
		return advisors;
	}

先看下父类的 AbstractAdvisorAutoProxyCreator.findCandidateAdvisors 方法,它会去找容器中所有的Advisor类型,找到后通过getBean实例化:

public List<Advisor> findAdvisorBeans() {

        //找到容器中所有的Advisor类型,并将名称缓存到cachedAdvisorBeanNames
		// Determine list of advisor bean names, if not cached already.
    		String[] advisorNames = this.cachedAdvisorBeanNames;
		if (advisorNames == null) {
			// Do not initialize FactoryBeans here: We need to leave all regular beans
			// uninitialized to let the auto-proxy creator apply to them!
			advisorNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
					this.beanFactory, Advisor.class, true, false);
			this.cachedAdvisorBeanNames = advisorNames;
		}
		
		//没有找到返回空列表
		if (advisorNames.length == 0) {
			return new ArrayList<>();
		}
        
        //有的话,getBean,然后加入到列表中返回。
		List<Advisor> advisors = new ArrayList<>();
		for (String name : advisorNames) {
			if (isEligibleBean(name)) {
				...
				else {
					try {
						advisors.add(this.beanFactory.getBean(name, Advisor.class));
					}
					...
				}
			}
		}
		return advisors;
	}

对于示例中的AspectJ切面的方式, buildAspectJAdvisors 方法用来解析 AspectJ切面的:

public List<Advisor> buildAspectJAdvisors() {
        //this.aspectBeanNames用来缓存容器中所有的aspectBeanNames。
		List<String> aspectNames = this.aspectBeanNames;

        //缓存为空,说明第一次触发该方法,进入解析。就算容器中没有aspectBean,在进行一次解析后, this.aspectBeanNames也不会为null。
		if (aspectNames == null) {
			synchronized (this) {
				aspectNames = this.aspectBeanNames;
				if (aspectNames == null) {
					List<Advisor> advisors = new ArrayList<>();
					aspectNames = new ArrayList<>();
					
					//查询出容器总所有的bean。
					String[] beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
							this.beanFactory, Object.class, true, false);
							
					//遍历所有bean,如果是Aspect(可以简单理解为使用@Aspect注解的)bean,则构建对应的Advisor,并放到缓存中。	
					for (String beanName : beanNames) {
						...
						if (this.advisorFactory.isAspect(beanType)) {
							aspectNames.add(beanName);
							AspectMetadata amd = new AspectMetadata(beanType, beanName);
							if (amd.getAjType().getPerClause().getKind() == PerClauseKind.SINGLETON) {
								
								//Aspect实例化的元信息工厂,封装了Aspect的元信息
								MetadataAwareAspectInstanceFactory factory =
										new BeanFactoryAspectInstanceFactory(this.beanFactory, beanName);
										
								//构建Advisor。注意一个Aspect中可以定义多个通知或者切点(前置通知、后置通知),因此一个Aspect会构建出来多个Advisor
								List<Advisor> classAdvisors = this.advisorFactory.getAdvisors(factory);
								
								//如果Aspect是单例,直接缓存对象
								if (this.beanFactory.isSingleton(beanName)) {
									this.advisorsCache.put(beanName, classAdvisors);
								}
								//否则缓存元信息工厂,后续可直接用工厂构建
								else {
									this.aspectFactoryCache.put(beanName, factory);
								}
								advisors.addAll(classAdvisors);
							}
							else {
								....
							}
						}
					}
					this.aspectBeanNames = aspectNames;
					return advisors;
				}
			}
		}

        //容器中压根没有Aspectj的bean,直接返回空列表
		if (aspectNames.isEmpty()) {
			return Collections.emptyList();
		}
		
		//如果有的话,遍历每个Aspectj,直接重缓存中获取(要么直接获取Advisor列表,要么获取到工厂,从工厂中获取)
		List<Advisor> advisors = new ArrayList<>();
		for (String aspectName : aspectNames) {
		    
		    //取缓存的成品
			List<Advisor> cachedAdvisors = this.advisorsCache.get(aspectName);
			if (cachedAdvisors != null) {
				advisors.addAll(cachedAdvisors);
			}
			
			//取缓存的元信息工厂,然后创建新的Advisor的实例
			else {
				MetadataAwareAspectInstanceFactory factory = this.aspectFactoryCache.get(aspectName);
				advisors.addAll(this.advisorFactory.getAdvisors(factory));
			}
		}
		return advisors;
	}

现在来看看如何通过Aspect来解析构建出Advisor来的:

public List<Advisor> getAdvisors(MetadataAwareAspectInstanceFactory aspectInstanceFactory) {
		
		//从元信息工厂中获取Aspect的元信息
		Class<?> aspectClass = aspectInstanceFactory.getAspectMetadata().getAspectClass();
		String aspectName = aspectInstanceFactory.getAspectMetadata().getAspectName();
		//校验一波。
		validate(aspectClass);
        
        //这儿使用Decorator设计模式(装饰者),对元信息工厂包装一下,提供了缓存实例能力,这样就只需要实例化一次。
		// We need to wrap the MetadataAwareAspectInstanceFactory with a decorator
		// so that it will only instantiate once.
		MetadataAwareAspectInstanceFactory lazySingletonAspectInstanceFactory =
				new LazySingletonAspectInstanceFactoryDecorator(aspectInstanceFactory);
            
        //遍历aspectClass的所有方法(包括父类或者接口的),注意要去除@PointCut注解的方法
		List<Advisor> advisors = new ArrayList<>();
		for (Method method : getAdvisorMethods(aspectClass)) {
		    
		    //对方法上有切面相关的注解的(@Before\@After等),包装成 InstantiationModelAwarePointcutAdvisorImpl 这种类型的 Advisor 实例返回。
			Advisor advisor = getAdvisor(method, lazySingletonAspectInstanceFactory, advisors.size(), aspectName);
			if (advisor != null) {
				advisors.add(advisor);
			}
		}

	    .....

		return advisors;
	}

getAdvisor方法:

public Advisor getAdvisor(Method candidateAdviceMethod, MetadataAwareAspectInstanceFactory aspectInstanceFactory,
			int declarationOrderInAspect, String aspectName) {

		validate(aspectInstanceFactory.getAspectMetadata().getAspectClass());
        
        //找到AspectJ的相关注解,封装为AspectJExpressionPointcut对象,注意会按照顺序找第一个出现的个人AspectJ注解【顺序为Before.class, Around.class, After.class, AfterReturning.class, AfterThrowing.class, Pointcut.class】。
		AspectJExpressionPointcut expressionPointcut = getPointcut(
				candidateAdviceMethod, aspectInstanceFactory.getAspectMetadata().getAspectClass());
		if (expressionPointcut == null) {
			return null;
		}

        //实例化 InstantiationModelAwarePointcutAdvisorImpl 返回
		return new InstantiationModelAwarePointcutAdvisorImpl(expressionPointcut, candidateAdviceMethod,
				this, aspectInstanceFactory, declarationOrderInAspect, aspectName);
	}

可以看到 AnnotationAwareAspectJAutoProxyCreator的postProcessBeforeInstantiation 方法核心作用就是找到容器中的所有切面,构建成Advisor放到缓存中。

创建代理对象

AnnotationAwareAspectJAutoProxyCreator 的另外一个重要的方法是 postProcessAfterInitialization, 它的调用时机位于bean实例化后(见getbean流程分析),它是真正创建代理对象的地方:

public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) throws BeansException {
		if (bean != null) {
			Object cacheKey = getCacheKey(bean.getClass(), beanName);
			if (!this.earlyProxyReferences.contains(cacheKey)) {
			
			    //如果必要的话,将对象包装成代理对象返回。IfNecessary这种命名方式在spring中也是比较常见的。
				return wrapIfNecessary(bean, beanName, cacheKey);
			}
		}
		return bean;
	}

wrapIfNecessary方法封装了包装代理对象的流程,其中核心为getAdvicesAndAdvisorsForBean方法(找到合适的增强器)和createProxy方法(创建代理对象)。

protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
		
		...
		//如果bean是不需要包装成代理对象的,直接返回,this.advisedBeans这个map,在前面的逻辑会对不需要代理的bean放到里面去。
		if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
			return bean;
		}
		
		//再一次的对bean进行校验,是否需要包装为代理。不需要的放到this.advisedBeans中,标记为false。
		//shouldSkip在前面已经分析过了,不同的是,因为已经对Aspectj解析过一遍了,再次调用时直接从缓存中获取。
		if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
			this.advisedBeans.put(cacheKey, Boolean.FALSE);
			return bean;
		}
        
        //需要代理的bean,则创建代理对象返回。
		// Create proxy if we have advice.
		
		//getAdvicesAndAdvisorsForBean 方法,找出满足当前bean的增强器(拦截器)。
		Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
		
		if (specificInterceptors != DO_NOT_PROXY) {
			this.advisedBeans.put(cacheKey, Boolean.TRUE);
			
			//使用增强器对bean进行增强,创建出代理对象。
			Object proxy = createProxy(
					bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
			
			this.proxyTypes.put(cacheKey, proxy.getClass());
			return proxy;
		}

		this.advisedBeans.put(cacheKey, Boolean.FALSE);
		return bean;
	}

getAdvicesAndAdvisorsForBean 方法会调用 findEligibleAdvisors 方法,其逻辑如下:

protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {
        //找到所有的Advisor,这个方法前面已经调用过,直接从缓存中获取已经缓存的Advisor,advisor也可以叫做增强器。
		List<Advisor> candidateAdvisors = findCandidateAdvisors();
		
		//从所有的增强器中,找出适配当前这个bean的。
		List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);
		//留一个扩展点。
		extendAdvisors(eligibleAdvisors);
		
		if (!eligibleAdvisors.isEmpty()) {
		    //对增强器排序,这会影响拦截的处理流程(哪个增强器先执行,哪个后执行)
			eligibleAdvisors = sortAdvisors(eligibleAdvisors);
		}
		return eligibleAdvisors;
	}

findAdvisorsThatCanApply方法就是调用了 AopUtils.findAdvisorsThatCanApply 方法, 该方法会遍历所有增强器,然后和当前bean去做匹配(大概的匹配过程为是,获取到当前bean的所有方法,包括父类和接口,然后遍历方法去和增强器里面获取到的切点表达式进行匹配,篇幅问题,不展开),匹配上了将当前的增强器加入的适配的列表中。

现在有bean实例了,也找到合适的增强器,最后一步就是来创建代理对象了:

protected Object createProxy(Class<?> beanClass, @Nullable String beanName,
			@Nullable Object[] specificInterceptors, TargetSource targetSource) {
        
        //暴露代理对象到AopContext中(ThreadLocal),当然这里只是在MergedBeanDefinition设置一个属性,还暴露不了(因为代理对象还没创建)
		if (this.beanFactory instanceof ConfigurableListableBeanFactory) {
			AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory) this.beanFactory, beanName, beanClass);
		}
        
        //代理工厂,代理对象由它来创建。
		ProxyFactory proxyFactory = new ProxyFactory();
		proxyFactory.copyFrom(this);

        //是否代理目标对象标记,这儿涉及到如果是代理目标对象的,底层使用cglib,否则底层使用JDK动态代理。
		if (!proxyFactory.isProxyTargetClass()) {
			if (shouldProxyTargetClass(beanClass, beanName)) {
				proxyFactory.setProxyTargetClass(true);
			}
			else {
				evaluateProxyInterfaces(beanClass, proxyFactory);
			}
		}

        //buildAdvisors,这方法中会加上公共的拦截器(默认为空),然后把传入的specificInterceptors包装成合适的Advisor返回。
		Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
		
		//设置代理对象的拦截器、目标源对象等
		proxyFactory.addAdvisors(advisors);
		proxyFactory.setTargetSource(targetSource);
		//又一个扩展点
		customizeProxyFactory(proxyFactory);
		
		
        //proxyFactory的其他配置
		...

        //创建代理对象
		return proxyFactory.getProxy(getProxyClassLoader());
	}

进入 ProxyFactory.getProxy方法,

public Object getProxy(@Nullable ClassLoader classLoader) {
	return createAopProxy().getProxy(classLoader);
}
	
protected final synchronized AopProxy createAopProxy() {
	if (!this.active) {
		activate();
	}
	//getAopProxyFactory 默认使用的 DefaultAopProxyFactory
	return getAopProxyFactory().createAopProxy(this);
}
	

进入到 DefaultAopProxyFactory 的 createAopProxy方法,它会创建对应两种不同的AopProxy返回:

public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
	if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
		Class<?> targetClass = config.getTargetClass();
		....
		
		if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
		    //返回JdkDynamicAopProxy这种AopProxy,它会使用JDK动态代理创建代理对象
			return new JdkDynamicAopProxy(config);
		}
		//返回ObjenesisCglibAopProxy这种AopProxy,它会使用cglib创建代理对象
		return new ObjenesisCglibAopProxy(config);
	}
	else {
	
	    //返回JdkDynamicAopProxy这种AopProxy,它会使用JDK动态代理创建代理对象
		return new JdkDynamicAopProxy(config);
	}
}

以JdkDynamicAopProxy为例,它的getProxy方法:

public Object getProxy(@Nullable ClassLoader classLoader) {
	...
	Class<?>[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true);
	findDefinedEqualsAndHashCodeMethods(proxiedInterfaces);
	
	//熟悉的JDK动态代理代码,能传入this,是因为 JdkDynamicAopProxy 实现了 InvocationHandler 接口。
	return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this);
}

JdkDynamicAopProxy的invoke方法中,包含了如何使用增强器对目标方法的增强逻辑,下一篇代理对象的调用在详细分析。

本篇文章详细分析了Spring AOP创建代理对象的原理,是如何找到@Aspectj标注的切面,又是如何解析成对应的Advisor,然后怎么从所有的Advisor中找到符合当前创建bean的,这些信息准备好了之后,最后是怎么创建代理对象的(分析了JDK动态代理方式,cglib的方式你可以自行分析,需要对cglib的库有所了解)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值