(22)基础加强-代理

基础加强-代理

基础加强-代理

代理的概念和作用:

生活中的代理:

例如:客户从代理商手中买电脑或其他商品,和直接跑到厂家或总部去买电脑,这里面你觉得最终的主题业务目标有什么区别吗?基本都一样,解决了核心问题。但是从代理商那里买真的一点好处没有吗?

程序中的代理:

要为已存在的多个具有相同接口的目标类的各个方法增加一些系统功能,例如:异常处理,日志,计算方法的运行时间,事务管理等等,我们该怎么做?

解决: 编写一个与目标类具有相同接口的代理类,代理类的每个方法调用目标类的相同方法,并在调用方法时加上系统功能的代码

简单代码实例:

目标类:

Calss X {
  Void sayHell(){
  System.out.println(“hello,heima”);
  }
}
代理类:(增加计算运行时间的功能)
XProxy{
  Void sayHell(){
  Starttime;
  X.sayHell();
  Endtime;
  }
}

代理架构图如下:


根据上图我们可以了解到:

1目标类和代理类通常实现同一个或多个接口,一般用该接口来引用其子类(代理类),如: Collection  coll = new ArrayList();

,2如果采用工厂模式和配置文件的方式进行管理,则不需要修改客户端程序,在配置文件中配置是使用目标类还是代理类。这样以后很容易切换,如果想要日志功能时,就配置代理类,否则配置目标类,这样,增加系统功能很容易,以后运行一段时间后,又想换掉系统功能也很容易。

AOP

概述:

AOPAspectOriented Program)即面向方面的编程

诞生背景:

 AOP诞生的背景 -----交叉业务的出现

例如:在系统中存在交叉业务,一个交叉业务就是要切入到系统中的一个方面,

                                安全      事务        日志

StudentService  ------|----------|------------|-------------

CourseService  ------|----------|------------|-------------

MiscService       ------|----------|------------|-------------

 安全、事务、日志等功能要贯穿于好多个模块中,所以他们就是交叉业务。

用具体的程序代码描述交叉业务:

交叉业务的代码实现

method1         method2          method3

{               {                {

 ------------------------------------------------------切面

 ....               ....              ......

------------------------------------------------------切面

}               }                 }

使用代理技术正好可以解决这种问题,代理是实现AOP功能的核心和关键技术,只要是用到面向方面的编程,就涉及到代理。

动态代理技术

动态代理技术的由来:

要为系统中的各种接口的类增加代理功能,那需要太多的代理类,全部采用静态代理方式的话,那就非常麻烦!如需要写成千上万个代理类那就太累了!

所以就有了动态代理,它是通过特定的设置,在程序运行期间指示JVM动态的生成类的字节码。这种动态生成的类往往被用作代理类,即动态代理类

注意:

JVM生成的动态类必须实现一个或多个接口,所以,JVM生成的动态类只能用作具有相同接口的目标类的代理。

CGLIB库:

CGLIB库可以动态生成一个类的子类,一个类的子类也可以用作该类的代理。所以,如果要为一个没有实现接口的类生成动态代理类,那么可以使用CGLIB库。

代理类各个方法通常除了调用目标相应方法和对外返回目标返回的结果外,还可以在代理方法中的如下位置上加上系统功能代码:

1在调用目标方法之前

2在调用目标方法之后

3在调用目标方法前后

4在处理目标方法异常的catch块中。

分析JVM动态生成的的类

创建实现Collection接口的动态类和查看其他方法,分析Proxy.getProxyClass方法的各个参数。

编码列出动态类中的所有构造方法和参数签名;

编码列出动态类中的所有方法和参数签名;

创建动态类的实例对象:

1,用反射获得构造方法;

2,编写一个最简单的InvocationHandler类;

3,调用构造方法创建动态类的实例对象,并将编写的InvocationHandler类的实例对象传进去;

4,打印创建的对象和调用对象的没有返回值的方法和getClass方法,演示调用其他有返回值的方法报告了异常。

5,创建动态类的实例对象的代理改成匿名内部类的形式编写。


首先了解下Proxyjava.lang.reflect.Proxy

Proxy类的作用:

Proxy 提供用于创建动态代理类和实例的静态方法,它还是由这些方法创建的所有动态代理类的超类。

因为Proxy中的方法全是静态方法,他是为创建动态代理类提供了相应的静态方法,也是通过自身的静态方法创建动态代理类的父类;

构造方法:

Protected Proxy(InvocationHandler h) 
         使用其调用处理程序的指定值从子类(通常为动态代理类)构建新的 Proxy 实例。

方法摘要:

  static InvocationHandler

getInvocationHandler(Object proxy) 
          返回指定代理实例的调用处理程序。

static Class<?>

getProxyClass(ClassLoader loader, Class<?>... interfaces) 
          返回代理类的 java.lang.Class 对象,并向其提供类加载器和接口数组。

static boolean

isProxyClass(Class<?> cl) 
          当且仅当指定的类通过 getProxyClass 方法或 newProxyInstance 方法动态生成为代理类时,返回 true。

static Object

newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) 
          返回一个指定接口的代理类实例,该接口可以将方法调用指派到指定的调用处理程序。

 

Proxy的方法生成的动态代理类都是Proxy的子类

让JVM创建动态类及其实例对象,需要提供的信息:

1生成类中的哪些方法,通过让其实现哪些接口的方式进行告知。

2产生的类字节码必须有一个关联的类加载器对象

3生成的类中的方法的代码是怎么样的,也得由我们自己提供。把我们的代码写在一个约定好的子接口对象的方法中,把对象传给它,它调用我们的方法,即相当于插入了我们自己的代码。提供执行代码的对象就是InvocationHandler对象,它是在创建动态类的实例对象的构造方法时传递进去的,在上面的InvocationHandler对象的invoke方法中,加一点代码就可以看到这些代码被调用运行了。

示例代码:

package cn.itheima.Day03;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Collection;

public class ProxyTest {

	/**
	 * @param args
	 */
	public static void main(String[] args) throws Exception{
		// TODO Auto-generated method stub
		//通过Proxy类获取字节码对象
		Class clazzProxy1 = 
			Proxy.getProxyClass(Collection.class.getClassLoader(), Collection.class);
		System.out.println(clazzProxy1.getName());//$Proxy0
		
		 System.out.println("----------begin constructors list----------");    
		 /*获取这个代理类的构造方法,以下列形式输出  
	      * $Proxy0()  
	      * $Proxy0(InvocationHandler,int)  
	      * */
		//获取$Proxy0该类所有的构造方法
		Constructor[] constructors = clazzProxy1.getConstructors();
		//迭代便签编码列出动态类中的所有构造方法和参数签名;
		for(Constructor constructor : constructors){
			String name = constructor.getName();
			//定义临时容器
			StringBuilder sBuilder = new StringBuilder(name);
			sBuilder.append('(');
			//获取构造方法中的参数,因为有多个参数所以返回的是数组
			Class[] clazzParams = constructor.getParameterTypes();
			//取出每一个参数
			for(Class clazzParam : clazzParams){
				//获取参数名并添加进缓冲区(若是多个参数列表那么为了显示清楚可以在后面加“,”)
				sBuilder.append(clazzParam.getName()).append(',');
			}
			//因为打印出来最后是逗号所以要将逗号去除,但是在这之前还有判断该数组是否null,是否0即:
			if(clazzParams!= null && clazzParams.length!=0)
				sBuilder.deleteCharAt(sBuilder.length()-1);
			sBuilder.append(')');
			System.out.println(sBuilder.toString());
		}
		
		System.out.println("----------begin method list----------");    
	     /*获取这个代理类具备的方法,以下列形式输出  
	         * toString()  
	         * hashCode()  
	         * clear()  
	         * addAll(java.util.Collection)  
	         * */ 
		Method[] methods = clazzProxy1.getMethods();//获取代理类里的所有方法
		for(Method method : methods){
			String name = method.getName();//获取方法名
			StringBuilder sBuilder = new StringBuilder(name);
			sBuilder.append('(');
			//通过getParameterTypes()获取方法中的所有参数 返回类型是Class
			Class[] clazzParams = method.getParameterTypes();
			for(Class clazzParam : clazzParams){//遍历取出每一个参数
				sBuilder.append(clazzParam.getName()).append(',');//添加进缓冲区
			}
			//如果没有构造函数或者构造函数没有参数,则删除最后一个","字符
			if(sBuilder!=null && sBuilder.length()!=0)
				sBuilder.deleteCharAt(sBuilder.length()-1);
			sBuilder.append(')');
			System.out.println(sBuilder.toString());
		}
		System.out.println("----------begin Create Instance Object list----------"); 
		Constructor constructor = clazzProxy1.getConstructor(InvocationHandler.class);

创建动态类的实例对象及调用其方法

因为根据原理知道newInstance创建实例对象必须是空参的构造方法,所以我们还是需要通过反射获取有参构造方法

//方法一:通过创建InvocationHandler的子类,传递给构造函数的创建实例对象方法参数,来创建实例  即:Constructor clazzProxy.getConstructor(InvocationHandler); 
通过查询API知道InvocationHandler是一个接口,所以为了参数实例化 那么我们还需要构造该接口的实现类,在newInstance(参数)参数就是newMyInvocationHandler();
		class MyInvocationHandler1 implements InvocationHandler{    
            @Override    
            public Object invoke(Object proxy, Method method, Object[] args)    
                    throws Throwable {    
                    
                return null;    
            }    
        }	
		 Collection proxy1 =(Collection)constructor.newInstance(new MyInvocationHandler1());    
	        System.out.println(proxy1.toString());    
	        proxy1.clear();//无异常    
	        //proxy1.size();//报错,空指针异常  因为size方法有返回值
	        
	    //方法二:利用匿名内部类来创建InvocationHandler子类对象,这样一步到位 相当于方法一的精简版
	        Collection proxy2 =(Collection)constructor.newInstance(new InvocationHandler(){

			@Override
			public Object invoke(Object proxy, Method method, Object[] args)
					throws Throwable {
				return null;
			}	    	 
	     });

通过前面两种方法,思考能不能让JVM创建动态类和其实例对象合二为一呢? 即两个步骤合起来,需要给他提供哪些信息呢?  

三个方面:

1,生成的类中有哪些方法,通过让其实现哪些接口的方式进行告知;

2,产生的类字节码必须有个一个关联的类加载器对象;

3,生成的类中的方法的代码是怎样的,也得由我们提供。把我们的代码写在一个约定好了接口对象的方法中,把对象传给它,它调用我的方法,即相当于插入了我的代码。提供执行代码的对象就是那个InvocationHandler对象,它是在创建动态类的实例对象的构造方法时传递进去的。在上面的InvocationHandler对象的invoke方法中加一点代码,就可以看到这些代码被调用运行了。

 

 //方法三:在Proxy接口中还提供了静态方法newProxyInstance()方法,综合了前两种方法,直接一步到位,接收三个参数
	     //里面接收三个参数:接口的类加载器,接口数组字节,handler接口子类
	        Collection proxy3 = (Collection)Proxy.newProxyInstance(
	        		Collection.class.getClassLoader(),//第一个参数定义代理类的类加载器
	        		new Class[]{Collection.class}, //第二个参数代理类要实现的接口列表
	        		new InvocationHandler(){//第三个参数代理类的构造函数的参数
	        			//复写invoke方法 
	        			public Object invoke(Object proxy, Method method, Object[] args)
	        					throws Throwable {
	        				ArrayList target = new ArrayList();
	        				long beginTime = System.currentTimeMillis();
	        				Object retVal = method.invoke(target, args);
	        				long endTime = System.currentTimeMillis();
	        				//测试方法运行时间
	        				System.out.println(method.getName()+"运行时间:"+(endTime-beginTime));
	        				return retVal;
	        			} 
	        		});
	        //通过代理类调用目标方法,每调用一个目标的方法就会执行代理类的方法      
	        //当调用一次add方法时,就会找一次InvocationHandler这个参数对象的invoke方法
	        //每次调用add就调用一个全新的目标,各个目标是没有关联的。
	        //若吧target集合对象放在invoke方法外打印长度就是3了。
	        proxy3.add("wangwu");
	        proxy3.add("lisi");
	        proxy3.add("wanba");
	        
	        System.out.println(proxy3.size());//0		     
	}
}

猜想分析动态生成的类的内部代码

分析InvocationHandler的对象的运行原理

1,动态生成的类实现了Collection接口(可以实现若干接口),生成的类有Collection接口中的所有方法和一个如下接受InvocationHandler参数的构造方法。

2,思考构造方法接受一个InvocationHandler对象,接受对象要干什么用呢?该方法内部代码回事怎么实现呢?

3,实现的Collection接口中的各个方法的代码又是怎样的呢?

通过生成的Collection接口中的方法的运行原理

Int size(){

Return handler.invoke(this,this.getClass().getMethod(“size”,null));

}

可以分析出,我们之前通过构造方法接受了handler,内部调用handler的方法,而里面唯一的方法就是invoke方法;这时再看:代理对象调用add方法,因之前已经接收了handler对象,add方法内部实际调用handler对象的invoke方法。

4,InvocatinHandler接口中定义的invoke方法接受的三个参数又是什么意思?

Client程序调用objProxy.add(“abc”)方法时,涉及三个要素:

1objProxy对象

2add方法

3,“abc”参数

其实在调用代理对象实现接口里面的方法的时候,在内部是把这三个要素传递给handler实例的invoke方法里了,如:

Class Proxy$ {

add(Object object) {

return handler.invoke(Object proxy, Method method, Object[] args);

}

}

参数解读就是:传递了当前哪个代理对象,该对象的哪个代理方法,该方法的哪个参数;

返回:当method.invocke(目标,参数s)返回Object,再通过invoke返回

这时我们可以修改某些参数,如目标方法的参数可以修改,返回的数据可以修改。

疑问解答:

为什么Proxy.getClass().getName();返回的不是目标类呢?即通过上面了解的原理,代理类对调用getClass方法,会传递给invoke方法,然后又找到目标,那么返回的应该是目标类啊?

这时候我们可以查阅文档,得知因为getClass方法是从Object类上继承过来的,对于从Object继承过来的方法只对(hashCodeequalstoString)这三个方法进行派发,委托给handler,其他方法有自己的实现不交给handler

总结分析动态代理类的设计原理与结构;让动态生成的类称为目标类的代理

动态代理工作原理图:


解读:

客户端调用代理--》代理的构造方法接受一个handler--》然客户端调用代理的各个方法--》各个方法会把调用请求转发给刚才通过构造方法传进去的handler对象--》该对象又把各个请求分发给目标 的相应方法

分析:

在图中log()被小圈圈着,这是一段代码是硬编码,当我们写好后别人是无法进行修改的,因为这段代码不是作为参数传进去的,是直接写进去的。这时,若我们做框架,希望是作为参数传进去的(即将系统功能的代码作为参数传进去)该怎么办呢?

为什么要作为参数传递呢?

因为这样就不会把程序写死,在程序运行时我可以临时设置,而不是提前编码硬邦邦的写好。

假想,我们若把这些代码写成很长的字符串,作为参数直接扔进去,然后说你执行这个字符串所代表的代码,可不可以呢?不可以的,但是这个在JavaSpecial里面有(称动态语言)

 

最后java里面怎么样的临时来传递代码呢?

可以这样做,首先不是传递代码进去而是传递对象给你(即传给InvocationHandler),在内部拿到我这个对象,调用对象实际调用的就是对象的方法,而之前我们可以把要执行的代码放在对象的方法里面,在图中log()被小圈圈着相当于我们把系统功能,日志功能等封装成了一个对象,这就是面向切面编程AOP,就是把切面的代码用对象方式进行封装,然后以对象的形式传递给你,你执行对象就可以了

 

思考:这时我们要给InvocationHandler传递什么东西进去呢?

要传递目标类和系统功能的对象,即我们要给InvocationHandler对象传递两个对象进去。这样才能把它做成框架。

实例代码:

package cn.it.heima01;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Collection;

public class ProxyTest2 {

	/**
	 * 构造小框架把目标和系统功能抽取
	 * @param args
	 */
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		final ArrayList target = new ArrayList();//指定目标
		 //使用Proxy提供的静态newProxyInstance方法来一步到位的创建代理类实例对象  
		Collection collection = (Collection)getProxy(target, new MyAdvice());
		collection.add("heima");
		collection.add("heima");
		collection.add("heima");
		collection.add("heima");
		System.out.println(collection.size());
	}
	//作为一个通用的方法,就使用Object      
    //传入一个目标,并传入一个接口,此接口作为通信的契约,才能调用额外的方法 
	private static Object getProxy(final Object target, final Advice advice) {
		Object proxy = Proxy.newProxyInstance(
				 //定义代理类的类加载器
				target.getClass().getClassLoader(),
				//new Class[]{Collection.class}, 
				//定义代理类要实现的接口列表,这里要与目标实现相同的接口
				target.getClass().getInterfaces(),
				//代理类的构造函数参数
				new InvocationHandler() {
				//复写invoke方法 
				public Object invoke(Object proxy, Method method, Object[] args )
							throws Throwable {
					/*
						long beginTime = System.currentTimeMillis();
						Object retVal = method.invoke(target, args);//调用目标
						long endTime = System.currentTimeMillis();
						System.out.println(method.getName()+"运行时间:"+(endTime-beginTime));
						return retVal;*/
						//将系统功能抽取定义一个对象,该对象是实现Advice接口
						//为什么一定要实现接口而不自己定义类写方法呢?
						//因为实现Advice中的方法,里面的代码功能可以随便定义,调用代理时就会被使用,这就叫做通行的契约
						advice.getStartTime();
						Object retVal = method.invoke(target, args);//调用目标
						 //使用约定的对象中的方法
						advice.getEndTime(method);
						
						return retVal;
					}
				});
		return proxy;
	}

}
package cn.it.heima01;

import java.lang.reflect.Method;
//只要实现Advice中的方法,里面的代码功能可以随便定义,调用代理时就会被使用 
public class MyAdvice implements Advice {

	long beginTime=0;

	@Override
	public void getStartTime(){
		// TODO Auto-generated method stub
		beginTime = System.currentTimeMillis();
		System.out.println("高新技术学习");
		for(int x=1;x<=1000;x++){
			x+=0;
			System.out.println(x);
		}
	}
	@Override
	public void getEndTime(Method method) {
		// TODO Auto-generated method stub
		System.out.println("然后努力进黑马拉");
		long endTime = System.currentTimeMillis();
		System.out.println(method.getName()+"运行时间:"+(endTime-beginTime));
	}
}
package cn.it.heima01;

import java.lang.reflect.Method;

//定义系统功能接口
/*接口中需要实现四个方法   
 * 调用目标方法之前   
 * 调用目标方法之后   
 * 调用目标方法前后   
 * 在处理目标方法异常的catch块中   
 */ 
public interface Advice {
	void getStartTime();
	void getEndTime(Method method);
}

实现AOP功能的封装与配置(实现类似spring的可配置的AOP框架)

实现步骤原理分析:

1工厂类BeanFactory负责创建目标类或代理类的实例对象并通过配置文件实现切换。

getBean方法根据参数字符串返回一个相应的实例对象,如果参数字符串在配置文件中对应的类名不是ProxyFactoryBean,则直接返回该类的实例对象,否则,返回该类实例对象的getProxy方法返回的对象。

2BeanFactory的构造方法接收代表配置文件的输入流对象,配置文件格式如下:

#xxx=java.util.ArrayList

xxx=cn.itcast.ProxyFactoryBean

xxx.target=java.util.ArrayList

xxx.advice=cn.itcast.MyAdvice

3ProxyFacotryBean充当封装生成动态代理的工厂,需要为工厂类提供目标与通知

配置参数信息

4编写客户端应用:

编写实现Advice接口的类和在配置文件中进行配置

调用BeanFactory获取对象

代码实例:

配置文件里的信息

//xxx=java.util.ArrayList

xxx=cn.heima.AOP.ProxyFactoryBean

xxx.target=java.util.ArrayList

xxx.advice=cn.it.heima01.MyAdvice

package cn.heima.AOP;
import java.io.InputStream;
//测试类
public class AopFramework {
	public static void main(String[] args) {  
	 //用输入流关联配置文件 
	InputStream ips = 
		AopFramework.class.getResourceAsStream("config.properties"); 
	
	Object bean = new BeanFactory(ips).getBean("xxx");
	System.out.println(bean.getClass().getName());	
	}
}
package cn.heima.AOP;

import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

import cn.it.heima01.Advice;

//创建Bean工厂类
//负责创建目标类或代理类的实例对象并通过配置文件实现切换。
public class BeanFactory {
	//因为要接收配置文件信息,所以需要定义Properties集合
	Properties pro = new Properties();
	//在构造该类对象时就将配置文件信息存储进集合
	public BeanFactory(InputStream ips){
		try {
			pro.load(ips);
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	//从配置文件中获取对象
	//定义普通方法getBean(),根据参数字符串(即键)返回一个相应的实例对象,
	
	public Object getBean(String name){
		//获取配置文件中的信息
		String className = pro.getProperty(name);
		Object bean =null;
		//通过反射获取对应类对象		
		try {
			Class clazz = Class.forName(className);
			bean = clazz.newInstance();
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} 
		//如果创建的对象是ProxyFactoryBean类型,则通过getProxy方法获取代理类对象   
		if(bean instanceof ProxyFactoryBean){
			
			Object proxy=null; 
			 ProxyFactoryBean proxyFactoryBean=(ProxyFactoryBean)bean; 
			try {
				//从配置文件中获取代理类额外添加的代码封装成的对象 
				Advice advice = (Advice)Class.forName(pro.getProperty(name+".advice")).newInstance();
				Object target = Class.forName(pro.getProperty(name+".target")).newInstance();
				//有了对象后就可以把它们设置进类中
				proxyFactoryBean.setAdvice(advice);
				proxyFactoryBean.setTarget(target);
				//调用getProxy方法,获取代理对象    
                proxy= proxyFactoryBean.getProxy(target, advice);  
			} catch (Exception e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			return proxy;
		}
		return bean;
	}	
}

package cn.heima.AOP;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

import cn.it.heima01.Advice;
//创建ProxyFactoryBean类,用于产生代理类实例对象
public class ProxyFactoryBean {
	//定义两个变量即advice与target
	private Advice advice;
	private Object target;
	public Advice getAdvice() {
		return advice;
	}

	public void setAdvice(Advice advice) {
		this.advice = advice;
	}

	public Object getTarget() {
		return target;
	}

	public void setTarget(Object target) {
		this.target = target;
	}	
	
//定义方法接受advice 和target,返回代理类对象
	static Object getProxy(final Object target, final Advice advice) {
		Object proxy = Proxy.newProxyInstance(
				 //定义代理类的类加载器
				target.getClass().getClassLoader(),
				//new Class[]{Collection.class}, 
				//定义代理类要实现的接口列表,这里要与目标实现相同的接口
				target.getClass().getInterfaces(),
				//代理类的构造函数参数
				new InvocationHandler() {
				//复写invoke方法 
				public Object invoke(Object proxy, Method method, Object[] args )
							throws Throwable {
						//将系统功能抽取定义一个对象,该对象是实现Advice接口
						//为什么一定要实现接口而不自己定义类写方法呢?
						//因为实现Advice中的方法,里面的代码功能可以随便定义,调用代理时就会被使用,这就叫做通行的契约
						advice.getStartTime();
						Object retVal = method.invoke(target, args);//调用目标
						 //使用约定的对象中的方法
						advice.getEndTime(method);					
						return retVal;
					}
				});
		return proxy;
	}	
}
---------------------- ASP.Net+Android+IOS开发.Net培训、期待与您交流! ----------------------详细请查看: http://edu.csdn.net
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值