Spring Aop 的原理解析

AOP概念:

连接点(joincut) 就是 可以被切入的所有方法
切入点(pointcut)就是你要在哪个方法上切入 的信息,这个信息就是切入点。
通知(advice)就是你要在那个方法前后要执行的具体方法
切面则是定义切入点和通知的组合
把切面应用到目标函数的过程称为织入(weaving)。
像下面这个类:

public class UserService {
    public void addUser(){}
    public void modifyUser(){}
    public void deleteUser(){}
}

连接点(joinpoint) 就是指哪些方法可以被拦截(上面就是addUser()modifyUser()deleteUser()
切入点(pointcut) 就是指定 具体在哪个方法上进行切入 的信息
通知 advice 在某个切入点上需要执行的代码,如日志记录和权限验证
切面(aspect) 由切入点和通知 组合而成
织入(weaving) 把切面的代码织入到目标函数的过程

织入又分静态织入和动态织入,
静态织入: 先将切面(aspect)类编译成class字节码之后,在Java目标类编译时织入,即先编译aspect类再编译目标类
**动态织入:**在运行时动态地将要增强的代码织入到目标类中,这样往往是通过动态代理技术完成。

Spring Aop示例

Spring Aop采用的是 动态织入,使用jdk和CGLIB来做动态代理。
我们先来做一个实例,然后再分析原理:
创建一个 SpringBoot 项目,然后导入依赖:

<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-web</artifactId>
</dependency>

然后编写Controller类:

@RestController
public class MyController {
    @RequestMapping("/hello")
    public String hello(){
        System.out.println("hello");
        return "hello";
    }
}

编写切面类:

@Component
@Aspect
public class WebAspect {
    @Pointcut("execution(public * com.learning.controller..*.*(..)))")
    public void controllerAspect(){}  //可以理解成 切入点的名称
    @Before("controllerAspect()")
    public void beforeMethod(){
        System.out.println("我在执行请求之前做操作");
    }
    @After("controllerAspect()")
    public void afterMethod(){
        System.out.println("我在执行请求之后做操作");
    }
}

@Aspect是告诉Spring容器,这个是一个切面类。
既然这个类是一个 切面(aspect),那么必须包含有 切入点(pointcut)和通知(advice).
其中
@Pointcut就是说明这是一个切入点,我们这里配置了 表达式,execution是指当执行匹配的那个方法时,
用法是:

execution(方法修饰符(可选)  返回类型  类路径 方法名  参数  异常模式(可选))

还有其他的匹配表达式:

execution: 匹配连接点

within: 某个类里面

this: 指定AOP代理类的类型

target:指定目标对象的类型

args: 指定参数的类型

bean:指定特定的bean名称,可以使用通配符(Spring自带的)

@target: 带有指定注解的类型

@args: 指定运行时传的参数带有指定的注解

@within: 匹配使用指定注解的类

@annotation:指定方法所应用的注解

注意,方法修饰符必须要是 public的,因为动态代理只能拦截那些能访问到的方法,所以尽量不要用其他修饰符

@Pointcut修饰的方法名就是切入点的名字,用来标识这个切入点。

@Before() ,@After(), @Around() @AfterReturning @AfterThrowing,就是定义通知(advice)

@Around(): 它的方法的参数一定要ProceedingJoinPoint,这个对象是JoinPoint的子类。我们可以把这个看作是切入点的那个方法的替身,这个proceedingJoinPoint有个 proceed() 方法,相当于就是那切入点的那个方法执行

@AfterReturning : 是在目标方法正常完成后把增强处理织入

@AfterThrowing: 异常抛出后织入的增强

Spring AOP 的源码分析:

Spring AOP 对我们上面的一些概念都做了抽象接口。

切入点(pointcut) 的接口就是 Pointcut:
在这里插入图片描述
我们前面说了 切入点 是指定具体在哪个方法上进行切入,所以接口里面就定义了 ClassFilterMethodMatcher这两个接口,指定具体在哪个类的哪个方法
分别看一下这两个接口:
ClassFilter接口 :
在这里插入图片描述
很简单,如果class类型匹配的话,matches()就会返回true,否则返回false,false就不会对该 连接点(Joincut)所在的类进行切入

MethodMatcher接口:

public interface MethodMatcher {
	boolean matches(Method method, Class<?> targetClass);
	boolean isRuntime();
	boolean matches(Method method, Class<?> targetClass, Object... args);
	MethodMatcher TRUE = TrueMethodMatcher.INSTANCE;
}

这里面有两个 matches() 方法,一个带了连接点方法的参数,一个没有,具体执行哪一个则是看 isRuntime() 方法来确定。

  1. 如果isRuntime() 返回false,表示不会考虑具体的 Joincut的方法参数,这种类型的MethodMatch称之为 StaticMethodMatcher,因为不用每次都检查参数,那么对于同样类型的方法匹配结构,就可以在框架内部缓存来提高性能。 所以 isRuntime() 返回false,则 matches(Method method, Class<?> targetClass)这个方法的匹配结果会成为其所属的 PointCut 接口的主要依据
  2. 如果isRuntime() 返回true,表示每次都对方法调用的参数进行匹配检查,叫做DynamicMethodMatcher ,检查时仍会先调用两个参数的matches() 方法,只有返回true时才会调用三个参数的matches() 方法去匹配。性能差,最好少使用。

Pointcut 接口 深入

看一下该接口的实现类:
在这里插入图片描述

NameMatchMethodPointcut

这是最简单的Pointcut 实现,见名知义,肯定就是根据指定的匹配字符串 和 连接点(JoinCut) 的方法名进行匹配 。
缺点就是无法对重载的方法进行区分,因为它只检查方法名称

JdkRegexpMethodPointcut

看到该实现类里面的 Regex ,我们就知道该类肯定是基于正则表达式来实现匹配的。
使用参考下面:

public class test1 {
    public static void main(String[] args) {
        JdkRegexpMethodPointcut pointcut = new JdkRegexpMethodPointcut();
        pointcut.setPattern("*.dosth.*");
    }
    public void dosth(){
        System.out.println("do sth");
    }
}

我们注意到,匹配的字符串是 *.dosth.* ,这是因为 ,使用该实现类的匹配模式必须以整个方法签名的形式指定,而不能像上面那个实现类一样仅指定 方法名称

AnnotationMatchingPointcut

见名知义,肯定是根据是否存在某注解来匹配 JoinPoint
示例:

AnnotationMatchingPointcut pointcut = new AnnotationMatchingPointcut(RequestMapping.class, RestController.class);

像这样的定义,就会将 所有 @RestController标注的类的所有@RequestMapping标注的方法作为 切入点(PointCut)

ComposablePointcut

该实现类就是可以实现几个 Pointcut 之间的交集或者并集
示例:

 public static void main(String[] args) {
        AnnotationMatchingPointcut pointcut = new AnnotationMatchingPointcut(RequestMapping.class, RestController.class);
        NameMatchMethodPointcut pointcut1 = new NameMatchMethodPointcut();
        pointcut1.addMethodName("dosth");
        ComposablePointcut composablePointcut = new ComposablePointcut();
        composablePointcut = composablePointcut.union(pointcut);
    }
ControlFlowPointcut

比如一个方法 method1() 被作为切入点(pointcut),其他的实现类都在这个方法被执行的时候就切入,但是这个实现类可以判断是哪一个方法调用的它,如果是被指定的方法调用的话才会切入。

Advice接口 深入

通知 advice 是在某个切入点上需要执行的具体逻辑 ,在Spring AOP中对应的接口是 Advice

根据 Advice 实例能否在所有目标对象类的所有实例中共享这一标准 ,可以划分为 per-classper-instance 类型的 Advice,per-class的 Advice只是提供方法拦截的功能,不会为目标对象类保存任何状态或者添加新的特性,
在这里插入图片描述

Spring AOP中 BeforeAdvice和AfterAdvice 都是 per-class,Interceptor 则是 per-instance

  1. BeforeAdvice 所实现的横切逻辑将在对应的 Joincut 之前执行,在 BeforeAdvice 执行完成之后,程序将会从Joincut 处继续执行。我们可以使用 BeforeAdvice 来进行整个系统的某些资源初始化,或者其他的一些准备工作。
  2. ThrowsAdvice这个就是 在抛出异常之后执行的切面逻辑,
    我们可以提供实现该接口来实现对系统中特定的异常情况进行监控。虽然该接口没有定义任何方法,但是我们在实现类中定义方法,仍然需要按照一定的规则: void afterThrowing(Method m ,Object[] args,Object target,Throwable t)(前三个参数是可以省略的)
  3. AfterReturningAdvice这个则是在方法执行成功之后进行的处理逻辑,其中定义了 afterReturning()方法,有个缺点就是,该接口只能访问方法的返回值,但却不能修改返回值
  4. 我们在前面的AOP理论中介绍了 Around Advice,那么在Spring AOP中对应的接口就是MethodInterceptor

Around Advice - MethodInterceptor

在这个接口中,定义了一个方法:

@Nullable
Object invoke(@Nonnull MethodInvocation invocation) throws Throwable;

其中MethodInvocation 接口定义了这样的方法:

@Nonnull
Method getMethod();

这就是获得当前的Joincut的方法,我们可以决定具体何时再调用该方法。
所以我们可以在 Joincut的逻辑执行之前或者之后插入相应的逻辑,甚至捕获 Joincut方法可能抛出的异常

Aspect(Advisor) 接口 深入

前面已经说过了,切面(aspect) 由切入点和通知 组合而成
当 PointCut 和 Advice 都准备好之后,就需要将它们装入到 切面(Aspect) 中去

Spring中的 Aspect 是 Advisor。正常来说,一个Aspect可以有多个Pointcut 和 多个 Advice ,但是Spring的 Advisor 只有一个 Pointcut和一个Advice,所以Spring中的 Advisor 是特殊的切面(aspect)
在这里插入图片描述
Spring AOP 的 Advisor接口层次如上。

实际上,看 Advisor接口:

public interface Advisor {
    Advice EMPTY_ADVICE = new Advice() {};
    Advice getAdvice();
    boolean isPerInstance();
}

它本身只有 (通知)Advice
到了 PointcutAdvisor接口才算一个有 Pointcut 和 Advice 的完整的 Advisor:

public interface PointcutAdvisor extends Advisor {
	Pointcut getPointcut();
}

下面来看一下具体的几个实现:

  1. DefaultPointcutAdvisor 这是最通用的PointcutAdvisor 实现,任何类型的Pointcut和任何类型的Advice都可以通过DefaultPointcutAdvisor来使用,可以直接通过构造方法或者 settergetter来注入Pointcut和Advice
  2. NameMatchMethodPointcutAdvisor是细化后的 DefaultPointcutAdvisor ,它限定自身能使用的 Pointcut类型为 NameMatchMethodPointcut,并且外部不可更改。不过 Advice任何类型均可用。
  3. RegexpMethodPointcutAdvisor这个类也是限定了自己能使用的 Pointcut类型,强制为AbstractRegexpMethodPointcut类型,默认使用的是 JdkRegexpMethodPointcut这个实现类
  4. DefaultBeanFactoryPointcutAdvisor这个是较少使用的一个实现类,它的唯一作用是可以通过 BeanName 来在容器里面找到具体的 Bean的 Pointcut 和 Advice ,其他和第一个没有什么区别。

织入

前面的概念已经说了,织入(weaving) 把切面的代码织入到目标函数的过程。
在Spring AOP中,org.springframework.aop.framework.ProxyFactory 这个类是最基本的织入器。

使用ProxyFactory需要两个最基本的东西 。

  1. 第一个是要对其进行织入的目标对象
  2. 第二个是将要应用到目标对象的 Aspect (Advisor),

我们知道Spring的织入过程是 使用JDK动态代理(类实现了一个接口)和 CGLIB代理(类没有实现任何接口) 。

ProxyFactory 深入:

这个类一看就是工厂类,所以只要看它提供产品类的方法即可:

public Object getProxy() {
	return createAopProxy().getProxy();
}

然后再去找 createAopProxy()方法,

protected final synchronized AopProxy createAopProxy() {
	if (!this.active) {
		activate();
	}
	return getAopProxyFactory().createAopProxy(this);
}

可以看到,它返回的是一个 AopProxy 接口。在这里插入图片描述
接口层次如上,可知,这个 AopProxy接口应该就是拿来做具体的织入过程的。

AopProxy 是通过getAopProxyFactory().createAopProxy(this);这个工厂方法来获取的
在这里插入图片描述
找到这个方法,发现到子类去实现了,所以我们再到子类去看:

@Override
	public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
		if (!NativeDetector.inNativeImage() &&
				(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);
		}
	}

伪代码如下:

if((config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)))){
// 创建CglibAopProxy实例返回
}else{
//创建JdkDynamicAopProxy实例返回
}

也就是说,如果传入的AdvisedSupport实例config的isOptimize或者 isProxyTargetClass为true或者对象没有实现任何接口,那么就使用 CGLIB去代理,否则 使用 JDK去代理

那么 这个传入的AdvisedSupport实例又是什么呢?

AdvisedSupport

来看一下它的类层次
AdvisedSupport
里面的 ProxyConfig仅定义了五个属性,分别控制在生成代理对象的时候,应该采取怎么样的措施:
在这里插入图片描述

  1. proxyTargetClass 如果为true,则使用 CGLIB进行代理。默认为false
  2. optimize主要用于告知代理对象是否需要采取进一步的优化措施。 如果为true,则使用CGLIB代理,默认false
  3. opaque该属性用于控制生成的代理对象是否可以强制转换成 Advised
  4. exposeProxy该属性可以让Spring AOP在生成代理对象时,将其绑定到 ThreadLocal
  5. frozen为true,则一旦针对代理对象生成的各项信息配置完成,则不容许更改

要生成代理对象,仅仅靠 ProxyConfig提供的这几个属性完全不够,我们还需要生成代理对象的一些具体信息,如,要针对哪些目标类生成代理对象,要为代理对象加入哪些横切逻辑等这些信息可以通过 Advised接口查询到,简单地说,我们可以使用 Advised接口访问相应代理对象所持有的Advisor,进行添加、移除Advisor的操作

AdvisedSupport继承了 ProxyConfig,我们可以设置代理对象生成的一些控制属性,实现了Advised接口,我们就可以设置生成代理对象相关的目标类、Advice等必要信息。因此,具体的AopProxy实现在生成代理对象的时候,可以从 AdvisedSupport处获得所有必要的信息

重回 ProxyFactory

描述
然后ProxyFactory 类继承了 AdvisedSupport,又能够通过 createAopProxy() 方法来获取AopProxy。所以我们既可以通过 ProxyFactory 设置生成代理对象的所需要的相关信息,也可以取得最终生成的代理对象,前者是 AdvisedSupport的职责,后者是 AopProxy的职责

ProxyFactoryBean

在这里插入图片描述
ProxyFactory只是最普通的一个 织入器。
ProxyFactoryBean这个类将Spring AOP和Spring IOC容器支持相结合,使我们可以在 容器中对 切入点(PointCut)和通知(Advice)管理更容易。

ProxyFactoryBean可以这样理解 Proxy+ FactoryBean,在IOC容器中,FactoryBean的作用是存储一个对象,如果容器中的某个对象持有某个FactoryBean的引用,那它取得的不是 FactoryBean实例本身,而是 getObject() 方法返回的对象。因此,如果容器中某个对象依赖了 ProxyFactoryBean的实例,那它就会使用到通过 getObject() 返回的代理对象。

因为 ProxyFactoryBean继承了 ProxyCreatorSupport 这个类,而这个类又已经把需要做的事情基本完成了(如设置目标对象,配置各种属性,生成对应的AopProxy对象),所以 ProxyFactoryBean做的主要事情就是拿出AopProxy ,调用它的 getProxy()拿到代理对象

//简化代码
public Object getObject() throws BeansException {
		if (isSingleton()) {
			return getSingletonInstance();
		}
	}
private synchronized Object getSingletonInstance() {
		if (this.singletonInstance == null) {
			this.singletonInstance = getProxy(createAopProxy());
		}
		return this.singletonInstance;
	}
protected Object getProxy(AopProxy aopProxy) {
		return aopProxy.getProxy(this.proxyClassLoader);
	}

可以发现,确实如我们所想

自动化织入过程

Spring AOP 的自动代理是建立在IOC容器的 BeanPostProcessor概念之上。使用BeanPostProcessor,我们可以在遍历容器中所有bean的基础上,对遍历到的bean进行一些操作。
我们只需要提供一个BeanPostProcesser ,然后在这个BeanPostProcesser 内部实现这样的逻辑: 当对象实例化的时候,为其生成代理对象并返回,而不是原本的对象,从而达到代理对象自动生成的目的。
伪代码如下:

  for (bean in Ioc容器){
            if(bean符合拦截条件){
                Object proxy = createProxyFor(bean);
                return proxy; //返回代理的对象
            }else{
                Object instance = createInstance(bean);
                return instance;
            }
}

createProxyFor()方法创建代理对象,就直接通过 ProxyFactoryBean的getBean()都可以。
对于拦截条件,则可以是标注了某些注解。
DefaultAdvisorAutoProxyCreator这个类就是实现了完全自动的自动注入。
在这里插入图片描述
可以发现,它确实实现了 BeanPostProcessor 这个接口。
它会自动搜寻容器内的所有 Advisor,然后根据各个 Advisor所提供的拦截信息,为符合条件的容器中的目标对象生成相应的代理对象。
我们这里去分析一下它的源码

DefaultAdvisorAutoProxyCreator源码分析

在这里插入图片描述
可以看到在该类中,postProcessBeforeInitialization()只是返回了当前对象,没有做任何操作,所以织入的具体过程应该在 postProcessAfterInitialization()方法里面:

@Override
	public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) throws BeansException {
		if (bean != null) {
			Object cacheKey = getCacheKey(bean.getClass(), beanName);
			if (!this.earlyProxyReferences.contains(cacheKey)) {
				return wrapIfNecessary(bean, beanName, cacheKey);
			}
		}
		return bean;
	}

第一步,判断bean非空
第二步:执行getCacheKey(),该方法是返回一个键,就是存到一些Map或者Set内的的String类型的键
第三步,判断earlyProxyReferences中是否存在过,存在过说明该对象已经被代理过了。

	private final Set<Object> earlyProxyReferences = Collections.newSetFromMap(new ConcurrentHashMap<>(16));

第四步,调用wrapIfNecessary()方法

	protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
		// Create proxy if we have 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;
	}

在第一步中调用的 getAdvicesAndAdvisorsForBean()方法,他返回对应于当前class和beanName的所有切面Advisor,
第二步是将当前键存入缓存,防止多次代理
第三步是进行了代理的过程,看一下该方法的源码:

	protected Object createProxy(Class<?> beanClass, @Nullable String beanName,
			@Nullable Object[] specificInterceptors, TargetSource targetSource) {
			//第一步
		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);
		
		proxyFactory.setFrozen(this.freezeProxy);
		if (advisorsPreFiltered()) {
			proxyFactory.setPreFiltered(true);
		}
		//最后一步
		return proxyFactory.getProxy(getProxyClassLoader());
	}

第一步是创建proxyFactory类,该类就是创建代理对象Proxy的工厂类,然后第二步为该工厂类配置属性,在最后一步获取最终的代理对象。
最后一步的方法:

	public Object getProxy(@Nullable ClassLoader classLoader) {
		return createAopProxy().getProxy(classLoader);
	}

调用createAopProxy方法去获取一个AopProxy的实现类,一种是JDK的是实现类,一种是CGLIB的实现类,使用getProxy()方法来具体获得代理对象。

总结:

在AOP概念中的词基本在Spring AOP都能找到对应的接口,Pointcut --> Pointcut接口 , Advice --> Advice接口,Aspect --> Advisor接口,织入 --> ProxyFactory
真正实现AOP,还是靠的 ProxyBeanFactory 这个类集成了 切入点和切面等信息,DefaultAdvisorAutoProxyCreator实现了 BeanPostProcessor 这个接口,在postProcessAfterInitialization()方法里面实现对象的代理过程,里面有个wrapIfNecessary()方法,里面获取适用于当前对象的所有Advisor对象,

能获取所有Advisor对象又是因为继承了BeanFactoryAware接口,调用了setBeanFactory()方法,所以在该类中有整个容器对象,从中获取所有对象,然后对其中的Advisor对象进行缓存过

然后使用createProxy()方法获取代理对象,在该方法内部先使用ProxyFactory这个工厂类,配置相关的代理信息,然后从该工厂类获取具体的AopProxy的实现类,使用该实现类进行具体的代理,并为其生成代理对象,而不是原本的对象,这样就实现了代理,自动地实现了AOP

  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring AOPSpring框架中的一个重要模块,它提供了一种面向切面编程的方式,可以让开发者将一些通用的、横切的关注点(如事务、安全、缓存等)从业务逻辑中剥离出来,使得业务逻辑更加清晰简洁,代码复用更加方便。 Spring AOP实现原理主要基于Java动态代理和CGLIB动态代理两种方式,其中Java动态代理主要用于接口代理,而CGLIB动态代理则主要用于类代理。Spring AOP中的核心概念是切面(Aspect)、连接点(Join Point)、通知(Advice)、切点(Pointcut)和织入(Weaving)。 在Spring AOP中,切面是一个横向的关注点,它跨越多个对象和方法,通常包含一些通用的功能,如日志记录、安全控制等。连接点则是程序中可以被切面拦截的特定点,如方法调用、异常抛出等。通知是切面在连接点执行前后所执行的动作,包括前置通知(Before)、后置通知(After)、异常通知(AfterThrowing)、返回通知(AfterReturning)和环绕通知(Around)。切点则是用来匹配连接点的规则,它可以指定哪些连接点会被切面拦截。织入则是将切面应用到目标对象中的过程,它可以在编译时、类加载时、运行时等不同的阶段进行。 Spring AOP的源码解析涉及到很多细节,包括代理的生成、通知的执行、切点的匹配等,需要深入了解Spring框架的内部实现和Java的反射机制。对于初学者而言,可以先从Spring AOP的基本概念和用法入手,了解其实现原理的同时,也可以通过调试和查看源码来加深理解。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值