【Java基础08】反射

【Java基础系列】
【Java基础01】基础概述
【Java基础02】常用类
【Java基础03】时间类
【Java基础04】异常
【Java基础05】枚举类
【Java基础06】泛型
【Java基础07】注解
【Java基础08】反射
【Java基础09】代理
【Java基础10】IO流

反射

文章概述:总结描述Java基础中反射的概述和使用,包括注解的概念、Class类对象的概念、类加载器ClassLoader的概念及涉及的双亲委派模型的原理,获取类对象的三种方式,反射操作获取类的构造方法、成员变量、成员方法、泛型及注解信息。

前言

反射在Java中是很强的一种技术,很多优秀的开源框架都是通过反射完成的,但是因为java反射影响性能,所以被运行时注解APT替代了,java反射有个开源框架jOOR相信很多人都用过,不过这里我们还是主要学习反射的基础语法。基础是根基之本,对于学习使用进阶起到重要作用。

反射是框架设计的灵魂
(使用的前提条件:必须先得到代表的字节码的Class,Class类用于表示.class文件(字节码))

一、反射的概述

Java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。

实际上,我们创建的每一个类也都是对象,即类本身是java.lang.Class类的实例对象。这个实例对象称之为类对象,也就是Class对象。

反射就是把java类中的各种成分映射成一个个的Java对象。

二、Class对象

Class对象的由来是将class文件读入内存,并为之创建一个Class对象。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Kpvxn45b-1678346571336)(images/【Java基础08】反射/20170513133210763.png)]

其中Class对象比较特殊,了解一下这个Class类

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KuzhE9KE-1678346571337)(images/【Java基础08】反射/20170513135521667.png)]

  • Class 类的实例表示正在运行的 Java 应用程序中的类和接口。也就是jvm中有N多的实例每个类都有该Class对象。(包括基本数据类型)
  • Class 没有公共构造方法。Class 对象是在加载类时由 Java 虚拟机以及通过调用类加载器中的defineClass 方法自动构造的。也就是这不需要我们自己去处理创建,JVM已经帮我们创建好了。
  • Class 对象用于提供类本身的信息,比如类的构造方法,各种属性,各种普通方法

Class 没有公共的构造方法,方法共有64个

![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lvDyYwxE-1678346571338)(images/【Java基础08】反射/20170513144141409.png)](https://img-blog.csdnimg.cn/9c2a300725664b54952e102be53375bc.png)

三、类加载器ClassLoader

3.1 类加载器ClassLoader

虚拟机设计团队把类加载阶段中的**“通过一个类的全限定名来获取此类的二进制字节流”这个动作放到 Java 虚拟机外部去实现**,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为“类加载器”。类装载器是用来把类(class)装载进 JVM 的。

从 Java 虚拟机的角度来讲,只存在以下两种不同的类加载器:

  • 启动类加载器(Bootstrap ClassLoader),使用 C++ 实现,是虚拟机自身的一部分
  • 所有其它类的加载器,使用 Java 实现,独立于虚拟机,并且全部继承自抽象类 java.lang.ClassLoader。

从 Java 开发人员的角度看,类加载器可以划分得更细致一些:

  • 启动类加载器(Bootstrap ClassLoader):前面已经大致介绍过了,这个类加载器负责将存放在 <JRE_HOME>\lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,直接使用 null 代替即可。
  • 扩展类加载器(Extension ClassLoader):这个类加载器是由 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将 <JAVA_HOME>/lib/ext 或者被 java.ext.dir 系统变量所指定路径中的所有类库加载到内存中,开发者可以直接使用扩展类加载器。
  • 应用程序类加载器(Application ClassLoader):这个类加载器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。由于这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此一般称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

启动加载类加载,加载核心库,例如:rt.jar包

扩展加载器加载,加载外部依赖

系统类加载器,加载对应程序的类

下面代码结果可以分析出三种类加载器的父子级关系

public class Reflex_02_ClassLoader {

    public static void main(String[] args) throws ClassNotFoundException {
        //获取系统类的加载器
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println(systemClassLoader);

        //获取系统类的加载器的父加载器 --> 扩展加载器
        ClassLoader parent = systemClassLoader.getParent();
        System.out.println(parent);

        //获取扩展加载器的父加载器 --> 根加载器
        ClassLoader parent1 = parent.getParent();
        System.out.println(parent1);

        //获取类对应的加载器
        ClassLoader classLoader = Class.forName("com.wei.moudle.reflex.Reflex_02_ClassLoader").getClassLoader();
        System.out.println(classLoader);

        classLoader = Class.forName("java.lang.Object").getClassLoader();
        System.out.println(classLoader);

    }
}

输出:

sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@2e817b38
null
sun.misc.Launcher$AppClassLoader@18b4aac2
null

3.2 双亲委派模型(类加载器的层次关系)

3.2.1 概念

上图是上面所介绍的这几种类加载器的层次关系,称为类加载器的双亲委派模型。该模型要求除了顶层的启动类加载器外,其它的类加载器都要有自己的父类加载器。

双亲委派模型,其实就是一种类加载器的层次关系

3.2.2 工作过程

如果一个类加载器收到了类加载请求,它首先不会自己尝试去加载这个类,而是把请求委派给父加载器去完成,每一个层次的类加载器都是如此。因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才开始自己去加载。

3.2.3 优点
  • 使得 Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一。

    例如 :java.lang.Object 存放在 rt.jar 中,如果编写另外一个 java.lang.Object 并放到 ClassPath 中,程序可以编译通过。由于双亲委派模型的存在,所以在 rt.jar 中的 Object 比在 ClassPath 中的 Object 优先级更高,这是因为 rt.jar 中的 Object 使用的是启动类加载器,而 ClassPath 中的 Object 使用的是应用程序类加载器。那么程序中所有的 Object 都是rt.jar 的 Object。

  • 避免了多份同样字节码的加载,内存是宝贵的,没必要保存相同的两份 Class 对象。JVM中每个类只会存在一个类对象

3.2.4 实现原理

双亲委派模型对于保证 Java 程序的稳定运作很重要,但它的实现却非常的简单,实现双亲委派的代码都集中在 java.lang.ClassLoader 的 loadClass() 方法中。

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
                c = findClass(name);

                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

从 loadClass() 的源码,我们再来看一下双亲委派模型的工作过程。它可以被下面这张图来概括。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-m1L0ahHV-1678346571338)(images/【Java基础08】反射/image-20230308170401408.png)]

  • 首先从底向上的检查类是否已经加载过,就是这行代码:Class<?> c = findLoadedClass(name);
  • 如果都没有加载过的话,那么就自顶向下的尝试加载该类。

四、获取类对象的三种方式

  • 通过全类名获取 Class.forName() (常用)
  • 通过类名获取 Object.class
  • 通过对象获取 new Object().getClass()

三种方式中,常用第一种,第二种需要导入类的包,依赖太强,不导包就抛编译错误。第三种对象都有了就无需反射了。一般都第一种,一个字符串可以传入也可写在配置文件中等多种方法。

public static void main(String[] args) throws ClassNotFoundException {
    Class aClass = Class.forName("com.wei.domain.user.User");
    System.out.println(aClass);
    // 一个类在内存中只有一个class对象
    // 一个类被加载后,类的整个结构都会被封装在class对象中
    Class regionEntityClass = com.wei.domain.user.User.class;
    System.out.println(regionEntityClass);

    com.wei.domain.user.User regionEntity = new com.wei.domain.user.User();
    Class bClass = regionEntity.getClass();
    System.out.println(bClass);
}

输出:

class com.wei.domain.user.User
class com.wei.domain.user.User
class com.wei.domain.user.User

**在一个JVM中,一种类,只会有一个类对象存在。**所以以上三种方式取出来的类对象,都是一样。(此处准确是在ClassLoader下,只有一个类对象)

五、反射获取类的各种属性

5.1 获取构造方法

与传统的通过new 来获取对象的方式不同反射机制,反射会先拿到对象的“类对象”,然后通过类对象获取“构造器对象”再通过构造器对象创建一个对象。

构造方法不同时,获取构造器对象的方法

//无参构造方法
public Reflex_03_Constructor(){
    System.out.println("调用了公有、无参构造方法执行了。。。");
}

// 一个参数的构造方法
public Reflex_03_Constructor(String str){
    System.out.println("参数1 = " + str);
}

//有多个参数的构造方法
public Reflex_03_Constructor(String name ,String code){
    System.out.println("参数1:"+name+"参数2:"+ code);
}

//受保护的构造方法
protected Reflex_03_Constructor(boolean n){
    System.out.println("受保护的构造方法 参数1: = " + n);
}

//私有构造方法
private Reflex_03_Constructor(float hp){
    System.out.println("私有的构造方法   参数1::"+ hp);
}

private void printInfo() {
    System.out.println("方法被调用....");
}

通过反射机制获取对象

/*
	 * 通过Class对象可以获取某个类中的:构造方法、成员变量、成员方法;并访问成员;
	 *
	 * 1.获取构造方法:
	 * 		1).批量的方法:
	 * 			public Constructor[] getConstructors():所有"公有的"构造方法
	            public Constructor[] getDeclaredConstructors():获取所有的构造方法(包括私有、受保护、默认、公有)

	 * 		2).获取单个的方法,并调用:
	 * 			public Constructor getConstructor(Class... parameterTypes):获取单个的"公有的"构造方法:
	 * 			public Constructor getDeclaredConstructor(Class... parameterTypes):获取"某个构造方法"可以是私有的,或受保护、默认、公有;
	 *
	 * 2.创建对象
	 * 		Constructor对象调用newInstance(Object... initargs)
	 */


public static void main(String[] args) throws Exception {
    //1.加载Class对象
    Class clazz = Class.forName("com.wei.moudle.reflex.Reflex_03_Constructor");


    //2.获取所有公有构造方法
    System.out.println("**********************所有公有构造方法*********************************");
    Constructor[] conArray = clazz.getConstructors();
    for(Constructor c : conArray){
        System.out.println(c);
    }


    System.out.println("************所有的构造方法(包括:私有、受保护、默认、公有)***************");
    conArray = clazz.getDeclaredConstructors();
    for(Constructor c : conArray){
        System.out.println(c);
    }

    System.out.println("*****************获取公有、无参的构造方法*******************************");
    Constructor con = clazz.getConstructor(null);
    //1>、因为是无参的构造方法所以类型是一个null,不写也可以:这里需要的是一个参数的类型,切记是类型
    //2>、返回的是描述这个无参构造函数的类对象。
    System.out.println("con = " + con);
    //调用构造方法
    Reflex_03_Constructor constructor1 = (Reflex_03_Constructor)con.newInstance();
    constructor1.printInfo();

    System.out.println("******************获取私有构造方法,并调用*******************************");
    con = clazz.getDeclaredConstructor(float.class);
    System.out.println(con);
    //调用构造方法
    con.setAccessible(true);//暴力访问(忽略掉访问修饰符)
    Reflex_03_Constructor constructor2 = (Reflex_03_Constructor)con.newInstance(100);
    constructor2.printInfo();
}

输出:

**********************所有公有构造方法*********************************
public com.wei.moudle.reflex.Reflex_03_Constructor(java.lang.String,java.lang.String)
public com.wei.moudle.reflex.Reflex_03_Constructor(java.lang.String)
public com.wei.moudle.reflex.Reflex_03_Constructor()
************所有的构造方法(包括:私有、受保护、默认、公有)***************
private com.wei.moudle.reflex.Reflex_03_Constructor(float)
protected com.wei.moudle.reflex.Reflex_03_Constructor(boolean)
public com.wei.moudle.reflex.Reflex_03_Constructor(java.lang.String,java.lang.String)
public com.wei.moudle.reflex.Reflex_03_Constructor(java.lang.String)
public com.wei.moudle.reflex.Reflex_03_Constructor()
*****************获取公有、无参的构造方法*******************************
con = public com.wei.moudle.reflex.Reflex_03_Constructor()
调用了公有、无参构造方法执行了。。。
方法被调用....
******************获取私有构造方法,并调用*******************************
private com.wei.moudle.reflex.Reflex_03_Constructor(float)
私有的构造方法   参数1::100.0
方法被调用....

总结

1.获取构造器对象方法:
1).批量的方法:
public Constructor[] getConstructors():所有"公有的"构造方法
public Constructor[] getDeclaredConstructors():获取所有的构造方法(包括私有、受保护、默认、公有)
2).获取单个的方法:
public Constructor getConstructor(Class… parameterTypes): 获取单个的"公有的"构造方法
public Constructor getDeclaredConstructor(Class…parameterTypes):获取"某个构造方法"可以是私有的,或受保护、默认、公有;

con.setAccessible(true);//暴力访问(忽略掉访问修饰符,否则私有方法不能访问)

5.2 获取成员变量

设置两个公有成员变量,一个私有成员变量

public String linkMan;

public String linkTel;

private String address;
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
    Class aClass = Class.forName("com.wei.moudle.reflex.Reflex_04_Fields");

    //包名 + 类名
    System.out.println(aClass.getName());
    //类名
    System.out.println(aClass.getSimpleName());

    Reflex_04_Fields reflex_04_fields = (Reflex_04_Fields) aClass.newInstance();

    //找到public属性
    System.out.println("**********************公有成员变量*********************************");
    Field[] fields = aClass.getFields();
    for (Field field : fields) {
        System.out.println(field);
        System.out.println(field.getName());
    }
    System.out.println("**********************所有成员变量*********************************");
    //找到全部属性
    fields = aClass.getDeclaredFields();
    for (Field field : fields) {
        System.out.println(field);
        System.out.println(field.getName());
    }

    fields[0].set(reflex_04_fields, "luo");
    System.out.println(reflex_04_fields.getLinkMan());
}

输出:

com.wei.moudle.reflex.Reflex_04_Fields
Reflex_04_Fields
**********************公有成员变量*********************************
public java.lang.String com.wei.moudle.reflex.Reflex_04_Fields.linkMan
linkMan
public java.lang.String com.wei.moudle.reflex.Reflex_04_Fields.linkTel
linkTel
**********************所有成员变量*********************************
public java.lang.String com.wei.moudle.reflex.Reflex_04_Fields.linkMan
linkMan
public java.lang.String com.wei.moudle.reflex.Reflex_04_Fields.linkTel
linkTel
private java.lang.String com.wei.moudle.reflex.Reflex_04_Fields.address
address
luo

总结

getField和getDeclaredField的区别
getField 只能获取public的,包括从父类继承来的字段。
getDeclaredField 可以获取本类所有的字段,包括private的,但是 不能获取继承来的字段。 (注: 这里只能获取到private的字段,但并不能访问该private字段的值,除非加上setAccessible(true))

5.3 获取成员方法

public void printInfo() {
    System.out.println("方法被调用....");
}

private void printInfoWithStr(String info) {
    System.out.println("方法被调用带信息:" + info);
}
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
    Class aClass = Class.forName("com.wei.moudle.reflex.Reflex_04_Fields");

    Reflex_04_Fields reflex_04_fields = (Reflex_04_Fields) aClass.newInstance();

    System.out.println("**********************获取方法*********************************");
    //获取方法
    aClass.getMethods();
    aClass.getDeclaredMethods();

    //获取指定方法
    Method printInfo = aClass.getMethod("printInfo", null);
    Method printInfoWithStr = aClass.getDeclaredMethod("printInfoWithStr", String.class);
    System.out.println(printInfo);
    System.out.println(printInfoWithStr);
    printInfo.invoke(reflex_04_fields, null);
    printInfoWithStr.invoke(reflex_04_fields, "hello");
}

输出

**********************获取方法*********************************
public void com.wei.moudle.reflex.Reflex_04_Fields.printInfo()
private void com.wei.moudle.reflex.Reflex_04_Fields.printInfoWithStr(java.lang.String)
方法被调用....
方法被调用带信息:hello

总结

获取成员方法:
public Method getMethod(String name ,Class<?>… parameterTypes):获取"公有方法";(包含了父类的方法也包含Object类)
public Method getDeclaredMethods(String name ,Class<?>… parameterTypes) :获取成员方法,包括私有的(不包括继承的)
参数解释:
  name : 方法名;
  Class: 形参的Class类型对象
调用方法
Method --> public Object invoke(Object obj,Object… args):
参数说明:
  obj : 要调用方法的对象;
  args:调用方式时所传递的实参;

5.4 获取main方法

public static void main(String[] args) {
    try {
        //1、获取HeroPlus对象的字节码
        Class clazz = Class.forName("com.wei.moudle.reflex.Reflex_01_GetClass");

        //2、获取main方法,第一个参数:方法名称,第二个参数:方法形参的类型,
        Method methodMain = clazz.getMethod("main", String[].class);
        //3、调用main方法
        // methodMain.invoke(null, new String[]{"a","b","c"});
        //第一个参数,对象类型,因为方法是static静态的,所以为null可以,第二个参数是String数组,这里要注意在jdk1.4时是数组,jdk1.5之后是可变参数
        //这里拆的时候将  new String[]{"a","b","c"} 拆成3个对象。所以需要将它强转。
        methodMain.invoke(null, (Object)new String[]{"a","b","c"});//方式一
        // methodMain.invoke(null, new Object[]{new String[]{"a","b","c"}});//方式二
    } catch (Exception e) {
        e.printStackTrace();
    }
}

5.5 反射操作泛型

Java 中的泛型仅仅是给编译器 javac 使用的,确保数据的安全性和免去强制类型转换的麻烦,但是一旦编译完成,所有与泛型有关的类型全部擦除。
使用泛型直接读取泛型,是读取不到的,因为反射是操作加载以后的类的。
Java新增的数据类型代表不能被归一到Class类中的类型但是又和原始类型齐名的类型

为了通过反射操作这些类型以迎合实际开发的需要

类型描述
ParameterizedType表示一种参数化的类型 , 比如 Collection,可以获取 String信息,List
GenericArrayType泛型数组类型
TypeVariable各种类型变量的公共父接口
WildcardType代表一种通配符类型表达式,比如? extends Number,? super Integer (Wildcard 是一个单词,就是通配符)
public void test01(Map<String, User> map, String value) {
}

public Map<String, User> test02(String value) {
    return null;
}

public static void main(String[] args) throws NoSuchMethodException {
    Method test01 = ClassTTest.class.getMethod("test01", Map.class, String.class);

    // 获取入参
    Type[] genericParameterTypes = test01.getGenericParameterTypes();
    for (Type genericParameterType : genericParameterTypes) {
        System.out.println("###:"+genericParameterType);
        // 如果参数类型属于 ParameterizedType 参数化的类型 如:List<String>
        if (genericParameterType instanceof ParameterizedType) {
            Type[] actualTypeArguments = ((ParameterizedType) genericParameterType).getActualTypeArguments();
            for (Type actualTypeArgument : actualTypeArguments) {
                System.out.println(actualTypeArgument);
            }
        }
    }

    System.out.println("##########");
    Method test02 = ClassTTest.class.getMethod("test02", String.class);

    // 获取返回结果
    Type genericReturnType = test02.getGenericReturnType();
    if (genericReturnType instanceof ParameterizedType) {
        Type[] actualTypeArguments = ((ParameterizedType) genericReturnType).getActualTypeArguments();
        for (Type actualTypeArgument : actualTypeArguments) {
            System.out.println(actualTypeArgument);
        }
    }

    Type[] genericParameterTypes1 = test02.getGenericParameterTypes();
    for (Type genericParameterType1 : genericParameterTypes1) {
        System.out.println("###:"+genericParameterType1);
    }

}

输出:

###:java.util.Map<java.lang.String, com.wei.moudle.reflex.User>
class java.lang.String
class com.wei.moudle.reflex.User
###:class java.lang.String
##########
class java.lang.String
class com.wei.moudle.reflex.User
###:class java.lang.String

5.6 反射操作注解

关于反射操作注解在【Java基础07】注解中也有相关例子的应用

public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException {
    Class c1 = Class.forName("com.wei.moudle.reflex.User");

    Annotation[] annotations = c1.getAnnotations();
    for (Annotation annotation : annotations) {
        System.out.println(annotation);
    }

    method annotation = (method) c1.getAnnotation(method.class);
    System.out.println(annotation.value());
    Field id = c1.getDeclaredField("id");
    field annotation1 = id.getAnnotation(field.class);
    System.out.println(annotation1.len()+annotation1.name());

}
}

@Data
@method("test_user")
class User {

    @field(name = "user_id", len = 10)
    private String id;
    @field(name = "user_age", len = 8)
    private Integer age;
}

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface method {
    String value();
}

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface field {
    String name();
    int len();

输出:

@com.wei.moudle.reflex.method(value=test_user)
test_user
10user_id

六、反射的用法举例

  • 使用JDBC连接数据库时,使用Class.forName(驱动类名),通过反射加载数据库的举动类。
  • Spring框架的IOC(动态加载管理Bean)创建对象以及AOP(动态代理)使用反射进行底层实现。
  • MyBatis框架的Mapper接口代理对象的创建,也是通过反射来实现的。

七、反射的优缺点

优点:

  • 自由,使用灵活,不受类的访问权限限制。
  • 可以根据指定类名、方法名来实现方法调用,非常适合实现业务的灵活配置。

缺点:

  • 反射不受类的访问权限限制,所以其安全性低,很大部分的java安全问题都是反射导致的。
  • 相对于正常的对象的访问调用,反射因为存在类和方法的实例化过程,性能也相对较低。
  • 破坏java类封装性,类的信息隐藏性和边界被破坏。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值