java设计模式:动态代理Dynamic_Proxy详解 (使用场景:事务、日志、监控等)

前言

1、导读

本文,通过三个案例模仿InvocationHandler拦截器和Proxy代理类的内部代码,助你更好的由浅入深的理解动态代理!

下面是被模仿两个类的源代码片段: 

package java.lang.reflect;
//系统自带的拦截器接口
public interface InvocationHandler {
 public Object invoke(Object proxy, Method method, Object[] args){
        throws Throwable;
 }
 public static Object invokeDefault(Object proxy, Method method, Object... args){
 ……
 }
}
package java.lang.reflect;
public class Proxy implements java.io.Serializable {
	public static InvocationHandler getInvocationHandler(Object proxy)
        throws IllegalArgumentException {   
		……
	}
	
	public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces,InvocationHandler h) {
		……
	}
}

Proxy类参数ClassLoader的作用 :它可以通过任意当前类获取。

示例如下所示: 

ClassLoader loader = Test.class.getClassLoader();//注:Test可以是当前类的类名
Class c4 = loader.loadClass("com.succ.demo.Person");

更多关于反射的参考,点击进入反射的意义、用法、优缺点等 。

Proxy类参数interfaces的作用 :它是业务类所implements的某个统一方法/业务接口;

比如,public class Tank  implements Moveable {} ,那么此处的interfaces就可以是Moveable

Proxy类参数InvocationHandler 的作用 :它就是具体的装饰类,也就是implements InvocationHandler的那个具体的实体类,主要用来包裹业务类的某个方法,起到修饰(过滤、拦截、校验等)作用。

比如:public class TimeInvocationHandler implements InvocationHandler {},那么此处的InvocationHandler 就可以是TimeInvocationHandler。

温馨提示在实际开发中,这两个类可以直接用,而不必像下方案例中的写法,自己拼装代码。

2、InvocationHandler和Proxy的用法

InvocationHandler用法:

自定义的各种拦截、过滤器直接 implements InvocationHandler 接口,并在实现类中完成自己的拦截、过滤等;

意义:它存在的意义,就是定义一个通用的接口类,可以在指定类某个方法的外层添加自己的拦截、过虑、校验代码!

具体用法:参考案例二中的TimeInvocationHandler 即可!

Proxy用法:

首先要了解,Proxy它是个包装类,被包装的类叫做本体(负责执行方法),用法如下方案例二所示:

public class TankClient {
	public static void main(String[] args) throws Exception {
		Moveable tank=new Tank();
		//动态生成一个实现了主类同一接口的代理类,并动态包装方法前后的代码
		Moveable timeProx=(Moveable)Proxy.newProxyInstance(Moveable.class,new TimeInvocationHandler(tank));
		System.out.println(timeProx);
		timeProx.move();
		timeProx.stop();
		
		//注:如果运行完上面代码,控制台打印:ClassNotFoundException。需要刷新一下工程里的代码把新自动生成的文件load到项目中。
	}
}

3、动态代理的使用场景

对方法执行前后(添加代码),进行监控(比如:方法运行[多少毫秒、记录日志、事务提交或回滚、权限监控、数据拦截、数据过滤]等)。

好处:让方法本体,与在该方法前后需要拦截、过虑、校验的方法隔离开,实现解耦。

4、是否使用动态代理对比

不采用代理:对一个类的多个方法进行监控时,重复的代码总是重复出现,不但破坏了原方法,如果要实现多个监控,将会对代码造成大量冗余。

同时,还导致业务代码,与非业务的监控代码掺杂在一起,不利于扩展和维护。

CarTimeProxy  DogTimeProxy   CatTimeProxy  CowTimeProxy 代理类在无限制膨胀,就需要无限的修改业务代码。

采用代理后:原方法不需要做任何改动,操作的是原方法的代理对象,而原方法不用做任何修改,就实现了被监控。

5、动态代理原理

原理:面向切面编程(动态生成代理对象)。

1、这些监控代码,是一些共性代码,可以抽离出来,而不必和业务代码混杂一起。

2、对指定方法,可以同时进行日志跟踪、权限判断、事务处理、过滤等操作。

带着以上知识点,进入正题:


¥¥¥¥¥¥¥¥¥通过下方案例,可让你更好的随心所欲、游刃有余的在合适的场景,使用动态代理类,InvocationHandlerProxy这两个类!¥¥¥¥¥¥¥¥¥


案例一硬性编码,最简单,最好理解,最冗余

实现原理:业务代码和拦截、过滤代码混在一起。

温馨提示

案例一看完后,可以先看案例三,回头再看案例二,因为一、三比较好理解,二稍来回拼接字符串,比较烧脑。

//1、定义统一的运动接口
public interface Moveable {
	void move() ;
	void stop() ;
}

//2、坦克类实现运动接口
public class Tank implements Moveable {
	
	public Tank() {
		super();
	}

	@Override
	public void move() {
		
		try {
			System.out.println("tank is moving");
			Thread.sleep(5000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
	@Override
	public void stop() {
		
		try {
			System.out.println("tank is stopping");
			Thread.sleep(3000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

//3、实现坦克的时间监控代理(注:main方法测试运行的时候,运行该代理,代理中传入 聚合类Tank就可以了)
public class TankTimeProxy implements Moveable {
	Moveable tank ;//采用聚合,这里不用extends继承tank
	
	public TankTimeProxy(Moveable tank) {
		super();
		this.tank = tank;
	}

	@Override
	public void move() {
		long start=System.currentTimeMillis();//方法运行的起始时间
		System.out.println("run start time:"+start);
		 
		tank.move();//调用父类的move方法
		
		long end=System.currentTimeMillis();//方法运行的截止时间
		System.out.println("run end time:"+end);
		System.out.println("运行了:"+(end-start)+"毫秒");
	}
	
	@Override
	public void stop() {
		long start=System.currentTimeMillis();//方法运行的起始时间
		System.out.println("run start time:"+start);
		
		tank.stop();//调用父类的stop方法
		
		long end=System.currentTimeMillis();//方法运行的截止时间
		System.out.println("run end time:"+end);
		System.out.println("运行了:"+(end-start)+"毫秒");
	}
}

//4、同理,实现对Tank方法的日志记录
public class TankLogProxy implements Moveable {
	Moveable tank ;//采用聚合,这里不用extends继承tank
	
	public TankLogProxy(Moveable tank) {
		super();
		this.tank = tank;
	}

	@Override
	public void move() {
		System.out.println("坦克启动,进入战场!");
		System.out.println();
		
		tank.move();//调用父类的move方法
		
		System.out.println();
		System.out.println("坦克撤离,离开战场~");
	}
	
	@Override
	public void stop() {
		System.out.println("坦克停下,进入补给状态!");
		System.out.println();
		
		tank.stop();//调用父类的stop方法
		
		System.out.println();
		System.out.println("补给完毕,结束休整~");
	}
}

//5、对以上代理,进行测试
public class TankClient {

	public static void LogAndTime(Tank tank) {
		//日志代理包在最外面,内层是时间代理(外层的代理,最后执行)
		Moveable timeProxy=new TankTimeProxy(tank);//时间代理包住“宿主坦克”
		Moveable logProxy=new TankLogProxy(timeProxy);//日志代理再包住“时间代理”
		logProxy.move();
		printLine();
		logProxy.stop();
	}
	
	public static void TimeAndLog(Tank tank) {
		//时间代理包在最外面,内层是日志代理(外层的代理,最后执行)
		Moveable logProxy=new TankLogProxy(tank);//日志代理包住“宿主坦克”
		Moveable timeProxy=new TankTimeProxy(logProxy);//时间代理包住“日志代理”
		timeProxy.move();
		printLine();
		timeProxy.stop();
	}
	public static void printLine() {
		System.out.println("-------------------------------------------------------");
		System.out.println("-------------------------------------------------------");
	}
	public static void main(String[] args) {
		Tank tank=new Tank();
		LogAndTime(tank);//日志记录在最外层
		//TimeAndLog(tank);//时间记录在最外层
	}

}

下面是测试结果

坦克启动,进入战场!

run start time:1616782198706
tank is moving
run end time:1616782203708
运行了:5002毫秒

坦克撤离,离开战场~
-------------------------------------------------------
-------------------------------------------------------
坦克停下,进入补给状态!

run start time:1616782203708
tank is stopping
run end time:1616782206708
运行了:3000毫秒

补给完毕,结束休整~

小节

优点:

1、上面的代码逻辑清晰,可读性很强。

2、如果被监控的地方比较少,扩展性尚可(如果需要被监控的类比较多的话,重复代码就会比较多)。

缺点:

1、有N个类需要被监控我们就要写N*(每个类有几种监控)个代理,比如:Tank 有TankLogProxy/TankTimeProxy/TankTransactionProxy等等,代码量比较大,一个Tank就会冒出很多个代理。

反思:

1、我们能否实现动态代理,动态生成代理类?

2、一个TimeProxy可以监控所有的满足该监控条件的类,比如:Tank、Cat、Dog、Cow等等。

3、监控代码,最好能分离出来,不和业务代码混淆在一起。

实现思路:

1、新建一个代理总类Proxy(JDK自带的有这个类,在本案例中我们模拟这个类)

2、在总代理类中要有一个newProxyInstance的方法,来通过入参动态生成我们需要的代理类(用该代理来运行我们需要运行的方法,比如:tank的move())。

public class Proxy {
    public static Object newProxyInstance(){
     Object newObjectProxy=null;
     return newObjectProxy;
    }
}

3、如上所示,我们是不是就可以通过该Proxy类,愉快的,动态生成我们需要的代理类了。(*—* 先自嗨一下)

注:由于逻辑比较简单(以上内容),但是实现动态代理的小细节比较多(如下内容),但是一旦写好Proxy以后的以后,就一劳永逸了。

首先我们要解决掉,几座小山头(如果基础比较扎实,可以略过下面的文字描述,直接看代码)。

1、把以上代理类(比如:TankTimeProxy )中的所有方法,都复制到Proxy的 newProxyInstance()方法内部,并对其进行字符串拼接。(工程量比较大,需仔细)。

2、拼接完字符串,发现:既然我要想实现动态代理,里面的部分代码是不能写死的。比如:对“方法”“”字符串的拼接 (参考上面 TankTimeProxy 类的move 方法、stop方法),在代理类中,我们可以用一个for循环,动态拼接方法。

3、我们在工程中新建的Java类,编译器会自动帮我们把编译后的class文件,放到bin目录下。对于我们自己动态生成的java类,需要我们手动编译。(特别提示:Class.forName("com.baidu.TankProxy").newInstance();,这种写法只能加载在内存中的类,比如:bin目录内的class)。我们手动动态创建的java文件,为了区分起见,避免不必要的冲突,需要单独放。此时我们就需要自己编译为class文件了。

4、Proxy类的newProxyInstance方法,为了更好的兼容,需要2个很重要的参数传入:Class interFace,InvocationHandler handler。前者 是实体类+代理类共同实现的接口类,后者是处理器总接口,具体要处理的是 log 还是transaction。

5、Proxy类的newProxyInstance方法中,可能会需要用到:创建目录、写数据到java文件中、URL方式加载文件、编译java文件为class

案例二、在理解案例一的基础上,进行字符串拼接和改装

具体实现:代码量不大,如果不容易看懂,请仔细阅读上面的提示。在理解的大原理基础上,就比较容易消化下面的代码。

注:

1、里面的路径可以随意指定Window系统下面的任意位置,在本案例中,统一为项目中的路径。

2、动态代理类这和jdk的保持一致$Proxy1

准备工作

先创建 监控处理器总接口InvocationHandler(JDK有这个类,此处为模拟写法),及其实现类之一TimeInvocationHandler。

import java.lang.reflect.Method;

//监控处理器,总接口(其他各种监控,都要implements 该接口)
public interface InvocationHandler {
	/**
	 * @param objectProxy 代理对象(该参数主要是在动态代理类Proxy中调用,Invoke()入参写的是this,就是动态生成的代理类本身$Proxy1),可以理解为:TankTimeProxy TankLogProxy
	 * @param method
	 * 特别注意:其实现类,具体实现该方法是,方法内部invoke()第一个参数调用的应该是声明的聚合对象 Object target,比如:User、Cat、Tank
	 */
	void invoke(Object objectProxy,Method method);
	void invoke(Object objectProxy,Method method,Object[] methodParameter );
}


//监控处理器,实现器之一,时间处理器
public class TimeInvocationHandler implements InvocationHandler {
	
	public Object target;//被代理对象,比如:Tank

	
	public TimeInvocationHandler() {
		super();
	}
	public TimeInvocationHandler(Object target) {
		super();
		this.target = target;
	}

	@Override
	public void invoke(Object objectProxy, Method method) {
		this.invoke(target,method,null);
	}
	@Override
	public void invoke(Object objectProxy, Method method, Object[] methodParameter) {
		
		long start=System.currentTimeMillis();//方法运行的起始时间
		System.out.println(method.getName()+" run start time:"+start);
		try {
			method.invoke(target, methodParameter);
		} catch (Exception e) {
			e.printStackTrace();
		}
		
		long end=System.currentTimeMillis();//方法运行的截止时间
		
		System.out.println(method.getName()+" run end time:"+end);
		System.out.println(method.getName()+"运行了:"+(end-start)+"毫秒");
	}

}

Proxy类具体实现

String字符串拼接、文件写入、class编译等。特别注意:invoke()中的参数,传递是this,指的是当前动态生成的代理类。

public class Proxy {
	
	final static String rt="\r\t";
	
	//下面是复制的TankTimeProxy源码,仅仅用于对比查看,不用于案例实际操作。
	public static Object newProxyInstance()  throws Exception{
		
		String src=
		"package com.xp.proxy;"+rt+rt+

		"public class TankTimeProxy implements Moveable {"+rt+
		"	Moveable tank ;//采用聚合,这里不用extends继承tank"+rt+
		
		"	public TankTimeProxy(Moveable tank) {"+rt+
		"		super();"+rt+
		"		this.tank = tank;"+rt+
		"	}"+rt+

		"	@Override"+rt+
		"	public void move() {"+rt+
		"		long start=System.currentTimeMillis();//方法运行的起始时间"+rt+
		"		System.out.println(\"tank run start time:\"+start);"+rt+
		"		 "+rt+
		"		tank.move();//调用父类的move方法"+rt+
		"		"+rt+
		"		long end=System.currentTimeMillis();//方法运行的截止时间"+rt+
		"		System.out.println(\"tank run end time:\"+end);"+rt+
		"		System.out.println(\"运行了:\"+(end-start)+\"毫秒\");"+rt+
		"	}"+rt+
		"	"+rt+
		"	@Override"+rt+
		"	public void stop() {"+rt+
		"		long start=System.currentTimeMillis();//方法运行的起始时间"+rt+
		"		System.out.println(\"tank stop time:\"+start);"+rt+
		"		"+rt+
		"		tank.stop();//调用父类的stop方法"+rt+
		"		"+rt+
		"		long end=System.currentTimeMillis();//方法运行的截止时间"+rt+
		"		System.out.println(\"tank go again time:\"+end);"+rt+
		"		System.out.println(\"停了:\"+(end-start)+\"毫秒\");"+rt+
		"	}"+rt+
		"	"+rt+
		"}";
				
		System.out.println(src);			
		return null;
	}

    //该方法为真正的拼接方法,实际操作用这个方法。考虑到代码的可读性,里面拆分了几个纯粹的小方法。
	public static Object newProxyInstance(Class interFace,InvocationHandler handler) throws Exception  {
		Method[] methods=interFace.getMethods();
		String interName=interFace.getName();
		
        //拼接字符串javaFileContent,并把拼接后的字符,写入到动态生成的$Proxy1.java文件中
		String javaFileContent=
		"package com.xp.proxy;"+rt+rt+
		"import java.lang.reflect.Method;"+rt+
		"public class $Proxy1 implements "+interName+" {"+rt+
		"   com.xp.proxy.InvocationHandler handler ; "+rt+	
		"  "+rt+	
		"	public $Proxy1(com.xp.proxy.InvocationHandler handler) {"+rt+
		"		super();"+rt+
		"		this.handler = handler;"+rt+
		"	}"+rt+
		//getStringMethods(methods).toString()+
		getStringMethodsByInterFace(interFace,methods).toString()+
		"}";
		
		Object newPro=doBirthJavaFileAndLoadFileClass(interFace,handler,javaFileContent);
		return newPro;
	}
	
	public static void makeDir(String dir) {
		 File file = new File(dir);
		 if(!file.exists()) {
			 file.mkdirs();
			 System.out.println("创建路径完毕");
		 }
	}
	
	public static Object doBirthJavaFileAndLoadFileClass(Class interFace,InvocationHandler handler,String javaFileContent) throws Exception {
		String projectPath=System.getProperty("user.dir");//   G:\Workspaces\Eclipse4.7 Jee\ProxyMode
		//makeDir(projectPath);
		String fileName=projectPath+"\\src\\com\\xp\\proxy\\$Proxy1.java";//双斜杠,其中一个是转义字符
		System.out.println("fileName=  "+fileName);
		
		File file=new File(fileName);
		FileWriter writer=new FileWriter(file);
		writer.write(javaFileContent);
		writer.flush();
		writer.close();
		
		//step 1:编译
		JavaCompiler compiler=ToolProvider.getSystemJavaCompiler();//获取java自带的编译器,把Java文件编译为class
		//System.out.println(compiler.getClass().getName());
		StandardJavaFileManager fileManager= compiler.getStandardFileManager(null,null,null);
		Iterable  compilationUnits=fileManager.getJavaFileObjects(fileName);//获取编译单元对象
		CompilationTask tasks=compiler.getTask(null, fileManager, null, null, null, compilationUnits);//获取编译任务
		//注,此处需要点击Eclipse任务栏的Window-->show view,切换到Navigator模式才能看到编译后的class文件
		tasks.call();//编译为class
		fileManager.close();
		
		//step 2:编译完毕,需要把class文件load到内存,然后从内存拿到对象。
		//注:用classLoad的时候,必须load的是bin目录下的class(例如:X.class.forName("com.xp.TankTimeProxy").newInstance()这种写法),当前动态编译的class不在bin目录下
		String localFilePath="file:\\"+projectPath+"\\src\\com\\xp\\proxy";//自动生成的java文件所在根目录
		//String localFilePath="file:/"+projectPath;//自动生成的java文件所在根目录
		System.out.println(localFilePath);
		URL[]urls=new URL[] {new URL(localFilePath)};
		URLClassLoader uLoader=new URLClassLoader(urls);
		Class cl=uLoader.loadClass("com.xp.proxy.$Proxy1");//被动态生成的代理类
		//System.out.println(c+" 666");
		
		//注:cl.newInstance(),使用的前提是,对应的cl类里有一个空的构造方法。
		Constructor c=cl.getConstructor(InvocationHandler.class);//自定义一个构造方法给cl,同时定义入参
		Object newPro=c.newInstance(handler);//把被代理对象传入,生成新的对象
		//newPro.move();
		return newPro;
	}

	//该方法的存在,仅仅是为了下面的方法getStringMethodsByInterFace做铺垫,实际中不予运用
	public static StringBuffer getStringMethods(Method[] methods) {
		StringBuffer srtBf=new StringBuffer();
		
		for (Method md : methods) {
			String strMethod=
					"	@Override"+rt+
					"	public void "+md.getName()+"() {"+rt+
					"		long start=System.currentTimeMillis();"+rt+  //动态生成方法前后的处理,需要引入Invocation
					"		System.out.println(\"run start time:\"+start);"+rt+
					"		 "+rt+
					"		obj."+md.getName()+"();"+rt+
					"		"+rt+
					"		long end=System.currentTimeMillis();"+rt+
					"		System.out.println(\"run end time:\"+end);"+rt+
					"		System.out.println(\"运行了:\"+(end-start)+\"毫秒\");"+rt+
					"	}"+rt+
					"	"+rt;
			srtBf.append(strMethod);
		}
		
		return srtBf;
	}
	
	public static StringBuffer getStringMethodsByInterFace(Class interFace,Method[] methods) throws Exception{
		StringBuffer srtBf=new StringBuffer();
		
		for (Method md : methods) {
			String strMethod=
					"	@Override"+rt+
					"	public void "+md.getName()+"() {"+rt+
					"      try{"+rt+
					"            Method m="+interFace.getName()+".class.getMethod(\""+md.getName()+"\");"+rt+
					"		    handler.invoke(this,m);"+rt+ //这里直接传入md会报错,需要上一行代码特殊处理一下,处理为m
					"         }catch(Exception e){"+rt+
					"		    e.printStackTrace();"+rt+
					"         }		"+rt+
					"	}"+rt+
					"	"+rt;
			srtBf.append(strMethod);
		}
		
		return srtBf;
	}
	 
}

对动态代理类进行测试

public class TankClient {
	public static void main(String[] args) throws Exception {
		Moveable tank=new Tank();
		//动态生成一个实现了主类同一接口的代理类,并动态包装方法前后的代码
		Moveable timeProx=(Moveable)Proxy.newProxyInstance(Moveable.class,new TimeInvocationHandler(tank));
		System.out.println(timeProx);
		timeProx.move();
		timeProx.stop();
		
		//注:如果运行完上面代码,控制台打印:ClassNotFoundException。需要刷新一下工程里的代码把新自动生成的文件load到项目中。
	}
}
tank run start time:1616785209732
tank is moving
tank run end time:1616785214732
move运行了:5000毫秒
tank run start time:1616785214732
tank is stopping
tank run end time:1616785217733
stop运行了:3001毫秒

案例三、在实现动态代理的基础上,对UserMsg类的add操作进行一些监控

//1、 创建接口
public interface UserMgr {
	void add();
}

//2、创建接口的实现类
public class UserMgrImpl implements UserMgr {

	@Override
	public void add() {
		System.out.println("添加用户成功");
	}

}

//3、创建操作处理器,具体添加监控逻辑代码

import java.lang.reflect.Method;

import com.xp.proxy.InvocationHandler;

public class TransactionInvocationHandler implements InvocationHandler {
	public Object target;
	
	
	public TransactionInvocationHandler() {
		super();
	}

	public TransactionInvocationHandler(Object target) {
		super();
		this.target = target;
	}

	@Override
	public void invoke(Object objectProxy, Method method) {
		invoke(target,method,null);
	}

	@Override
	public void invoke(Object objectProxy, Method method, Object[] methodParameter) {
		System.out.println("transaction is begining");
		try {
			System.out.println("方法正在运行");
			method.invoke(target);
		} catch (Exception e) {
			e.printStackTrace();
		} 
		System.out.println("transaction is submited");
	}

}
//4、创建测试类
public class UserClient {

	public static void main(String[] args) throws Exception {
		UserMgr mgr=new UserMgrImpl();
		InvocationHandler  h=new TransactionInvocationHandler(mgr);
		
		UserMgr userProx=(UserMgr)Proxy.newProxyInstance(UserMgr.class,h );
		userProx.add();
		
	}
}

延伸案例:运行结果

transaction is begining
方法正在运行
添加用户成功
transaction is submited

总结:

绕了这么大圈,费了这么大劲,拼了半天字符串,调试了半天。

无非是想动态生成个代理类,并且不对原方法产生伤害,如你所愿,已展示完毕。

温馨提示:Proxy代理类,JDK6以上版本均已帮我们实现,本案例主要是阐述这种思想。实际开发中,用的会比较多。

尾言

最后:上传上案例中的目录结构,便于初学者理解吸收,欢迎大家交流,指导批评。

后记

一个方法外层嵌套多个监控器:

如果想实现动态代理,拼接字符串较为繁琐,本案例没有展示(如果展示的话,不难,但可读性不是很好)。

可喜的是,在案例一给你自己实现多层动态代理,提供了一种思路,感兴趣的话,可以在案例二基础上尝试一下。

附注

猜你可能感兴趣

1、JAV反射:反射的意义价值和用法 | 通过反射获取私有属性和方法 |反射的作用 | 反射的优缺点 | 反射破坏了封装性为什么还要用 

2、JAVA多线程:yield/join/wait/notify/notifyAll等方法的作用

3、更多的设计模式,比如:工厂模式、责任链模式、观察者模式,点击进入

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值