Spring AOP 拦截器调用的实现

Spring AOP 拦截器调用的实现

前言

本篇是这个系列的正式内容最后一篇,主要是对拦截器相关设计原理介绍和具体实现源码的分析,也会和前面的内容串联起来进行回顾,加深印象。

相关文章

项目环境

1.设计原理

在 Spring AOP 通过 JDK 的 Proxy 方式或者 CGLIB 方式生成代理对象的时候,相关的拦截器已经配置到代理对象中去了,拦截器在代理对象中起作用是通过对这些方法的回调来完成的。

如果使用 JDK 的 Proxy 来生成代理对象,那么需要通过 InvocationHandler 来设置拦截器回调;而使用 CGLIB 来生成代理对象,就需要根据 CGLIB 的使用要求,通过 DynamicAdvisedInterceptor 来完成回调。

2.JdkDynamicAopProxy 的 invoke 拦截

前面的文章介绍了在 Spring 中通过 PorxyFactoryBean 实现 AOP 功能的第一步,得到 AopProxy 代理对象的基本过程,以及通过使用 JDK 和 CGLIB 最终产生 AopProxy 代理对象的实现原理。下面来看看 AopProxy 代理对象的拦截机制是怎样发挥作用和实现 AOP 功能的。

在 JdkDynamicAopProxy 中生成 Proxy 对象时,我们回顾一下 AopProxy 代理对象的生成调用,相关代码如下:

  • org.springframework.aop.framework.JdkDynamicAopProxy#getProxy(java.lang.ClassLoader)
Proxy.newProxyInstance(classLoader, proxiedInterfaces, this);

这里的 this 参数对应的是 JdkDynamicAopProxy 本身,而 JdkDynamicAopProxy 实现了 InvocationHandler 接口,InvocationHandler 是 JDK 定义的反射类的一个接口,这个接口定义了 invoke 方法,而这个 invoke 方法是作为 JDK Proxy 代理对象进行拦截的回调入口出现的。

当 Proxy 对象的代理方法被调用时,JdkDynamicAopProxy 的 invoke 方法作为 Proxy 对象的回调函数被触发,而通过 invoke 的具体实现来完成对目标对象方法调用的拦截或者说功能增强。

代码清单如下:

	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		Object oldProxy = null;
		boolean setProxyContext = false;

		TargetSource targetSource = this.advised.targetSource;
		Object target = null;

		try {
			if (!this.equalsDefined && AopUtils.isEqualsMethod(method)) {
                // 如果目标对象没有实现 Object 类的 equals 方法
                // 并且当前执行的方法是 equals 方法
				return equals(args[0]);
			}
			else if (!this.hashCodeDefined && AopUtils.isHashCodeMethod(method)) {
                // 如果目标对象没有实现 Object 类的 hashCode 方法
                // 并且当前执行的方法是 hashCode 方法
				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;
			}

			// Get as late as possible to minimize the time we "own" the target,
			// in case it comes from a pool.
            // 得到目标对象
			target = targetSource.getTarget();
			Class<?> targetClass = (target != null ? target.getClass() : null);

			// Get the interception chain for this method.
            // 获取定义好的拦截器链
			List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);

			if (chain.isEmpty()) {// 如果拦截器链为空
                // 直接调用 Target 的对应方法
				Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
				retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);
			}
			else {
				// We need to create a method invocation...
                // 如果有拦截器的设置,那么需要先调用拦截器
				MethodInvocation invocation =
						new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
				// Proceed to the joinpoint through the interceptor chain.
                // 沿着拦截器链继续执行
				retVal = invocation.proceed();
			}

			// Massage return value if necessary.
			Class<?> returnType = method.getReturnType();
            ...// 返回值处理
			return retVal;
		}
		finally {
			if (target != null && !targetSource.isStatic()) {
				// Must have come from TargetSource.
				targetSource.releaseTarget(target);
			}
			if (setProxyContext) {
				// Restore old proxy.
				AopContext.setCurrentProxy(oldProxy);
			}
		}
	}

从代码清单中可以看到,对 Proxy 对象的代理设置是在 invoke 方法中完成,这些设置包括目标对象、拦截器链,同时把这些对象作为输入,创建了 ReflectiveMethodInvocation 对象,通过这个 ReflectiveMethodInvocation 对象来完成对 AOP 功能实现的封装。

在这个 invoke 方法中,包含了一个完整的拦截器链对目标对象的拦截过程,比如获得拦截器链中的拦截器进行配置,逐个运行拦截器链里的拦截增强,直到拦截器链中的拦截器都完成以上的拦截过程为止。

3.CglibAopProxy 的 intercept 拦截

在分析CglibAopProxy 和 AopProxy 代理对象生成的时候,我们了解到 AOP 的拦截调用,相关的回调是在 DynamicAdvisedInterceptor 对象中实现的,这个回调的实现在 intercept 方法中。

代码清单如下:

		public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
			Object oldProxy = null;
			boolean setProxyContext = false;
			Object target = null;
			TargetSource targetSource = this.advised.getTargetSource();
			try {
				if (this.advised.exposeProxy) {
					// Make invocation available if necessary.
					oldProxy = AopContext.setCurrentProxy(proxy);
					setProxyContext = true;
				}
				// 获得目标对象
				target = targetSource.getTarget();
				Class<?> targetClass = (target != null ? target.getClass() : null);
				// 从 advised 中获取配置的拦截器链 advice 通知
                List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
				Object retVal;
				//如果拦截器链为空并且方法修饰符为 public
				if (chain.isEmpty() && Modifier.isPublic(method.getModifiers())) {
					// 直接执行目标方法
					Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
					retVal = methodProxy.invoke(target, argsToUse);
				}
				else {
					// We need to create a method invocation...
                    // 沿着拦截器链继续执行
					retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed();
				}
                // 返回值处理
				retVal = processReturnType(proxy, target, method, retVal);
				return retVal;
			}
			finally {
				if (target != null && !targetSource.isStatic()) {
					targetSource.releaseTarget(target);
				}
				if (setProxyContext) {
					// Restore old proxy.
					AopContext.setCurrentProxy(oldProxy);
				}
			}
		}

从代码清单可以看到 CglibAopProxy 的 intercept 回调方法的实现和 JdkDynamicAopProxy 的回调实现非常类似,只是在 CglibAopProxy 中构造 CglibMethodInvocation 对象来完成拦截链的调用,而在 JdkDynamicAopProxy 中是通过构造 ReflectiveMethodInvocation 对象来完成这个功能。

4.目标对象方法的调用

如果没有设置拦截器,那么会对目标方法直接进行调用。对于 JdkDynamicAopProxy 代理对象,这个对目标对象的方法调用时通过 AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse); 进行调用。

AopUtils#invokeJoinpointUsingReflection 方法代码如下:

可以看到就是直接使用 method.invoke 反射调用。

	public static Object invokeJoinpointUsingReflection(@Nullable Object target, Method method, Object[] args)
			throws Throwable {

		// Use reflection to invoke the method.
		try {
            // 使用反射调用 target 对象方法
			ReflectionUtils.makeAccessible(method);
			return method.invoke(target, args);
		}
        ... //异常处理
	}

对于使用 CglibAopProxy 的代理对象,它对目标对象的调用是通过 CGLIB 的 MethodProxy 对象来直接完成的,这个对象的使用是由 CGLIB 的设计决定的。具体的调用在 CglibAopProxy.DynamicAdvisedInterceptor#intercept 方法中可以看到,使用的是 CGLIB 封装好的功能,和 JdkDynamicAopProxy 实现的功能一样都是完成对目标对象方法的调用,相关的代码如下:

Object retVal = methodProxy.invoke(this.target, args);

5.AOP拦截器链的调用

前面介绍了 JDK 和 CGLIB 会生成不同的 AopProxy 代理对象,从而构造了不同的回调方法来启动对拦截器链的调用,比如在 JdkDynamicAopProxy 中的 invoke 方法,以及在 CglibAopProxy.DynamicAdvisedInterceptor#intercept 方法。虽然他们使用了不同的 AopProxy 代理对象,但是最终对 AOP 拦截的处理却是一样的,他们对连接器链的调用都是通过 ReflectiveMethodInvocation#proceed 方法实现的。

在 proceed 方法中,会逐个运行拦截器的拦截方法。在运行拦截器的拦截方法之前,需要对象代理方法完成一个匹配判断,通过这个匹配判断来决定拦截器是否满足切面增强的要求。

proceed 方法设计的相当巧妙代码,利用递归的思想,清单如下:

	public Object proceed() throws Throwable {
		// 从索引为 -1 的拦截器开始调用,按序递增
        // 如果拦截器链中的拦截器迭代调用完毕,调用 target 对应的方法,这里是通过反射直接调用
		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;
			Class<?> targetClass = (this.targetClass != null ? this.targetClass : this.method.getDeclaringClass());
            // 对拦截器进行动态匹配判断,这里和 Pointcut 的现实类型相关
			if (dm.methodMatcher.matches(this.method, targetClass, this.arguments)) {
				return dm.interceptor.invoke(this);// 执行拦截器中的方法
			}
			else {
				// 如果匹配失败,那么 proceed 递归调用,直到所有的拦截器都被运行为止
				return proceed();
			}
		}
		else {
			// 如果是一个 interceptor,直接调用 interceptor 对应方法
            // 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);
		}
	}

从上面的代码清单可以看到,先进行拦截器索引位置判断,如果现在已经运行到拦截器的末尾,那么就会直接调用目标对象的相应方法,否则沿着拦截器继续执行,得到下一个拦截器。

这里有两种情况的:

  • 第一种,拦截器的类型是动态(InterceptorAndDynamicMethodMatcher),则通过这个拦截器进行动态 matches 判断,判断是否适用于横切增强的场合,如果是,从拦截器中得到通知器,并启动通知器的 invoke 方法进行切面增强。如果匹配失败,会迭代调用 proceed 方法,直到拦截器链中的拦截器都完成以上的拦截过程为止。

  • 第二种,拦截器的类型是静态,那么直接执行 invoke 方法,然后 invoke 方法中会递归调用 proceed 方法,直到拦截器都完成以上的拦截过程为止。

这一段原文有相应的注释,意思是这个 pointcut 在这个目标对象构造之前已经被静态计算过。

            // It's an interceptor, so we just invoke it: The pointcut will have
			// been evaluated statically before this object was constructed.

6.配置通知器

拦截器(通知器)链 interceptorsAndDynamicMethodMatchers 对象是在哪里被初始化的,然后又是如何传到 ReflectiveMethodInvocation#proceed 方法中的呢?

我们可以通过 Idea 的 Find Usages 来查找 ReflectiveMethodInvocation 在哪些地方用到了
在这里插入图片描述
找到在 org.springframework.aop.framework.JdkDynamicAopProxy#invoke 方法中,相关代码如下:

先查找到 chain,然后构造 ReflectiveMethodInvocation,最后执行 proceed 方法。

			// Get the interception chain for this method.
			List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
                ...
				// We need to create a method invocation...
				MethodInvocation invocation =
						new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
				// Proceed to the joinpoint through the interceptor chain.
				retVal = invocation.proceed();
			}

继续往下寻找,最终获取拦截器链方法的在 DefaultAdvisorChainFactory#getInterceptorsAndDynamicInterceptionAdvice 中,代码清单如下(省略掉部分代码):

	public List<Object> getInterceptorsAndDynamicInterceptionAdvice(
			Advised config, Method method, @Nullable Class<?> targetClass) {

		// This is somewhat tricky... We have to process introductions first,
		// but we need to preserve order in the ultimate list.
		AdvisorAdapterRegistry registry = GlobalAdvisorAdapterRegistry.getInstance();
		Advisor[] advisors = config.getAdvisors();// 从配置中获取 Advisor 通知器集合
		List<Object> interceptorList = new ArrayList<>(advisors.length);
		Class<?> actualClass = (targetClass != null ? targetClass : method.getDeclaringClass());
		Boolean hasIntroductions = null;

		for (Advisor advisor : advisors) {// 循环遍历通知器集合
			if (advisor instanceof PointcutAdvisor) {
                   ...// 判断是否匹配
					if (match) {
						MethodInterceptor[] interceptors = registry.getInterceptors(advisor);
						if (mm.isRuntime()) {// 判断是否是动态
							for (MethodInterceptor interceptor : interceptors) {
								interceptorList.add(new InterceptorAndDynamicMethodMatcher(interceptor, mm));
							}
						}
						else {// 静态
							interceptorList.addAll(Arrays.asList(interceptors));
						}
					}
				}
			}
             ...//其他场景
		}

		return interceptorList;
	}

从代码清单中可以看到,在这个拦截器链的获取过程中,首先从配置中获取 Advisor 通知器集合,然后设置了一个 List,长度就是通知器的个数。然后 DefaultAdvisorChainFactory 通过一个 AdvisorAdapterRegistry 来实现拦截器的注册,AdvisorAdapterRegistry 对 advice 通知的整合起了很大的作用,相关的内容在下个小节中会进行讨论。

有了 AdvisorAdapterRegistry 注册器,利用它来对从 ProxyFactoryBean 配置中得到的通知进行适配,从而获得相应的拦截器,再把它加入到前面设置好的 List 中,完成拦截器的注册过程。

在注册完成之后,List 中的拦截器会被 JDK 生产的 AopProxy 代理对象的 invoke 方法或者 CGLIB 中的 intercept 方法获得,并启动拦截器的 invoke 调用,最终触发通知的切面增强。

7.Advice通知的实现

经过前面的分析,我们看到在 AopProxy 代理对象生成时,其拦截器也同样也建立起来了,而且我们还了解到拦截器的拦截调用和最终目标对象方法调用的实现原理。本小节,我们将讨论 Spring AOP 定义的通知是怎么实现对目标对象增强的。

相关的核心实现类 DefaultAdvisorAdapterRegistry 代码清单:

public class DefaultAdvisorAdapterRegistry implements AdvisorAdapterRegistry, Serializable {

	private final List<AdvisorAdapter> adapters = new ArrayList<>(3);


	/**
	 * Create a new DefaultAdvisorAdapterRegistry, registering well-known adapters.
	 */
	public DefaultAdvisorAdapterRegistry() {
        // 初始化 adapters 集合
		registerAdvisorAdapter(new MethodBeforeAdviceAdapter());
		registerAdvisorAdapter(new AfterReturningAdviceAdapter());
		registerAdvisorAdapter(new ThrowsAdviceAdapter());
	}


	@Override
	public Advisor wrap(Object adviceObject) throws UnknownAdviceTypeException {
        ...// 场景无关
	}

	@Override
	public MethodInterceptor[] getInterceptors(Advisor advisor) throws UnknownAdviceTypeException {
		List<MethodInterceptor> interceptors = new ArrayList<>(3);
		Advice advice = advisor.getAdvice();
		if (advice instanceof MethodInterceptor) {
			interceptors.add((MethodInterceptor) advice);
		}
		for (AdvisorAdapter adapter : this.adapters) {
			if (adapter.supportsAdvice(advice)) {// 判断通知类型
				interceptors.add(adapter.getInterceptor(advisor));
			}
		}
		if (interceptors.isEmpty()) {
			throw new UnknownAdviceTypeException(advisor.getAdvice());
		}
		return interceptors.toArray(new MethodInterceptor[0]);
	}

	@Override
	public void registerAdvisorAdapter(AdvisorAdapter adapter) {
		this.adapters.add(adapter);
	}

}

逻辑比较简单分为两步

  • 第一在构造的时候,初始化 adapters 集合,将 MethodBeforeAdviceAdapter、AfterReturningAdviceAdapter、ThrowsAdviceAdapter 三类不同通知的适配器加入到集合中。
  • 第二步将通知器 advisor 中的通知 advice 遍历,通过适配器判断对应的类型,加入到拦截器链集合中返回

适配器的实现我们以 MethodBeforeAdviceAdapter 为例,其他的类似

  • supportsAdvice 判断类型,使用 instanceof 关键字
  • getInterceptor 强转之后封装成对应的通知类型
class MethodBeforeAdviceAdapter implements AdvisorAdapter, Serializable {

	@Override
	public boolean supportsAdvice(Advice advice) {
		return (advice instanceof MethodBeforeAdvice);
	}

	@Override
	public MethodInterceptor getInterceptor(Advisor advisor) {
		MethodBeforeAdvice advice = (MethodBeforeAdvice) advisor.getAdvice();
		return new MethodBeforeAdviceInterceptor(advice);
	}

}

到这里拦截器链的封装就完成了。接着这条线索,我们继续看 MethodBeforeAdviceInterceptor 类,看看它是怎样完成 advice 的封装的,MethodBeforeAdviceInterceptor 的代码清单如下:

public class MethodBeforeAdviceInterceptor implements MethodInterceptor, BeforeAdvice, Serializable {

	private final MethodBeforeAdvice advice;


	/**
	 * Create a new MethodBeforeAdviceInterceptor for the given advice.
	 * @param advice the MethodBeforeAdvice to wrap
	 */
    // 为指定的 Advice 创建对应的 MethodBeforeAdviceInterceptor 对象
	public MethodBeforeAdviceInterceptor(MethodBeforeAdvice advice) {
		Assert.notNull(advice, "Advice must not be null");
		this.advice = advice;
	}


	@Override
    // 这个 invoke 方法是拦截器的回调方法,会在代理对象的方法被调用是触发回调
	public Object invoke(MethodInvocation mi) throws Throwable {
		this.advice.before(mi.getMethod(), mi.getArguments(), mi.getThis());
		return mi.proceed();
	}

}

从代码清单中可以看到,首先触发了 adivice 的 before 回调,然后才是 MethodInvocation 的 proceed 方法的调用。 看到这里,就可以和前面 ReflectiveMethodInvocation#proceed 的分析联系起来了。

我们再来回顾一下,在 AopProxy 代理对象触发的 ReflectiveMethodInvocation#proceed 方法中,在取得拦截器以后,调用拦截器的 invoke 方法。

按照 AOP 的配置规则,ReflectiveMethodInvocation 触发的拦截器 invoke 方法,最终会根据不同的 advice 类型,触发不同的 advice 拦截器的封装,比如 MethodBeforeAdvice,最终会触发 MethodBeforeAdviceInterceptor 的 invoke 方法。

最后在 MethodBeforeAdviceInterceptor#invoke 方法中,会先调用 advice 的 before 方法,这就是 MethodBeforeAdvice 所需要的对目标对象的增强效果,然后调用 proceed 方法进行沿着拦截器链执行,直到所有的拦截器都完成以上的拦截过程为止。

其他的两只通知器类型类似,这里就不做分析了。

8.参考

  • 《Spring技术内幕:深入解析Spring架构与设计原理(第2版)》- 计文柯
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Spring AOP中,拦截器责任链处理过程是指当目标方法被多个通知匹配到时,Spring通过引入拦截器链来保证每个通知的正常执行。拦截器链是由一系列的拦截器组成的,每个拦截器都负责在目标方法的前后执行特定的逻辑。 在源码中,拦截器责任链的处理过程主要通过MethodInvocation接口来实现。MethodInvocation接口提供了proceed()方法,用于执行拦截器链中下一个拦截器的逻辑。当调用proceed()方法时,会按照拦截器链的顺序依次执行每个拦截器的逻辑,直到达到链的末尾或者某个拦截器决定终止链的执行。 在拦截器责任链处理过程中,每个拦截器可以在目标方法的调用前后执行自定义的逻辑。拦截器可以对方法的参数进行检查、修改方法的返回值,或者在方法执行前后记录日志等操作。通过拦截器责任链的处理,Spring AOP能够实现面向切面编程的功能。 需要注意的是,拦截器链的执行顺序是根据拦截器的配置顺序确定的。在Spring的配置文件中,可以通过配置拦截器的顺序来控制拦截器链的执行顺序。这样可以确保每个拦截器按照预期的顺序执行,从而达到期望的功能效果。 总结起来,Spring AOP源码的拦截器责任链处理过程主要通过MethodInvocation接口实现,它通过调用proceed()方法来依次执行拦截器链中每个拦截器的逻辑。拦截器链的执行顺序可以通过配置文件来控制,从而实现面向切面编程的功能。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [Spring AOP源码:拦截器责任链处理过程](https://blog.csdn.net/weixin_45031612/article/details/128806966)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 33.333333333333336%"] - *2* [Spring AOP 自动代理源码 DefaultAdvisorAutoProxyCreator](https://download.csdn.net/download/weixin_38530536/14854229)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 33.333333333333336%"] - *3* [【SpringSpring AOP 源码分析-拦截器链的执行过程(四)](https://blog.csdn.net/qq_46514118/article/details/121912507)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 33.333333333333336%"] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值