11_AOP原理

在spring中。Aop是其一大特色。要想开启spring的aop功能,可以通过在xml中配置

<aop:aspectj-autoporxy/>

开启注解,然后定义对应的切面,切点来完成。同样也可以通过@EnableAspectJAutoProxy注解来完成开启。在分析之前我们首先通过一个例子

@Configuration

@ComponentScan(basePackages = {"com.test.aop"})

@EnableAspectJAutoProxy

public class AopConfig {

}

在com.test.aop包下创建如下两个类

@Component

//代表当前是一个切面,spring会解析该注解中的方法进行植入

@Aspect

public class LogHandler

{

    /**

     * 定义一个切入点

     * 这代表切入点是名称为saying方法(可以配置包路径,参数等)

     */

     @Pointcut("execution(* *.saying(..))")

     public void sayings(){}

    //定义通知,前置通知

    @Before("sayings()")

    public void LogBefore()

    {

        System.out.println("Log before method");

    }

    //后置通知

    @After("sayings()")

    public void LogAfter()

    {

        System.out.println("Log after method");

    }

}

定义一个含有saying方法的bean

@Service

public class Business {

    public void saying(){

        System.out.println("saying 方法执行了");

    }

}

在调用business.saying的时候会输出如下结果

 

可以看到已经在方法执行进行切入了,那么我们从源码的角度来分一下。Spring是如何做的。

首先看@EnableAspectJAutoProxy注解做了什么。

@Target(ElementType.TYPE)

@Retention(RetentionPolicy.RUNTIME)

@Documented

@Import(AspectJAutoProxyRegistrar.class)

public @interface EnableAspectJAutoProxy {

         boolean proxyTargetClass() default false;

         boolean exposeProxy() default false;

}

还记得我们之前在说通过ConfigurationClassPostProcessor的作用吗。忘记了请查看8_invokeBeanFactoryPostProcessors 在spring进行解析配置类的时候如果发现有@Import或者实现了ImportSelector和importBeanDefinitionRegistrars接口会分别调用对应的方法,将其中的符合的bean注册到spring容器中。

class AspectJAutoProxyRegistrar implements ImportBeanDefinitionRegistrar {

 

         /**

          * Register, escalate, and configure the AspectJ auto proxy creator based on the value

          * of the @{@link EnableAspectJAutoProxy#proxyTargetClass()} attribute on the importing

          * {@code @Configuration} class.

          */

         @Override

         public void registerBeanDefinitions(

                            AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {

 

                   AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry);

 

                   AnnotationAttributes enableAspectJAutoProxy =

                                     AnnotationConfigUtils.attributesFor(importingClassMetadata, EnableAspectJAutoProxy.class);

                   if (enableAspectJAutoProxy.getBoolean("proxyTargetClass")) {

                            AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);

                   }

                   if (enableAspectJAutoProxy.getBoolean("exposeProxy")) {

                            AopConfigUtils.forceAutoProxyCreatorToExposeProxy(registry);

                   }

         }

}

通过源码可以看出它是实现了ImportBeanDefinitionRegistrar接口,所以在ConfigurationClassPostProcessor执行postProcessBeanDefinitionRegistry会调用该方法。

继续分析AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry);

实际就是向spring注册了一个AnnotationAwareAspectJAutoProxyCreator的bean。然后下面的代码为向该bean的proxyTargetClass进行设置相关的赋值。

下面我看下AnnotionAwareAspectJAutoProxyCreator的继承关系

 

从图中可以看出实现了SmartInstantionationAwareBeanPostProcessor

前面我们说过,在初始化完成bean后会调用后置处理器的postProcessAfterInstantiation来对相关的类进行处理,那么我们来看下AnnotationAwareAspectJAutoProxyCreator类的源码

@Override

         public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {

                   if (bean != null) {

            //先从缓存中获取

                            Object cacheKey = getCacheKey(bean.getClass(), beanName);

            //在之前谈到过,为了解决循环依赖问题,会返回一个

           //当前bean的ObjectFactory。如果当前bean在提前暴露

           //的map中,说明已经有通过ObjectFactory.getObject方式获取

          //当前bean的代理类了,于是当前的类创建过程不需要继续返回一个

         //相关的代理。如果没有,则对当前类进行封装

                            if (!this.earlyProxyReferences.contains(cacheKey)) {

                                     return wrapIfNecessary(bean, beanName, cacheKey);

                            }

                   }

                   return bean;

         }

下面进入wrapIfNecessary方法

protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {

        //已处理过,直接返回

                   if (beanName != null && this.targetSourcedBeans.contains(beanName)) {

                            return bean;

                   }

        //当前类不需要增强,这些是在postProcessBeforeInitialization进行初始化

                   if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {

                            return bean;

                   }

       //实现了Advice,PointCut,Advisor, AopInfrastructureBean是基础类,不需要代理

                   if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {

                            this.advisedBeans.put(cacheKey, Boolean.FALSE);

                            return bean;

                   }

 

                   //查询当前bean符合的advice。

                   Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);

        //如果有,则进行创建代理。

                   if (specificInterceptors != DO_NOT_PROXY) {

                            this.advisedBeans.put(cacheKey, Boolean.TRUE);

                            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;

         }

下面我们分别分析上面的两个动作,一是查找可以适配的增强器,二是生成对应的代理

@Override

         protected Object[] getAdvicesAndAdvisorsForBean(Class<?> beanClass, String beanName, TargetSource targetSource) {

                   List<Advisor> advisors = findEligibleAdvisors(beanClass, beanName);

                   if (advisors.isEmpty()) {

                            return DO_NOT_PROXY;

                   }

                   return advisors.toArray();

         }

进入findEligibleAdvisors

protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {

       //查找所有的增强器

                   List<Advisor> candidateAdvisors = findCandidateAdvisors();

        //获取当前的bean适合的增强器

                   List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);

       //添加一个默认的增强DefaultPointcutAdvisor

                   extendAdvisors(eligibleAdvisors);

                   if (!eligibleAdvisors.isEmpty()) {

            //所有的增强进行排序

                            eligibleAdvisors = sortAdvisors(eligibleAdvisors);

                   }

                   return eligibleAdvisors;

         }

 

进入AnnotationAwareAspectJAutoProxyCreator的findCandidateAdvisors

@Override

         protected List<Advisor> findCandidateAdvisors() {

                   // 查找实现了所有的advisor接口的bean

                   List<Advisor> advisors = super.findCandidateAdvisors();

                   //遍历所有的bean,查找声明了Aspect注解,并且进行相关解析

       //@Before,@Around,@After

                   advisors.addAll(this.aspectJAdvisorsBuilder.buildAspectJAdvisors());

                   return advisors;

         }

下面我们分析一下获取到对应的增强器,如何判断当前增强器是否可以使用的

protected List<Advisor> findAdvisorsThatCanApply(

                            List<Advisor> candidateAdvisors, Class<?> beanClass, String beanName) {

        //设置线程相关的变量,当前的线程正在试图增强的bean的名称

                   ProxyCreationContext.setCurrentProxiedBeanName(beanName);

                   try {

            //真正进行增强器选择

                            return AopUtils.findAdvisorsThatCanApply(candidateAdvisors, beanClass);

                   }

                   finally {

                            ProxyCreationContext.setCurrentProxiedBeanName(null);

                   }

         }

进入AopUtils.findAdvisorThatCanApply函数中

public static List<Advisor> findAdvisorsThatCanApply(List<Advisor> candidateAdvisors, Class<?> clazz) {

                   if (candidateAdvisors.isEmpty()) {

                            return candidateAdvisors;

                   }

                   List<Advisor> eligibleAdvisors = new LinkedList<Advisor>();

                   for (Advisor candidate : candidateAdvisors) {

             //根据不同的增强器类型来进行不同的判断

                            if (candidate instanceof IntroductionAdvisor && canApply(candidate, clazz)) {

                                     eligibleAdvisors.add(candidate);

                            }

                   }

                   boolean hasIntroductions = !eligibleAdvisors.isEmpty();

                   for (Advisor candidate : candidateAdvisors) {

                            if (candidate instanceof IntroductionAdvisor) {

                                     // already processed

                                     continue;

                            }

                            if (canApply(candidate, clazz, hasIntroductions)) {

                                     eligibleAdvisors.add(candidate);

                            }

                   }

                   return eligibleAdvisors;

         }

实际的判断方法存在于canApply方法中。

public static boolean canApply(Advisor advisor, Class<?> targetClass, boolean hasIntroductions) {

                   if (advisor instanceof IntroductionAdvisor) {

                            return ((IntroductionAdvisor) advisor).getClassFilter().matches(targetClass);

                   }

        //通过aop代理实现的增强器是PointcutAdvisor类型

                   else if (advisor instanceof PointcutAdvisor) {

                            PointcutAdvisor pca = (PointcutAdvisor) advisor;

            //通过判断当前的类型

            //里边的大概逻辑就是通过method找到对应的匹配表达式,验证

            //是否能够进行匹配。AspectJExpressionPointcut: () sayings()

                            return canApply(pca.getPointcut(), targetClass, hasIntroductions);

                   }

                   else {

                            // It doesn't have a pointcut so we assume it applies.

                            return true;

                   }

         }

下面我们回到之前看下aop代理的实现方式。现在找到对应的增强器。紧接着需要根据增强器生成对应的代理类。

protected Object createProxy(

                            Class<?> beanClass, String beanName, Object[] specificInterceptors, TargetSource targetSource) {

        //根据beanFactory设置exposeTargetClass

                   if (this.beanFactory instanceof ConfigurableListableBeanFactory) {

                            AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory) this.beanFactory, beanName, beanClass);

                   }

        //新建一个代理工厂

                   ProxyFactory proxyFactory = new ProxyFactory();

   //获取属性       

         proxyFactory.copyFrom(this);

        //判断使用的代理方式

                   if (!proxyFactory.isProxyTargetClass()) {

                            if (shouldProxyTargetClass(beanClass, beanName)) {

                                     proxyFactory.setProxyTargetClass(true);

                            }

                            else {

                //添加代理接口

                                     evaluateProxyInterfaces(beanClass, proxyFactory);

                            }

                   }

 

                   Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);

        //添加增强

                   proxyFactory.addAdvisors(advisors);

                   proxyFactory.setTargetSource(targetSource);

                   customizeProxyFactory(proxyFactory);

        //控制代理后,该bean是否还允许被修改

                   proxyFactory.setFrozen(this.freezeProxy);

                   if (advisorsPreFiltered()) {

                            proxyFactory.setPreFiltered(true);

                   }

         //使用工厂创建代理类。

                   return proxyFactory.getProxy(getProxyClassLoader());

         }

进入ProxyFactory

protected final synchronized AopProxy createAopProxy() {

                   if (!this.active) {

                            activate();

                   }

        //获取aopProxyFactory.由于可以采用CGLIB以及JDK自带的代理

       //所以需要在这里选择一个方式

                   return getAopProxyFactory().createAopProxy(this);

         }

进入DefaultAopProxyFactory.有以下方法可以看出,如果当前的类有接口,默认使用jdk代理,当然通过更改ProxyTargetClass属性可以让其使用CGLib代理。Jdk动态代理只能针对接口,而CGLIB是通过继承父类实现。

         public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {

                   if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {

                            Class<?> targetClass = config.getTargetClass();

                            if (targetClass == null) {

                                     throw new AopConfigException("TargetSource cannot determine target class: " +

                                                        "Either an interface or a target is required for proxy creation.");

                            }

                            if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {

                                     return new JdkDynamicAopProxy(config);

                            }

                            return new ObjenesisCglibAopProxy(config);

                   }

                   else {

                            return new JdkDynamicAopProxy(config);

                   }

         }

下面首先进入CglibAopProxy的getProxy中.在当前类通过Cglib通用的Enhancer辅助类,进行创建了代理对象,这里我们只要关注的是getCallback函数中加入了一个

DynamicAdvisedInterceptor,在调用该代理对象的方法时会调用该拦截器的intercept方法。

先留个伏笔,看下jdk动态代理返回的对象。

@Override

         public Object getProxy(ClassLoader classLoader) {

                   if (logger.isDebugEnabled()) {

                            logger.debug("Creating CGLIB proxy: target source is " + this.advised.getTargetSource());

                   }

 

                   try { 

            //获取父类

                            Class<?> rootClass = this.advised.getTargetClass();

                            Assert.state(rootClass != null, "Target class must be available for creating a CGLIB proxy");

 

                            Class<?> proxySuperClass = rootClass;

            //判断当前的类是否是cglib代理过的。其实就是判断是否包含$$。

            //如果是cglib代理的,需要把当前的代理接口拿出来

                            if (ClassUtils.isCglibProxyClass(rootClass)) {

                                     proxySuperClass = rootClass.getSuperclass();

                                     Class<?>[] additionalInterfaces = rootClass.getInterfaces();

                                     for (Class<?> additionalInterface : additionalInterfaces) {

                                               this.advised.addInterface(additionalInterface);

                                     }

                            }

 

                            // 验证。写入相关日志,是否有static方法以及final方法

                            validateClassIfNecessary(proxySuperClass, classLoader);

 

                            // 创建一个Enhancer

                            Enhancer enhancer = createEnhancer();

                            if (classLoader != null) {

                                     enhancer.setClassLoader(classLoader);

                                     if (classLoader instanceof SmartClassLoader &&

                                                        ((SmartClassLoader) classLoader).isClassReloadable(proxySuperClass)) {

                                               enhancer.setUseCache(false);

                                     }

                            }

           //设置要代理的类为父类

                            enhancer.setSuperclass(proxySuperClass);

                            enhancer.setInterfaces(AopProxyUtils.completeProxiedInterfaces(this.advised));

                            enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE);

                            enhancer.setStrategy(new ClassLoaderAwareUndeclaredThrowableStrategy(classLoader));

            //设置拦截器

            //当前会注入很多的拦截器DynamicAdvisedInterceptor为其中之一

            //

Callback[] callbacks = getCallbacks(rootClass);

                            Class<?>[] types = new Class<?>[callbacks.length];

                            for (int x = 0; x < types.length; x++) {

                                     types[x] = callbacks[x].getClass();

                            }

                            // fixedInterceptorMap only populated at this point, after getCallbacks call above

                            enhancer.setCallbackFilter(new ProxyCallbackFilter(

                                               this.advised.getConfigurationOnlyCopy(), this.fixedInterceptorMap, this.fixedInterceptorOffset));

                            enhancer.setCallbackTypes(types);

 

                            //生成代理类

                            return createProxyClassAndInstance(enhancer, callbacks);

                   }

                   catch (CodeGenerationException ex) {

                            throw new AopConfigException("Could not generate CGLIB subclass of class [" +

                                               this.advised.getTargetClass() + "]: " +

                                               "Common causes of this problem include using a final class or a non-visible class",

                                               ex);

                   }

                   catch (IllegalArgumentException ex) {

                            throw new AopConfigException("Could not generate CGLIB subclass of class [" +

                                               this.advised.getTargetClass() + "]: " +

                                               "Common causes of this problem include using a final class or a non-visible class",

                                               ex);

                   }

                   catch (Throwable ex) {

                            // TargetSource.getTarget() failed

                            throw new AopConfigException("Unexpected AOP exception", ex);

                   }

         }

下面进入JdkDynamicAopProxy的getProxy。根据我们对jdk动态代理的理解,通过实现InvocationHandler接口。然后返回一个对象。在进行动态代理的时候,会通过调用invoke方法

public Object getProxy(ClassLoader classLoader) {

                   if (logger.isDebugEnabled()) {

                            logger.debug("Creating JDK dynamic proxy: target source is " + this.advised.getTargetSource());

                   }

                   Class<?>[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true);

                   findDefinedEqualsAndHashCodeMethods(proxiedInterfaces);

         //创建代理

                   return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this);

         }

紧接着我们分析下具体调用方法的时候,aop切入的过程。经过以上的步骤已经创建了对应的增强器,和代理类。进入JdkDynamicAopProxy的invoke

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

                   MethodInvocation invocation;

                   Object oldProxy = null;

                   boolean setProxyContext = false;

        //代理的目标类

                   TargetSource targetSource = this.advised.targetSource;

                   Class<?> targetClass = null;

                   Object target = null;

 

                   try {

                            if (!this.equalsDefined && AopUtils.isEqualsMethod(method)) {

                                     // The target does not implement the equals(Object) method itself.

                                     return equals(args[0]);

                            }

                            else if (!this.hashCodeDefined && AopUtils.isHashCodeMethod(method)) {

                                     // The target does not implement the hashCode() method itself.

                                     return hashCode();

                            }

                            else if (method.getDeclaringClass() == DecoratingProxy.class) {

                                     // There is only getDecoratedClass() declared -> dispatch to proxy config.

                                     return AopProxyUtils.ultimateTargetClass(this.advised);

                            }

                            else if (!this.advised.opaque && method.getDeclaringClass().isInterface() &&

                                               method.getDeclaringClass().isAssignableFrom(Advised.class)) {

                                     // Service invocations on ProxyConfig with the proxy config...

                                     return AopUtils.invokeJoinpointUsingReflection(this.advised, method, args);

                            }

 

                            Object retVal;

 

                            if (this.advised.exposeProxy) {

                                     // Make invocation available if necessary.

                                     oldProxy = AopContext.setCurrentProxy(proxy);

                                     setProxyContext = true;

                            }

 

                            // May be null. Get as late as possible to minimize the time we "own" the target,

                            // in case it comes from a pool.

                            target = targetSource.getTarget();

                            if (target != null) {

                                     targetClass = target.getClass();

                            }

 

                            // 获取拦截器链

                            List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);

 

                            // Check whether we have any advice. If we don't, we can fallback on direct

                            // reflective invocation of the target, and avoid creating a MethodInvocation.

                            if (chain.isEmpty()) {

                                     // We can skip creating a MethodInvocation: just invoke the target directly

                                     // Note that the final invoker must be an InvokerInterceptor so we know it does

                                     // nothing but a reflective operation on the target, and no hot swapping or fancy proxying.

                                     Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);

                                     retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);

                            }

                            else {

                                     // 创建一个methodInvocation

                                     invocation = new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);

                                     //不断的调用proceed

                                     retVal = invocation.proceed();

                            }

 

                            // Massage return value if necessary.

                            Class<?> returnType = method.getReturnType();

                            if (retVal != null && retVal == target &&

                                               returnType != Object.class && returnType.isInstance(proxy) &&

                                               !RawTargetAccess.class.isAssignableFrom(method.getDeclaringClass())) {

                                     // Special case: it returned "this" and the return type of the method

                                     // is type-compatible. Note that we can't help if the target sets

                                     // a reference to itself in another returned object.

                                     retVal = proxy;

                            }

                            else if (retVal == null && returnType != Void.TYPE && returnType.isPrimitive()) {

                                     throw new AopInvocationException(

                                                        "Null return value from advice does not match primitive return type for: " + method);

                            }

                            return retVal;

                   }

                   finally {

                            if (target != null && !targetSource.isStatic()) {

                                     // Must have come from TargetSource.

                                     targetSource.releaseTarget(target);

                            }

                            if (setProxyContext) {

                                     // Restore old proxy.

                                     AopContext.setCurrentProxy(oldProxy);

                            }

                   }

         }

下面进入内部类

public Object proceed() throws Throwable {

                   //      We start with an index of -1 and increment early.

        //判断当前的索引的值和拦截器连的size。

        //执行完了所有的拦截方法,执行切点方法

                   if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {

                            return invokeJoinpoint();

                   }

 

        //获取下个要执行的拦截器

                   Object interceptorOrInterceptionAdvice =

                                     this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);

                   if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {

                            // Evaluate dynamic method matcher here: static part will already have

                            // been evaluated and found to match.

                            InterceptorAndDynamicMethodMatcher dm =

                                               (InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice;

                            if (dm.methodMatcher.matches(this.method, this.targetClass, this.arguments)) {

                                     return dm.interceptor.invoke(this);

                            }

                            else {

                                     // Dynamic matching failed.

                                     // Skip this interceptor and invoke the next in the chain.

                                     return proceed();

                            }

                   }

                   else {

                            // It's an interceptor, so we just invoke it: The pointcut will have

                            // been evaluated statically before this object was constructed.

                            return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);

                   }

         }

通过上面的代码就将拦截器的执行过程构造成一个链路。

如当前的切点有实现了before和after切入。

那么list里边会存放三个拦截器分别为

 ExposeInvocationInterceptor

AspectJAfterAdvice

MethodBeforeAdviceInterceptor

执行ExposeInvocationInterceptor方法的时候如下,将当前的methodInvocat放入线程变量相关,继续执行当前的proceed。

public Object invoke(MethodInvocation mi) throws Throwable {

                   MethodInvocation oldInvocation = invocation.get();

                   invocation.set(mi);

                   try {

                            return mi.proceed();

                   }

                   finally {

                            invocation.set(oldInvocation);

                   }

         }

当前的索引进行++.进行AspectJAfterAdvice的invoke方法。从代码中可以看出,当前的代码继续执行methodInvocation的proceed。

@Override

         public Object invoke(MethodInvocation mi) throws Throwable {

                   try {

                            return mi.proceed();

                   }

                   finally {

                            invokeAdviceMethod(getJoinPointMatch(), null, null);

                   }

         }

而执行mi.proceed,当前的索引有重复++,执行对应的MethodBeforeAdviceInterceptor得invke方法

@Override

         public Object invoke(MethodInvocation mi) throws Throwable {

       //先调用before的增强方法

                   this.advice.before(mi.getMethod(), mi.getArguments(), mi.getThis() );

       //继续调用method.procced,当前调用要执行具体的切入点方法

     //执行完,返回上一层。

                   return mi.proceed();

         }

经过上面,整个链路就执行完毕。

 

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值