Spring AOP之低级别支持

3 篇文章 0 订阅

在Spring 2.0以后,Spring AOP已经封装的比较完善,用@AspectJ注解或XML的方式就可以完成AOP功能,不了解AOP原理的话,是不大看的出AOP是基于动态代理的。本文的主要内容是Spring AOP的前世(Spring 1.2),以便能更好的理解动态代理转换到Spring AOP的过程。

有三篇文章,循序渐进的顺序为:1.代理模式学习笔记,2.本文(Spring AOP低级别支持),3.Spring相关文档翻译-chapter9(AOP部分)(Spring AOP高级别支持)

动态代理可以参考文章1。动态代理是由JDK自己为基类生成代理,在本章中Spring将此功能封装进了ProxyFactoryBean中。

一点基础知识

1.aop技术基于动态代理模式。动态代理模式实现由两种方式:基于JDK和基于CGLIB。我们知道JDK动态代理生成,需要被代理类实现一个或多个接口;如果要为一个没有实现接口的被代理类生成动态代理,就需要使用CGLIB。这也是Spring AOP自动代理所使用的规则。

2.低级别的Spring AOP支持,生成动态代理的主要类是ProxyFactoryBean。该类调用getObject()生成动态代理,生成方式取决于被代理类是否实现了接口。

3.ProxyFactoryBean定义时,有几个主要属性:

1)proxyInterfaces:指定被代理类实现的接口。不设置此属性,创建CGLIB代理。
2)target:指定被代理类
3)InterceptorNames:指定方法调用时,需要切入的消息或拦截器队列。此队列有先后顺序。
4)proxyTargetClass:默认为false。创建动态代理的方式
5)exposeProxy:默认为false。如果true,将代理暴露给本地线程,target可以通过AopContext.currentProxy()方法获取获取自己的代理。
6)singleton:默认为true。创建的消息为单例。如果要创建消息带有状态,需要设置为false。
关于上面几个属性的设置说明:
1)关于这几个参数,可以类比JDK生成动态代理的语句:Proxy.newProxyInstance(ClassLoarder loader,Class<?>[] interfaces,InvocationHandler h)。loader参数是我们web程序的类加载器,相当于上面的target(ClassLoarder loader = target.getClass().getClassLoader(););interfaces参数相当于上面的proxyInterfaces;h与InterceptorNames指定的类功能一致:在方法调用前后添加一些功能。
2)如果需要被代理的target没有实现任何接口,那么spring会为其创建一个基于CGLIB的动态代理。这是spring 2.0以后新增的功能。因为指定了一个实现类的实例target,那么根据此实例,可以获取其实现了的接口列表:target.getClass().getInterfaces()。那么指定proxyInterfaces属性就显得有些多余,spring也是如此做的:在proxyInterfaces属性中指明target所实现的全部接口,与省略proxyInterfaces属性,效果相同,都会生成一个覆盖了全部接口的代理。所以,如果target实现了4个接口,而你的proxyInterfaces属性只指明了少于4个的接口,spring会认为是开发者故意为之,只会生成实现了指定接口的代理。
3)如果target有实现的接口,那么生成proxy的类型,就由ProxyFactoryBean的配置决定:只要proxyTargetClass设置为true,不论proxyInterfaces属性是否指定,生成CGLIB代理。proxyTargetClass属性默认为false。
4)如果没有特殊要求,proxyInterfaces属性、proxyTargetClass属性都可以省略。交给spring自己决定如何创建代理。

ProxyFactoryBean使用

ProxyFactoryBean中需要定义InterceptorNames属性,也就是指向Spring AOP中的advisor。在这里一个advisor是一个类(与Spring AOP高级支持不同:一个切面一个类,一个切面可自定义多个通知方法体)。advisor的类型一样有5中:Around、before、throw、after returning、Introductions。下面直接贴代码来直观表达。

before advice

首先,先建一个接口:
package com.business.born;

public interface IHello {
	void sayHello(String name);
	void danceHey(String type);
}
然后,该接口的实现类:
package com.business.born;

public class HelloLiu implements IHello {

	@Override
	public void sayHello(String name) {
		System.out.println("你好,"+name);
	}
	@Override
	public void danceHey(String type) {
		System.out.println("邀请你跳舞,"+type);
	}
}
其次,before advice需要实现一个接口MethodBeforeAdvice,并实现before方法,方法参数可以访问被代理类连接点的信息。如下:
package com.business.aop;

import java.lang.reflect.Method;

import org.springframework.aop.MethodBeforeAdvice;

public class DoBeforAdvice implements MethodBeforeAdvice {

	@Override
	public void before(Method method, Object[] args, Object target)
			throws Throwable {
		System.out.println("------在方法前做事 start");
		System.out.println("调用方法:"+method.getName());
		System.out.print("方法参数:");
		for(Object arg:args){
			System.out.print(arg);
		}
		System.out.println();
		System.out.println("方法所在类:"+target.getClass().getName());
		System.out.println("######在方法前做事 end");
	}

}
接着,在Spring.xml中配置如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:tx="http://www.springframework.org/schema/tx"
	xmlns:aop="http://www.springframework.org/schema/aop"
	xsi:schemaLocation="http://www.springframework.org/schema/beans
		http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/tx
		http://www.springframework.org/schema/tx/spring-tx.xsd
		http://www.springframework.org/schema/aop
		http://www.springframework.org/schema/aop/spring-aop.xsd">
	
	<bean id="iHello" class="com.business.born.HelloLiu"/>
	<bean id="doBeforeAdvice" class="com.business.aop.DoBeforAdvice"/>
	
	<bean id="helloProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
	<!-- HelloLiu实现类实现了接口,spring可以自动检测,用JDK创建动态代理。所以proxyInterfaces属性也不用设置 -->
<!-- 		<property name="proxyInterfaces"> -->
<!-- 			<value>com.business.born.IHello</value> -->
<!-- 		</property> -->
		<property name="target">
			<ref bean="iHello"/>
		</property>
		<property name="interceptorNames">
			<list>
				<value>doBeforeAdvice</value>
			</list>
		</property>
	</bean>
</beans>
最后,junit测试:
package com.business.junit;

import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import com.business.born.IHello;

public class AopTest {
	
	@Test
	public void helloBeforeAdivceTest(){
		ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
		IHello iHello = (IHello) ac.getBean("helloProxy");
		iHello.sayHello("2015元旦");
		iHello.danceHey("华尔兹");
	}
}
输出结果如下,不解释:
***方法调用前,输出调用方法的信息:
调用类:com.business.born.HelloLiu
调用方法:sayHello
方法参数:
2015元旦 
准备调用方法...
你好,2015元旦
***方法调用后,输出返回值和方法执行时间:
返回值:null
执行耗时:0ms
***方法调用前,输出调用方法的信息:
调用类:com.business.born.HelloLiu
调用方法:danceHey
方法参数:
华尔兹 
准备调用方法...
邀请你跳舞,华尔兹
***方法调用后,输出返回值和方法执行时间:
返回值:null
执行耗时:0ms

上面的例子是before advice。配置一个advice需要注意的点有:

1)自定义不同的advice,需要实现不同的接口。(至于是哪些接口,在下面的例子中)。

2)advice在spring配置文件中,需要以bean的形式定义,就想一个普通类的bean定义一样。

3)定义一个代理bean,改bean的名称(如helloProxy)就是我们在客户端中引用的代理名称,class属性为ProxyFactoryBean。该类中的属性设置,可以关联被代理类和advice,以及一些参数设置。

4)注意,此时虽然我们有一个名称为iHello的bean,但实际上我们在测试类中并不是调用此bean,而是调用spring为我们生成的代理bean(helloProxy),以此方式来控制我们队iHello bean的访问。

5)before advice这几步的定义,同样适用于around、after-returning、throw advice。这几个类的下面例子只贴了advice类代码,其他的测试配置步骤一样,你可以看出实现不同advice需要实现的不同接口。

around advice

package com.business.aop;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

public class DoAroundAdvice implements MethodInterceptor {

	@Override
	public Object invoke(MethodInvocation paramMethodInvocation)
			throws Throwable {
		Object result = null;		
		System.out.println("***方法调用前,输出调用方法的信息:");
		Object[] args = paramMethodInvocation.getArguments();
		
		System.out.println("调用类:"+paramMethodInvocation.getThis().getClass().getName());
		System.out.println("调用方法:"+paramMethodInvocation.getMethod().getName());
		System.out.println("方法参数:");
		for(Object arg:args){
			System.out.print(arg+" ");
		}
		System.out.println("\n准备调用方法...");
		Long beforTimes = System.currentTimeMillis();
		result = paramMethodInvocation.proceed();
		System.out.println("***方法调用后,输出返回值和方法执行时间:");
		System.out.println("返回值:"+result);
		Long time = System.currentTimeMillis() - beforTimes;
		System.out.println("执行耗时:"+time+"ms");
		return result;
	}
	
}

After returning advice

package com.business.aop;

import java.lang.reflect.Method;

import org.springframework.aop.AfterReturningAdvice;

public class DoAfterAdvice implements AfterReturningAdvice {

	@Override
	public void afterReturning(Object returnValue, Method method,
			Object[] args, Object target) throws Throwable {
		System.out.println("------after returning ..start");
		System.out.println("调用类:"+target.getClass().getName());
		System.out.println("调用方法:"+method.getName());
		System.out.print("方法传入参数:");
		for(Object arg:args){
			System.out.print(arg+" ");
		}
		System.out.println("\n返回值:"+returnValue);
		System.out.println("######after returning ..end");
	}

}


throwing advice

package com.business.aop;

import java.lang.reflect.Method;
import java.rmi.RemoteException;

import javax.servlet.ServletException;

import org.springframework.aop.ThrowsAdvice;

public class DoThrowAdvice implements ThrowsAdvice {

	/**
	 * ThrowAdvice接口只是一个tag接口,接口中并为包含任何方法。
	 * afterThrowing()方法必须自己添加,只要方法签名参数不同,就可以添加任意多的同名方法。
	 * @param re
	 */
	public void afterThrowing(RemoteException re){
		System.out.println("Java RMI 调用异常!");
	}
	
	public void afterThrowing(ServletException se){
		System.out.println("servlet调用,发生异常!");
	}
	
	/**
	 * 前三个参数可以省略,取决于开发者是否需要获取异常抛出点的信息。
	 * @param m
	 * @param args
	 * @param target
	 * @param e
	 */
	public void afterThrowing(Method m,Object[] args,Object target,Exception e){
		System.out.println("DoThrowAdvice调用:");
		System.out.println("调用类:"+target.getClass().getName());
		System.out.println("调用方法:"+m.getName());
		System.out.print("方法参数:");
		for(Object arg:args){
			System.out.print(arg + " ");
		}
		System.out.println("\n抛出异常:"+e.getMessage());
	}
}

Introduction advice

例子一:资源访问初始化

Introduction advice与上面的几种advice有些不同。它的功能指定某些类,让这些类实现一个共同的接口,也就是引入一个新类,已扩充原类的功能。
定义一个Introduction advice有如下5要素(下面的代码功能:为一个资源访问类,增加一个初始化资源功能的接口):
1)原功能接口抽象
package com.business.init;

import java.util.List;
/**
 * 原功能接口
 */
public interface IResourceAccess {
	List getUserInfo();
	boolean isUserRegist(String id);
}
2)原功能接口实现类
package com.business.init;

import java.util.List;
/**
 * 原接口功能实现类
 */
public class ResourceAccessImpl implements IResourceAccess {

	@Override
	public List getUserInfo() {
		System.out.println("访问用户资源..");
		return null;
	}
	@Override
	public boolean isUserRegist(String id) {
		System.out.println("通过用户id,判断此用户信息是否注册..");
		return false;
	}
}
3)要引入的功能接口抽象
package com.business.init;
/**
 * 扩展功能接口
 */
public interface IinitResource {
	void initResouce();
	boolean inited();
}
4)要引入的功能接口实现。此类处理实现引入接口,还需继承IntroductionInterceptor或DelegatingIntroductionInterceptor父类,父类中有invoke方法,负责调用“原功能接口”中的方法。DelegatingIntroductionInterceptor的设计是用来代表“扩展功能接口”的一个实现类的引用,并借此来隐藏IntroductionInterceptor。DelegatingIntroductionInterceptor实现了接口IntroductionInterceptor接口,IntroductionInterceptor接口继承了MethodInterceptor接口,此接口就是around-advice需要实现的接口,其中的invoke()方法,将“原功能接口”的方法暴露出来,我们可以在方法前后添加一些操作。这里的DelegatingIntroductionInterceptor.invoke()方法也是此功能。实现了DelegatingIntroductionInterceptor父类,也表示此类可以作为引入类使用。
package com.business.init;

import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.support.DelegatingIntroductionInterceptor;
/**
 * 扩展功能接口的实现类
 * 此类继承DelegatingIntroductionInterceptor类,可以通过invoke方法将原功能接口的方法暴露出来
 * 在invoke的前后,你可以像around-advice一样,做一些操作。
 * 如果你并不需要获取原方法的调用信息,invoke方法无需重写
 */
public class InitResourceImpl extends DelegatingIntroductionInterceptor implements IinitResource {
<span style="white-space:pre">	</span>private static final long serialVersionUID = 3603308890579750856L;
<span style="white-space:pre">	</span>
<span style="white-space:pre">	</span>private boolean inited = false;
<span style="white-space:pre">	</span>@Override
<span style="white-space:pre">	</span>public void initResouce() {
<span style="white-space:pre">		</span>System.out.println("初始化资源..");
<span style="white-space:pre">		</span>this.inited = true;
<span style="white-space:pre">	</span>}

<span style="white-space:pre">	</span>@Override
<span style="white-space:pre">	</span>public boolean inited() {
<span style="white-space:pre">		</span>return inited;
<span style="white-space:pre">	</span>}
<span style="white-space:pre">	</span>
<span style="white-space:pre">	</span>@Override
<span style="white-space:pre">	</span>public Object invoke(MethodInvocation mi) throws Throwable{
<span style="white-space:pre">		</span>//you can do something here ..
<span style="white-space:pre">		</span>//if you don't need do something here,Override invoke(MethodInvocation mi) method is not necessary.
<span style="white-space:pre">		</span>return super.invoke(mi);
<span style="white-space:pre">	</span>}
}
5)定义一个Introduction advice,此通知将绑定引入接口。
package com.business.init;

import org.springframework.aop.support.DefaultIntroductionAdvisor;
/**
 * 创建Introduction-advice
 */
public class InitResourceAdvisor extends DefaultIntroductionAdvisor {
	/**
	 * 构造方法:为扩展功能接口创建AOP可引用的Advisor
	 */
	public InitResourceAdvisor(){
		super(new InitResourceImpl(),IinitResource.class);
	}
}
接下来是此Introduction advice在spring XML中的定义配置:
<span style="white-space:pre">	</span><!-- 初始化资源,Introduction-advice测试定义 -->
	<!-- 1.定义原接口功能实现类bean -->
	<bean id="resourceAccess" class="com.business.init.ResourceAccessImpl"/> 
	<!-- 2.定义Introduction-advice bean,bean中包含“扩展功能接口”实现类的引用-->
	<bean id="initAdvisor" class="com.business.init.InitResourceAdvisor"/> 
	<!-- 3.为ProxyFactoryBean生成的动态代理定义一个引用 -->
 	<bean id="resourceAccessProxy" class="org.springframework.aop.framework.ProxyFactoryBean"> 
		<property name="proxyInterfaces"> 
			<value>com.business.init.IResourceAccess</value> 
 		</property> 
		<property name="target" ref="resourceAccess"/> 
		<property name="interceptorNames"> 
 			<list> 
				<value>initAdvisor</value> 
 			</list> 
 		</property> 
 	</bean> 
接下来使用此Introduction advice来测试:
<span style="white-space:pre">	</span>@Test
	public void introductionAdvisorInitResourceTest(){
		ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
		//代理类原功能的引用
		IResourceAccess ira = (IResourceAccess)ac.getBean("resourceAccessProxy");
		ira.isUserRegist("132213");
		ira.getUserInfo();
		System.out.println("----");
		//代理类扩展功能的引用
		IinitResource iir = (IinitResource)ira;
		//调用扩展功能,如果数据没有初始化,先初始化资源,再访问
		if(!iir.inited()){
			iir.initResouce();
		}
		//初始化完成,继续业务逻辑的访问
		ira.isUserRegist("132213");
		ira.getUserInfo();
	}
测试结果如下:
通过用户id,判断此用户信息是否注册..
访问用户资源..
----
初始化资源..
通过用户id,判断此用户信息是否注册..
访问用户资源..
这里通过resourceAccessProxy bean获得的一个对象,此对象同时实现了原接口和扩展接口,要调用不同接口中的方法时,将此对象向上转型为对应的接口在调用即可。

例子二:Spring的例子为方法加锁

上面的代码是自己测试的一个简单Introuduction advice例子,而spring相关文档中也举了一个例子,此例子的功能是将原实现类中的set方法枷锁,如果set方法锁定时,调用set方法抛出MethodLockException。下面直接贴代码。
1)扩展接口
package com.business.lock;

public interface Lockable {
	
	void lock();
	void unlock();
	boolean locked();
}
2)扩展接口实现
package com.business.lock;

import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.support.DelegatingIntroductionInterceptor;

public class LockMixin extends DelegatingIntroductionInterceptor implements Lockable {

	private boolean locked;
	@Override
	public void lock() {
		this.locked = true;
	}
	@Override
	public void unlock() {
		this.locked = false;
	}
	@Override
	public boolean locked() {
		return this.locked;
	}	
	@Override
	public Object invoke(MethodInvocation mi) throws Throwable{
		if(locked() && mi.getMethod().getName().indexOf("say")>-1){<span style="white-space:pre">	</span>//我这里修改后,不是锁定set方法,而是锁定包含say字符串的方法
			throw new MethodLockException();
		}
		return super.invoke(mi);
	}
}
3)Introduction advice
package com.business.lock;

import org.springframework.aop.support.DefaultIntroductionAdvisor;

public class LockMixinAdvisor extends DefaultIntroductionAdvisor {
	
	private static final long serialVersionUID = -2046296275775575276L;

	public LockMixinAdvisor() {
		super(new LockMixin(),Lockable.class);
	}
}
4)自定义异常
package com.business.lock;

public class MethodLockException extends Exception {

	public MethodLockException(){
		super("调用方法被锁定。");
	}
	public MethodLockException(String msg){
		super(msg);
	}
}
5)测试代码
<span style="white-space:pre">	</span>@Test
	public void introductionAdvisorLockableTest(){
		ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext.xml");
		/*
		 * ac.getBean("lockProxy")获取到的代理实例包含两个接口的功能:IHello和Lockable
		 * 但是调用每个接口中的方法时,需向上转型到指定接口,再调用各自接口中的方法。
		 * 扩展接口实现类继承的DelegatingIntroductionInterceptor中可以暴露原接口中的方法,
		 * 如果扩展功能涉及到原功能(原方法),可以通过重新invoke方法来实现。
		 * 原接口与扩展接口的关联(暴露invoke()方法),是由ProxyFactoryBean来实现的。
		 */
		IHello helloProxy = (IHello)ac.getBean("lockProxy");
		Lockable lockProxy = (Lockable)helloProxy;
		helloProxy.sayHello("阿毛");
		helloProxy.danceHey("交谊舞");
		
		lockProxy.lock();
		helloProxy.sayHello("阿毛阿毛");
	}

关于Introduction advice的思考

使用Introduction advice把代码搞的很复杂,这到底有什么好处?
1)上面资源访问初始化的例子,我们假设没有Introduction advice这玩意,我们要如何实现?首先,定义一个接口,也就是其中包含的方法也就是初始化资源(与“扩展功能接口”一样),然后,所有访问资源的类,除了实现原业务逻辑接口,在实现“扩展功能接口”。问题在于,所有实现类都需要修改:实现新接口,重新新方法,并且这些新方法有可能一模一样(都是完成初始化);在调用资源之前,加初始化语句。此方法缺点非常明显,要动的类太多。如果哪天初始化方法不要了,或者要按需求修改,那需要在吐血一次;另一种实现方法:在业务逻辑接口中,新增初始化资源的方法,所有实现类,也要实现此方法,初始化资源。其效果与上面的分析是一样的。无论哪种改法,都违反开闭原则。
2)用introduction-advice的好处:首先,不用修改“原功能接口”和“原功能接口实现”;其次,新增功能就用新增代码来实现,并且“扩展功能接口”与“原功能接口”都是松耦合的,是通过配置联系在一起的,若哪一天资源又不需要初始化了,直接将applicationContext.xml中的自动代理配置删除即可,灵活方便。当然,即便用introduction-advice也是需要修改代码的:客户端对原接口的引用,要改成对aop代理的引用;添加初始化语句代码。
3)引申问题:如果调用资源的业务逻辑实现类有几十个,难道要为每一个实现类创建一个动态代理吗?这也是工作量啊 spring亲。于是乎,spring引入auto-proxy来批量生成代理。

使用auto-proxy功能

用于自动为bean生成代理的类有:BeanNameAutoProxyCreator和DefaultAdvisorAutoProxyCreator,前者根据bean名称来自动生成代理,多个bean名以逗号分隔;后者根据spring上下文中定义的advisor,来自动为bean生成代理。
如BeanNameAutoProxyCreator的XML配置:
<span style="white-space:pre">	</span><!-- 1.定义原接口功能实现类bean -->
	<bean id="resourceAccess" class="com.business.init.ResourceAccessImpl"/>
	<!-- 2.定义Introduction-advice bean,bean中包含“扩展功能接口”实现类的引用-->
	<bean id="initAdvisor" class="com.business.init.InitResourceAdvisor"/>
	<!-- 通过bean名称,为多个bean自动生成代理 -->
	<bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
		<property name="beanNames">
			<value>resourceAccess,iHello</value>
		</property>
		<property name="interceptorNames">
			<list>
				<value>initAdvisor</value>
			</list>
		</property>
	</bean>
无论BeanNameAutoProxyCreator,还是DefaultAdvisorAutoProxyCreator,在定义bean时,无需指定id属性。当在客户端中要访问代理类时,直接获取原类的bean id即可,因为此时的bean,已经是代理类(这样是有好处的,可以隐藏被代理类,不被访问)。比如,上面我们要获得ResourceAccessImpl类的代理,我们只需要在测试类中用下面语句:
//代理类原功能的引用,与上面测试的不同在于bean名称的改变,看似调用的是本来的实现,其实这是此类的代理引用
IResourceAccess ira = (IResourceAccess)ac.getBean("resourceAccess");

如果是新的新的项目,目前都不太推荐用低级别的Spring AOP实现切面功能。而是推荐使用Spring 2.0以后的AOP支持,也就是@AspectJ注解和XML标签支持( spring相关文档翻译-AOP部分)。因为这些支持提供了更多的支持,以及强大的切入点表达式,可以让我们很方便的定义要代理的对象。
当然,要注意一点:XML标签支持AOP时,因为<aop:config>标签也是极强的依赖于自动代理。所以,如果你在应用容器中已经指定了一个自动代理工厂(如:BeanNameAutoProxyCreator、DefaultAdvisorAutoProxyCreator、AbstractAdvisorAutoProxyCreator),此时再使用<aop:config>就可能会出现问题。因此,我们推荐<aop:config>和AutoProxyCreator,只能选其一使用。









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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值