反射和类加载

当调用Java命令运行某个程序时,该命令将会启动一个Java虚拟机进程,不管该Java程序有多么复杂,该程序启动了多少个线程,它们都处于Java虚拟机进程里。即同一个JVM的所有线程、所有变量都处于同一个进程里,它们都使用该JVM进程的内存区,当系统出现以下几种情况时,JVM进程将会被终止:
1、程序运行到最后正常结束
2、程序运行到使用System.exit()或Runtime.getRuntime().exit()代码结束程序
3、程序进行过程中遇到未捕获的异常或错误而结束
4、程序所在平台强制结束了JVM进程
程序运行结束时,JVM进程结束,该进程在内存中的状态将会丢失。每次重新运行JVM时,都会重新初始化类——前一次运行JVM结束后,它对类做的修改将全部丢失,两次运行java程序处于两个不同的JVM进程中,两个JVM之间并不会共享数据。

类加载器的机制:
当程序主动使用某个类时,如果该类未被加载到内存中,则系统会通过加载、连接、初始化三个步骤对类进行初始化。三个步骤也称为类加载或类初始化。
类加载指的是将类的class文件读入内存中,并为之创建一个java.lang.Class对象。
类的加载由类加载器完成,类加载器通常由JVM提供,JVM提供的也称为系统类加载器,开发者也可以通过继承ClassLoader基类来创建自己的类加载器。
使用不同的类加载器,可以从不同来源加载类的二进制数据,如下几种来源:
1、从本地文件系统加载class文件
2、从JAR包加载class文件
3、通过网络加载class文件
4、把一个Java源文件动态编译、并执行加载。

类加载器通常无需等到“首次使用”该类时才加载该类,Java虚拟机规范允许系统预先加载某些类。

类的连接:
类被加载之后,系统会为之生成一个对应的Class对象,接着将会进入连接阶段,连接阶段负责把类的二进制数据合并到JRE中,连接分为三阶段:
1、验证:验证阶段用于检验被加载的类是否有正确的内部结构,并和其他类协调一致。
2、准备:类准备阶段则负责为类的类变量分配内存,并设置默认初始值
3、解析:将类的二进制数据的符号引用替换成直接引用。
类的初始化:
初始化阶段,虚拟机负责对类进行初始化,主要就是对类变量进行初始化,初始化步骤:
1、假如这个类还没有被加载黑连接,则程序先加载并连接该类
2、假如该类的直接父类没有被初始化,则先初始化直接父类
3、假如该类中有初始化语句,则系统一次执行这些初始化语句。
HVM最先初始化的总是java.lang.Object类,当程序主动使用任何一个类时,系统会保证该类以及所有父类都会被初始化。

类初始化的时机:
Java程序首次通过以下6中方式使用类或接口时,系统会初始化该类或接口:
1、创建类的实例。包括new 创建、反射创建和反序列化的方式创建。
2、调用某个类的类方法(静态方法)
3、访问某个类或接口的类变量,或为该类变量赋值
4、使用反射方式来强制创建某个类或接口对应的java.lang.Class对象。
5、初始化某个类的子类。
6、直接使用java.exe命令来运行某个主类。
对于一个final修饰的类变量,如果该类变量的值在编译时就可以确定下来,name这个类变量相当于“宏变量”,Java编译器会在编译时直接把这个类变量出现的地方替换成它的值,因此即使程序使用该静态类变量也不会导致该类的初始化。(此时相当于使用常量)
当使用ClassLoader类的对象loadClass()方法来加载某个类时,该方法只是加载该类,并不会执行该类的初始化,使用Class的静态方法forName()才会导致强制初始化该类。

类加载器负责将.class文件加载到内存中,并为之生成对应的java.lang.Class对象。一个类被载入JVM中,同一个类就不会被再次载入了。
对于Java程序而言,如何识别同一个类:只要该类的全限定类名相同即可(类名和包名都要相同)。
对于类加载器而言,如何识别同一个类:类名相同、包名相同、类加载器也相同。
当JVM启动时,会形成由三个类加载器组成的初始类加载器层次结构:
Bootstrap ClassLoader:根(引导/原始)类加载器:负责加载Java的核心类
Extension ClassLoader:扩展类架子阿奇
System ClassLoader:系统类加载器
类加载机制:
1、全盘负责,当一个类加载器来加载某个Class时,该Class所依赖或引用的其他类都将由该类加载器负责加载。除非显式使用另外一个类加载器来加载。
2、父类委托:JVM首先尝试使用父类加载器来加载某个类,只有当父类加载器无法加载某个类时,才会去使用其子类加载器。
3、缓存机制:类加载会自动保证所有被加载过的类都会被缓存,当程序需要使用某个Class时,类加载器先从缓存区中搜索该Class,只有当缓存区中不存在Class对象时,系统才会读取该类对应的二进制数据,并将其转换为Class对象,存入缓存区中。

Java的4种类加载器:
1、根类加载器(Bootstrap)
2、扩展类加载器(平台类加载器)
3、系统类加载器
4、用户定义的类加载器(通过继承ClassLoader实现)
类加载器之间的父子关系并不是继承上的负责关系,这里的父子关系是类加载器实例之间的关系。
访问JVM的类加载:ClassLoader systemLoader = ClassLoader.getSystemClassLoader()返回系统 类加载器
获取系统类加载器的加载路径:systemLoader.getResources(“”) 返回Enumeration对象
加载路径通常由CLASSPATH环境变量指定。若不指定,以当前路径作为加载路径
获取系统类加载器的父类加载器,得到扩展类加载器:systemLoader.getParent()
若获取扩展类加载器对象的getParent()获取它的父类加载器(根类加载器),将会得到null,这是因为根类加载器并没有继承ClassLoader抽象类,根类加载器并不是Java实现的,所以扩展类加载器的getParent()返回null
系统类加载器是AppClassLoader类的实例,扩展类加载器是PlatformClassLoader类的实例,
实际上,这两个类都是URLClassLoader类的子类。

面试题:能不能写一个类替换Java自身的:java.lang.String类
Java提供了ClassLoader对象来代表加载器
Java的根加载器并不是使用java语言实现的,因此程序无法访问根类加载器。
classLoader提供了如下方法来访问类加载器:
getSystemClassLoader():获取系统类加载器
getPlatformClassLoader():获取平台类加载器(扩展类加载器)
实例方法:对象.getParent()获取上一级父类加载器。

何时实现自定义类加载器:
程序只要继承ClassLoader基类,就可以实现自定义的类加载器。一般来说,并不需要实现自定义的的类加载器。
继承ClassLoader基类实现类加载器时,通常要实现如下两个方法:
Class<?> LoadClass(String name,boolean resolve):该方法是ClassLoad的关键方法,ClassLoader正是调用该方法来得到对应的Class对象。
findClass():根据名称来找到类
通常重写findClass()方法,而不是重写loadClass()方法,loadClass()执行步骤如下:
1、用findLoadedClass(String)检查是否已经加载该类,如果已经加载则直接返回。
2、在父类加载器上调用loadClass()方法,如果父类加载器为null,则使用根类加载器来加载
3、调用findClass(String)方法查找类
如上可知:重写findClass(String)方法可以避免覆盖默认类加载器的父类委托、缓冲机制两种策略。
通常而言,主要是在程序需要对类文件做一些混淆、加密时,此时就可能需要提供自己的类加载器。

URLClassLoader:该类是系统类加载器和扩展类加载器的父类,它是java提供的一个基于URL的类加载器,因此它可以直接加载网络上(任意指定路径)的二进制文件来生成Class对象。
URLClassLoader就是java本身提供的一个用户自定义的类加载器,因此它可以用于加载指定路径(文件系统、网络)的JAR包或二进制文件来生成class对象。有如下构造器:
URLClassLoader(URL[] urls):使用默认的父类加载器创建一个ClassLoader对象,该对象将从url所指定的系列路径来查询并加载类
URLClassLoader(URL[] urls,ClassLoader parent):使用指定父类加载器创建一个ClassLoader对象,该对象将从url所指定的系列路径来查询并加载类
由构造器可知,参数为URL[]数组,该数组元素为url,可以加载网路各个路径的类。
一旦得到了URLClassLoader对象后,就可以调用loadClass()方法加载指定类。

理解反射:
反射作用:动态使用java(创建对象、调用方法)
1、动态创建对象,2、动态调用方法
如果代码是具有一定通用性质的、属于基础框架性质的。由于是框架,因此项目后期可能出现的类都是动态的——此处就必须使用动态创建对象、动态调用方法——这就是反射。
某些对象在运行时会出现两种类型:编译时类型和运行时类型,如:Person p = new Student()
编译时类型为Person,运行时为Student,此时若希望调用该对象的运行时方法(即Student独有的方法),此时就可以使用反射。解决办法:
1、若编译时和运行时完全知道该类具体类型的真实信息,可以先使用instanceof运算符判 断,再强制转换成运行时类型的变量即可
2、编译时无法预知该对象和类可能属于哪些类,程序只依靠运行时信息来发现该对象和类 的真实信息,此时必须使用反射。

Class:代表一个类,Class又是一个对象。
Class代表所有类的类,每个Class实例代表一个具体的类。
类在加载后,系统就会为该类生成一个对应的Class对象,通过该Class对象就可以访问到JVM中的这个类。可以以如下三种方式获得Class对象:
1、使用Class类的静态方法forName(String clazzName)。clazzName参数的值是全限定类名
2、调用某个类的class属性来获取该类对应的Class对象,如:Person.class
3、调用对象的getClass()方法,该方法是Object的方法,该方法返回该对象所属类对应的 Class对象
对比1和2两种方式比较,方式2有两大优势:
优势一:代码更安全,程序在编译阶段就可以检查需要访问的Class对象是否存在
优势二:程序性能更好,因为这种方式无需调用方法。
因此,大部分时候使用第二种方式来获取指定类的Class对象。若只能得到一个字符串,则只能使用方式1,方式1会抛出一个ClassNotFoundException异常。
获取到类对应的Class对象之后,就可以调用Class对象的方法获得该对象和该类的真实信息。

从Class对象获取信息:
类的五大成员:成员变量、方法、构造器、初始化块、内部类
类上注解,这些东西都可以通过反射来获取。
在Class类提供的方法中,带Declared字样的就是获取所有的指定成员,不带Declared字样的只是获取public的指定成员。P858—860
获取构造器:返回值都是Constructor对象(即构造器对象)或Constructor<?>[]
getConstructor(Class<?>…parameterTypes):返回此Class对应类的、带指定形参列表的public 构造器,papameterTypes指定形参列表参数类型。如String类型 则传入时写:String.class
getConstructors():返回此Class对应类的所有public构造器
getDeclaredConstructor(Class<?>…parameterTypes):返回此Class对应类的、带指定形参列表 的构造器访问权限无关
getDeclaredConstructors():返回此Class对应类的所有构造器,与访问权限无关

获取方法:返回值都是Method(即方法对象)或Method[]
getMethod(String name,Class<?>…parameterTypes):返回此Class对象对应类的、带指定形参 列表的public方法,name指定方法名,papameterTypes指定形参列表参数类型。
getMethods():返回此Class对象对应类的所有public方法
getDeclaredMethod(String name,Class<?>…parameterTypes):返回此Class对象对应类的、带 指定形参列表的方法,与方法的访问权限无关。
getDeclaredMethods():返回此Class对象对应类的所有方法,与方法的访问权限无关。

获取成员变量:返回值都是Field(即变量对象)或Field[]
getField(String name):返回此Class对象对应类的、指定名称的public成员变量
getFields():返回此Class对象对应类的所有public成员变量
getDeclaredField(String name):返回此Class对象对应类的、指定名称的成员变量,与访问权 限无关。
getDeclaredFields():返回此Class对象对应类的所有成员变量,与访问权限无关

获取注解:
getAnnotation(Class annotationClass):尝试获取该Class对象对应类上存在的、指定类型 的Annotation,如果该类型的注解不存在,返回null
getDeclaredAnnotation(Class annotationClass):尝试获取直接修饰该Class对象对应类的、 指定类型的Annotation,如果该类型的注解不存在,返回null
getAnnotations(Class annotationClass):返回修饰该Class对象对应类上存在所有Annotation
getDeclaredAnnotation(Class annotationClass):返回直接修饰该Class对象对应类的 Annotation

获取内部类:
getClasses():获取所有public内部类
getDeclaredClasses():返回该Class对象对用类里包含的全部内部类

目前操作的Class本身就是其他类的内部类时,用以下方法获取外部类:
getDeclaringClass():获取该类本身所在的外部类
获取继承的父类:
getSuperclass():返回该Class对象对应类所继承的父类
获取该类所有实现的接口:
getInterfaces():返回该Class对象所实现的全部接口
获取该类的修饰符
Int getModifiers():返回该类或接口的所有修饰符,返回的是一个整数,需要使用Modifier 工具类的方法来解码才可以获取真实的修饰符
Package getPackage():获取此类的包
String getName():返回此Class对象所表示的类的名称
getSimpleName():返回此Class对象所表示的类的简称
Class对象还可以通过以下方法判断该类是否为接口、枚举、注解类型:
isAnnotation():该Class对象是否表示一个注解类型
isAnnotationPresent(Class<? extends Annotation> annotationClass):判断此Class对象是否使用 了Annotation修饰
isAnonymousClass():是否是一个匿名类
isArray():是否是一个数组类
isEnum():shifou shi yige meiju
isInterface():是否是一个接口
isInstance(Object obj):判断obj是否是此Class对象的实例,和instance相似
【注意】反射获取的是运行时Class对象,所以无法获取到只能在源码上保留的注解。

Executable抽象基类,代表可执行的类成员,该类派生的子类有Constructor和Method
,Executable提供了大量方法来获取修饰方法或构造器的注解信息。
isVarArgs():判断该方法或构造器是否包含数量可变的形参
getModifiers():获取该方法或构造器的修饰符
【关键点:】编译java源文件的时候需要增加-parameters选项,这样java编译器才会保留方法的形参名和形参上的 修饰符。
getParameterCount():获取该构造器或方法的形参个数
Parameter[] getParameters():获取该构造器或方法的所有形参。得到Parameter参数后,Parameter:代表一个参数,可用于获取参数相关的信息,Parameter也提供了大量方法来获取参数的泛型等参数信息:
getModifiers():获取修饰该形参的修饰符
String getName():获取形参名
Type getParameterizedType():获取带泛型的形参类型
Class<?> getType():获取形参类型
Boolean isNamePresent():该方法返回该类的class文件中是否包含了方法的形参名信息。
Boolean isVarArgs():该方法用于判断该参数是否为个数可变的形参。否则得不到形参名
使用这些方法时,需要在编译时加-parameters选项。

使用反射生成并操作对象:
创建对象:使用Class对象获取指定的Constructor对象,再调用Constructor对象的newInstance()方法来创建该Class对象对应类的实例。
在很多Java EE框架中,需要根据配置文件信息来创建Java对象,从配置文件中读取的只是某个类的字符串类名,程序需要根据该字符串来创建对应的实例就必须使用反射。
【重要发现】:构造器的真正的方法名:
动态创建对象:
通过默认的构造器
//根据字符创来创建对象(该对象可以是动态改变的)
Class<?> clazz = Class.forName(“java.util.Date”);
Constructor c = clazz.getConstructor();//获取public 无参数的构造器
//newInstance用于调用指定构造器来创建对象,可传入可变个数的参数。
Object obj = c.newInstance();
通过Class动态创建对象:实际是通过调用构造器的newInstance(参数)方法来创建对象。
获取指定构造器创建:调用时需要传入对应的参数。
Class<?> clazz = Class.forName(“java.util.Date”);
Constructor c = clazz.getConstructor(Long.class);//获取指定参数类型构造器
Object obj = C.newInstance(220033);//传入对应的参数创建对象。

动态地调用方法:
通过调用Method的invoke(调用者,个数可变的参数)方法来调用。
Object invoke(Object obj,Object…args):invoke第一个参数obj是方法的调用者,因此,如果方法是static方法,invoke的第一个参数可使用null,后面是该方法的实参
如果需要调用某个对象的private方法,则先调用Mthod如下方法:
setAccessible(boolean flag):将Method对象的accessible设置为指定的布尔值。值为true指示该Method在使用时应该取消Java语言的访问权限检查,值为false,指示该Method在使用时要实施Java语言的访问权限检查,。
以上方法并不属于Method本身,而是属于它的父类AccessibleObject,因此Method、Constructor、Field都可以调用该方法取消访问权限检查。

动态访问Field
getField()获取到Field变量对象后,就可以调用Fiel=对象的如方法动态获取和设置Field的值:
getXxx(Object obj):返回obj对象的成员变量field的值,此处Xxx对应8种基本类型,如果 该成员变量的类型是引用类型,则取消get后面的Xxx,参数obj为后去的对象
setXxx(target,val):将obj对象的该成员变量设置成val值,参数target为获取的对象,val为 要设置的值
Field的get或者set方法的第一个参数是调用者,因此如果该Field是static,get或者set时第一个参数可以使用null.
【总结论】:不管是构造器、还是方法、Field,即使它是private修饰的。在反射时,只要调用setAccessible(true),它们就可以不去管访问权限的修饰符的限制。

动态生成数组:
在java.lang.reflect包下还提供了一个Array类,Array类对象可以代表所有的数组。程序可以通过使用Array来动态地创建数组。
Array类的方法都是static,是一个工具类。如下:
static Object newInstance(Class<?> componentType,int…length):创建一个具有指定的元素类 型、指定维度的新数组,length是一个可变参数,填在最后的是元素个数,如要创建一 个String类型的三维数组 Array.newInstance(String.class,3,4,10)表示一个3维的,1维10 个元素,二维4个,三维3个。
Static xxx getXxx(Object array,int index):返回数组中第index个元素,其中Xxx是各种基本数 据类型,如果数组元素是引用类型,则方法变为get(Object array,int index)。
Static void setXxx((Object array,int index,xxx val):将array数组中第index个元素的值设为val,其中xxx是各种基本数据类型,如果是引用类型,可以去掉Xxx

Java 11新增的嵌套访问权限
【java的private成员的特殊点】
1、Java的外部类可以直接访问内部类的private成员
2、同一个类中的多个内部类之间,它们可以互相访问对方的private成员。
但在java 11之前
1、java的外部类通过反射反而不能访问内部类的private成员,除非设置setAccessible(true);
2、同一个类的多个内部类之间,通过反射也不能互相访问对方的private成员,除非设置setAccessible(tru e);

Java11之后,java把它们进行了统一,
1、Java的外部类可以直接、也可以通过反射访问内部类的private成员
2、同一个类中的多个内部类之间,它们可以直接互相访问、也可以通过反射访问对方的private成员。

Java 11之所以能进行这样的改进,关键就在于增加了一个嵌套访问权限:
Java 11为Class增加了如下方法:
GetNestHost():获取该类所在的属主类——外部类的属主是它本身
IsNestmateOf():判断一个类是否为另一个类的嵌套同伴——同一类的多个内部类属于嵌套同伴,外部类和它所有的内部类之间也属于嵌套同伴。
Class[] getNestMenbers():获取该类所有的内部类——其中也包括它本身。

Java.lang.reflect包下提供了Proxy类和InvocationHandler接口,使用这个类和接口可以生成JDK动态代理类或动态代理对象。Proxy类提供了用于创建动态代理类和动态代理对象的静态方法,是所有动态代理类的父类,如果需要在程序中为一个或多个接口动态生成实现类可以使用Proxy来创建动态代理类,如果需要为一个或多个接口动态地创建实例,也可以使用Proxy来创建动态代理实例。

JDK动态代理:可以在运行时动态地生成一个类、或者动态生成一个类的实例,被生成的类,看不到class文件,或者说它在内存中动态生成了class文件。
JDK动态代理类似的技术:
CGLIB:Code generator lib(代码生成库):可以在运行时动态地生成一个类、或者动态生成一个类的实例。
Javassist:运行时动态地生成一个类、或者动态生成一个类的实例。(Hibernate)
JDK动态代理要求被代理的类必须实现了接口,但CGLIB和Javcassist就没有这个限制。

JDK动态代理的实现机制:
Proxy类:提供了如下方法来创建动态代理类和动态代理实例:
Static Class<?> GetProxyClass(ClassLoader loader,Class<?>…interface):创建一个动态代理类所对应的Class对象,该代理类将实现interface所指定的多个接口,第一个ClassLoader参数指定生成动态代理类的类加载器。
Static Object newProxyInstance(ClassLoader loader,Class<?> interfaces,InvocationHandler h):创建一个动态代理对象,该代理对象的实现类实现了interface指定的系列接口欧,执行代理对象的每个方法都会被替换执行InvocationHadnler对象的invoke方法。
若使用GetProxyClass创建动态代理类后,希望使用该动态代理类创建对象,依然需要传入一个InvocationHandler对象,即系统生成的每个代理对象都有一个与之关联关联的invocationHandler对象。
动态代理对象需要实现所代理类的所有接口里定义的所有方法,实现这些方法体由InvocationHadnler对象
【关键点】:当程序员为InvocationHandler的invoke(…)方法提供实现体时,实际上就是为动态代理类的所有方法体提供了实现体。
直接生成动态代理类实例的代码片段1:
InvocationHandler handler = new MyInvocationHandler(…);
Foo f = (Foo) Proxy.newProxyInstance(Foo.class.getClassLoader(),
new Class<?>[] { Foo.class },handler);

先生成动态代理类,再生成实例的代码片段2(功能类似,但先生成动态代理类本身):
Class proxyClass = Proxy.getProxyClass(Foo.class.getClassLoader(), Foo.class );
Constructor con = proxyClass.getConstructor(InvocationHandler.class);
InvocationHandler handler = new MyInvocationHandler(…);
Foo f = (Foo)con.newInstance(handler);
自定义动态代理:
1、定义一个接口,用于被动态代理生成动态代理类或动态代理对象
2、定义一个类实现InvocationHandler接口,并实现invoke方法,在调用代理对象的所有方法时,都会被替换成执行该方法,调用的对象、方法名和参数会传入该invoke方法,如下:
Public Object invoke(Object proxy,Method method,Object[] args):
proxy:代表动态代理对象
method:代表正在执行的方法
args:代表调用目标方法时传入的实参
3、创建实现InvocationHandler接口的类对象,得到InvocationHandler对象,随后使用Proxy的newProxyInstance(Classloader,InvocationHandler)创建动态代理对象,此时调用动态代理对象的所有方法就会被替换成调用InvocationHandler对象的invoke方法(调用的对象、方法名、参数等都会传递给invoke作为参数)。

JDK动态代理与AOP
Spring的AOP就是基于JDK动态代理的。当被代理的目标类没有实现接口,Spring AOP不得不使用CGLIB作为补充
【说明】:由于动态代理类的所有方法都将被替换成InvocationHandler的invoke()方法,因此,如果要为代理对象的所有方法都增加某种通用处理(或者进行某种修改),只要修改该invoke方法即可。
在该invoke方法的method参数代表了正在被调用的方法(method是动态变化),因此程序可通过method去回调目标对象正在被执行的方法。
实现步骤:
1、定义一个Dog接口,再定义一个GunDog实现Dog接口。且实现info和run方法
2、创建自定义的调用处理对象类MyInvocationHandler实现InvocationHandler接口
3、实现invoke方法,定义对实现接口Dog的类对象的所有方法都执行通用处理的代码块
,在中间通过method参数回调执行的方法:method.invoke(target,age)
4、此时,只要调用动态代理类对象执行任何方法,都相当于执行invoke方法,在所有方法前后都已经加上了通用的处理。

泛型和Class类,如:String.class的类型实际是Class,如果Class对应的类型位置,则使用Class<?>
若使用Field对象的getType方法只能获取到普通类型的参数类型,若需要获取指定成员变量的泛型类型,先使用如下方法获取该成员变量的泛型类型:
Field f = clazz.getField(“xx”);
Type gTyoe = f.getGenericType();
以上得到泛型类型Type后,强制转换为ParameterizedType对象,ParameterizedType代表被参数化的类型,也就是增加了泛型限制的类型,ParameterizedType有一下方法获取泛型:
getRawType():返回没有泛型信息的原始类型,也就是没有带泛型的类型
getActualTypeArguments()返回泛型参数的类型(有可能有多个,如Map<String,Integer>)

Java 9的模块化
模块化的价值:
1、java太庞大,急切需要把java分模块加载,使得java运行环境更轻量化。
2、包的职责太薄弱了,通过模块可以把java按功能分成不同的组件。
一个项目可包含N个模块,一个模块包含N个包,一个包包含N个类。
从文件结构来看,项目需要专门为模块建一个文件夹
Module-info.java是一个模块说明文件,该文件主要定义以下5种语句
1、导出语句:export,将该模块
2、开放语句:open
3、需要语句:require
4、使用语句:use
5、提供语句:provide

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值