JAVASE(七)反射和注解

反射就是把Java类中的各个成分映射成一个个的Java对象。即在运行状态中,对于任意一个类,都能够知道这个类所有的属性和方法,对于任意一个对象,都能调用它的任意一个方法和属性。这种动态获取信息及动态调用对象方法的功能叫Java的反射机制

简而言之,我们可以通过反射机制,获取到类的一些属性,包括类里面有哪些字段,有哪些方法,继承自哪个类,甚至还能获取到泛型!它的权限非常高,慎重使用!

Java类加载机制

在学习Java的反射机制之前,我们需要先了解一下类的加载机制,一个类是如何被加载和使用的
在这里插入图片描述
在Java程序启动时,JVM会将一部分类class文件)先加载(并不是所有的类都会在一开始加载),通过ClassLoader将类加载,在加载过程中,会将类的信息提取出来(存放在元空间中,JDK1.8之前存放在永久代),同时也会生成一个Class对象存放在内存堆内存),注意此Class对象只会存在一个,与加载的类唯一对应

思考:既然说和与加载的类唯一对应,那如果我们手动创建一个与JDK包名一样,同时类名也保持一致,那么JVM会加载这个类吗?

package java.lang;

public class String {       //JDK提供的String类也是
    public static void main(String[] args) {
        System.out.println("lbwnb");
    }
}

我们发现,会出现以下报错:

java: 程序包已存在于另一个模块中: java.base

但是我们明明在自己写的String类中定义了main方法啊,为什么会找不到此方法呢?实际上这是ClassLoader的双亲委派机制在保护Java程序的正常运行:
在这里插入图片描述
java9模块化之后,对classloader有所改造,其中一点就是将ext classloader改为platform classloader

实际上我们的类最开始是由BootstarpClassLoader进行加载,BootstarpClassLoader用于加载JDK提供的类,而我们自己编写的类实际上是AppClassLoader只有BootstarpClassLoader都没有加载的类,才会让AppClassLoader来加载,因此我们自己编写的同名包同名类不会被加载,而实际要去启动的是真正的String类,也就自然找不到main方法了!

public class Main {
    public static void main(String[] args) {
        System.out.println(Main.class.getClassLoader());   //查看当前类的类加载器     AppClassLoader
        System.out.println(Main.class.getClassLoader().getParent());  //父加载器    PlatformClassLoader
        System.out.println(Main.class.getClassLoader().getParent().getParent());  //爷爷加载器   null
        System.out.println(String.class.getClassLoader());   //String类的加载器  null
    }
}

由于BootstarpClassLoader是C++编写的,我们在Java中是获取不到的。

Class对象

通过前面,我们了解了类的加载,同时会提取一个类的信息生成Class对象存放在内存中,而反射机制其实就是利用这些存放的类信息,来获取类的信息和操作类。那么如何获取到每个类对应的Class对象呢,我们可以通过以下方式:

public class Main {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<String> clazz = String.class;     //使用class关键字,通过类名获取
        Class<?> clazz2 = Class.forName("java.lang.String");    //使用Class类静态方法forName(),通过包名.类名获取,注意返回值是Class<?>
        Class<?> clazz3 = new String("lbwnb").getClass();   //通过实例对象获取
    }
}

注意Class类也是一个泛型类,只有第一种方法,能够直接获取到对应类型的Class对象,而以下两种方法使用了?通配符作为返回值,但是实际上都和第一个返回的是同一个对象

public class Main {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<String> clazz = String.class;     //使用class关键字,通过类名获取
        Class<?> clazz2 = Class.forName("java.lang.String");    //使用Class类静态方法forName(),通过包名.类名获取,注意返回值是Class<?>
        Class<?> clazz3 = new String("lbwnb").getClass();   //通过实例对象获取

        System.out.println(clazz == clazz2);    // true
        System.out.println(clazz == clazz3);    // true
    }
}

通过比较,验证了我们一开始的结论,在JVM中每个类始终只存在一个Class对象,无论通过什么方法获取,都是一样的。现在我们再来看看这个问题:

public class Main {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> clazz = int.class;     //基本数据类型有Class对象吗?
        System.out.println(clazz);      // int
    }
}

迷了,不是每个类才有Class对象吗,基本数据类型又不是类,这也行吗?实际上,基本数据类型也有对应的Class对象(反射操作可能需要用到),而且我们不仅可以通过class关键字获取,其实本质上是定义在对应的包装类中的:

/**
 * The {@code Class} instance representing the primitive type
 * {@code int}.
 *
 * @since   JDK1.1
 */
@SuppressWarnings("unchecked")
public static final Class<Integer>  TYPE = (Class<Integer>) Class.getPrimitiveClass("int");

/*
 * Return the Virtual Machine's Class object for the named
 * primitive type
 */
static native Class<?> getPrimitiveClass(String name);   //C++实现,并非Java定义

每个包装类中(包括Void),都有一个获取原始类型Class方法,注意,getPrimitiveClass获取的是原始类型,并不是包装类型,只是可以使用包装类来表示

public class Main {
    public static void main(String[] args) throws ClassNotFoundException {
        System.out.println(Integer.TYPE == int.class);  // true
    }
}

通过对比,我们发现实际上包装类型都有一个TYPE,其实也就是基本类型的Class,那么包装类的Class和基本类的Class一样吗?

public class Main {
    public static void main(String[] args) throws ClassNotFoundException {
        System.out.println(Integer.TYPE == Integer.class);  // false
    }
}

我们发现,包装类型的Class对象并不是基本类型Class对象。数组类型也是一种类型,只是编程不可见,因此我们可以直接获取数组的Class对象:

public class Main {
    public static void main(String[] args) {
        Class<String[]> clazz = String[].class;
        System.out.println(clazz.getName());    //获取类名称(得到的是包名+类名的完整名称) [Ljava.lang.String;
        System.out.println(clazz.getSimpleName());  // String[]
        System.out.println(clazz.getTypeName());    // java.lang.String[]
        System.out.println(clazz.getClassLoader());     //获取它的类加载器  null
        System.out.println(clazz.cast(new Integer("10")));  //强制类型转换
        /**
         * Exception in thread "main" java.lang.ClassCastException: Cannot cast java.lang.Integer to [Ljava.lang.String;
         * 	at java.base/java.lang.Class.cast(Class.java:3816)
         * 	at Main.main(Main.java:11)
         */
    }
}

再谈instanceof

正常情况下,我们使用instanceof进行类型比较

public class Main {
    public static void main(String[] args) {
        String str = "";
        System.out.println(str instanceof String);      // true
    }
}

它可以判断一个对象是否为此接口或是类的实现或是子类,而现在我们有了更多的方式去判断类型

public class Main {
    public static void main(String[] args) {
        String str = "";
        System.out.println(str.getClass() == String.class);     //直接判断是否为这个类型   // true
    }
}

如果需要判断是否为子类或是接口/抽象类的实现,我们可以使用asSubClass()方法:

public class Main {
    public static void main(String[] args) {
        Integer i = 10;
        i.getClass().asSubclass(Number.class);  //当Integer不是Number的子类时,会产生异常
    }
}

获取父类信息

通过getSuperclass()方法,我们可以获取到父类的Class对象

public class Main {
    public static void main(String[] args) {
        Integer i = 10;
        System.out.println(i.getClass().getSuperclass());   // class java.lang.Number
    }
}

也可以通过getGenericSuperclass()获取父类的原始类型的Type

public class Main {
    public static void main(String[] args) {
        Integer i = 10;
        Type type = i.getClass().getGenericSuperclass();
        System.out.println(type);       // class java.lang.Number
        System.out.println(type instanceof Class);  // true
    }
}

我们发现Type实际上是Class类的父接口,但是获取到的Type的实现并不一定是Class。

同理,我们也可以像上面这样获取父接口:

public class Main {
    public static void main(String[] args) {
        Integer i = 10;
        for (Class<?> anInterface : i.getClass().getInterfaces()) {
            System.out.println(anInterface.getName());
        }
        for (Type genericInterface : i.getClass().getGenericInterfaces()) {
            System.out.println(genericInterface.getTypeName());
        }
    }
}

创建类对象

既然我们拿到了类的定义,那么是否可以通过Class对象来创建对象、调用方法、修改变量呢?当然是可以的,那我们首先来探讨一下如何创建一个类的对象:

public class Main {
    public static void main(String[] args) throws InstantiationException, IllegalAccessException {
        Class<Student> clazz = Student.class;
        Student student = clazz.newInstance();
        student.test();
    }

    static class Student {
        public void test() {
            System.out.println("lbwnb");
        }
    }
}

通过使用newInstance()方法来创建对应类型的实例,返回泛型T,注意它会抛出InstantiationException和IllegalAccessException异常,那么什么情况下会出现异常呢?

public class Main {
    public static void main(String[] args) throws InstantiationException, IllegalAccessException {
        Class<Student> clazz = Student.class;
        Student student = clazz.newInstance();      // 报错
        student.test();
    }

    static class Student {
        public Student (String text) {
            
        }
        public void test() {
            System.out.println("lbwnb");
        }
    }
}

当类默认的构造方法被带参构造覆盖时,会出现InstantiationException异常,因为newInstance()只适用于默认无参构造

public class Main {
    public static void main(String[] args) throws InstantiationException, IllegalAccessException {
        Class<Student> clazz = Student.class;
        Student student = clazz.newInstance();
        student.test();
    }

    static class Student {
        private Student(){}
        public void test() {
            System.out.println("lbwnb");
        }
    }
}

当默认无参构造的权限不是public时,会出现IllegalAccessException异常,表示我们无权去调用默认构造方法。在JDK9之后,不再推荐使用newInstance()方法了,而是使用我们接下来要介绍到的,通过获取构造器,来实例化对象

public class Main {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        Class<Student> clazz = Student.class;
        Student student = clazz.getConstructor(String.class).newInstance("what's up");
        student.test();
    }

    static class Student {
        public Student(String str){}

        public void test() {
            System.out.println("lbwnb");
        }
    }
}

通过获取类的构造方法(构造器)来创建对象实例,会更加合理,我们可以使用getConstructor()方法来获取类的构造方法,同时我们需要向其中填入参数,也就是构造方法需要的类型,当然我们这里只演示了。那么,当访问权限不是public的时候呢?

public class Main {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        Class<Student> clazz = Student.class;
        Student student = clazz.getConstructor(String.class).newInstance("what's up");
        student.test();
    }

    static class Student {
        private Student(String str){}

        public void test() {
            System.out.println("lbwnb");
        }
    }
}

我们发现,当访问权限不足时,会无法找到此构造方法,那么如何找到非public的构造方法呢?

public class Main {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        Class<Student> clazz = Student.class;
        Constructor<Student> constructor = clazz.getDeclaredConstructor(String.class);
        constructor.setAccessible(true);	//修改访问权限
        Student student = constructor.newInstance("what's up");
        student.test();
    }

    static class Student {
        private Student(String str){}

        public void test() {
            System.out.println("lbwnb");
        }
    }
}

使用getDeclaredConstructor()方法可以找到类中的非public构造方法,但是在使用之前,我们需要先修改访问权限,在修改访问权限之后,就可以使用非public方法了(这意味着,反射可以无视权限修饰符访问类的内容

调用类的方法

我们可以通过反射来调用类的方法(本质上还是类的实例进行调用)只是利用反射机制实现了方法的调用,我们在包下创建一个新的类:

package com.test;

public class Student {
    public void test(String str) {
        System.out.println("萨日朗" + str);
    }
}

这次我们通过forName(String)来找到这个类并创建一个新的对象:

public class Main {
    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
        Class<?> clazz = Class.forName("com.test.Student");
        Object instance = clazz.newInstance();      //创建出学生对象
        Method method = clazz.getMethod("test", String.class);  //通过方法名和形参类型获取类中的方法

        method.invoke(instance, "what's up");   //通过Method对象的invoke方法来调用方法
    }
}

通过调用getMethod()方法,我们可以获取到类中所有声明为public的方法,得到一个Method对象,我们可以通过Method对象的invoke()方法(返回值就是方法的返回值,因为这里是void,返回值为null)来调用已经获取到的方法,注意传参

我们发现,利用反射之后,在一个对象从构造到方法调用,没有任何一处需要引用到对象的实际类型,我们也没有导入Student类,整个过程都是反射在代替进行操作,使得整个过程被模糊了,过多的使用反射,会极大地降低后期维护性。

同构造方法一样,当出现非public方法时,我们可以通过反射来无视权限修饰符,获取非public方法并调用,现在我们将test()方法的权限修饰符改为private:

package com.test;

public class Student {
    private void test(String str) {
        System.out.println("萨日朗" + str);
    }
}
public class Main {
    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
        Class<?> clazz = Class.forName("com.test.Student");
        Object instance = clazz.newInstance();      //创建出学生对象
        Method method = clazz.getDeclaredMethod("test", String.class);  //通过方法名和形参类型获取类中的方法
        method.setAccessible(true);

        method.invoke(instance, "what's up");   //通过Method对象的invoke方法来调用方法
    }
}

MethodConstructor都和Class一样,他们存储了方法的信息,包括方法的形式参数列表,返回值,方法的名称等内容,我们可以直接通过Method对象来获取这些信息:

public class Main {
    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
        Class<?> clazz = Class.forName("com.test.Student");
        Method method = clazz.getDeclaredMethod("test", String.class);  //通过方法名和形参类型获取类中的方法

        System.out.println(method.getName());   //获取方法名称   // test
        System.out.println(method.getReturnType()); //获取返回值类型   // void
        System.out.println(method.getParameterTypes()); // 返回值是"Class<?>[]"     // [Ljava.lang.Class;@2133c8f8
        System.out.println(Arrays.toString(method.getParameterTypes()));    // [class java.lang.String]
    }
}

当方法的参数为可变参数时,我们该如何获取方法呢?实际上,我们在之前就已经提到过,可变参数实际上就是一个数组,因此我们可以直接使用数组的class对象表示

package com.test;

public class Student {
    private void test(String... str) {
        System.out.println("萨日朗" + str);
    }
}
public class Main {
    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
        Class<?> clazz = Class.forName("com.test.Student");
        Method method = clazz.getDeclaredMethod("test", String[].class);  //通过方法名和形参类型获取类中的方法

        System.out.println(method.getName());   //获取方法名称   // test
        System.out.println(method.getReturnType()); //获取返回值类型   // void
        System.out.println(method.getParameterTypes()); // 返回值是"Class<?>[]"     // [Ljava.lang.Class;@2133c8f8
        System.out.println(Arrays.toString(method.getParameterTypes()));    // [class [Ljava.lang.String;]
    }
}

反射非常强大,尤其是我们提到的越权访问,但是请一定谨慎使用,别人将某个方法设置为private一定有他的理由,如果实在是需要使用别人定义为private的方法,就必须确保这样做是安全的,在没有了解别人代码的整个过程就强行越权访问,可能会出现无法预知的错误。

修改类的属性

我们还可以通过反射访问一个类中定义的成员字段也可以修改一个类的对象中的成员字段值,通过getField()方法来获取一个类定义的指定字段

package com.test;

public class Student {
    public int i;

    public void test() {
        System.out.println("萨日朗");
    }
}

public class Main {
    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
        Class<?> clazz = Class.forName("com.test.Student");
        Object instance = clazz.newInstance();

        Field field = clazz.getField("i");      //获取类的成员字段i
        field.set(instance, 100);       //将类实例instance的成员字段i设置为100

        Method method = clazz.getMethod("test");
        method.invoke(instance);
    }
}

在得到Field之后,我们就可以直接通过set()方法为某个对象,设定此属性的值,比如上面,我们就为instance对象设定值为100,当访问private字段时,同样可以按照上面的操作进行越权访问:

public class Student {
    private int i;

    public void test() {
        System.out.println("萨日朗");
    }
}

public class Main {
    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
        Class<?> clazz = Class.forName("com.test.Student");
        Object instance = clazz.newInstance();

        Field field = clazz.getDeclaredField("i");      //获取类的成员字段i
        field.setAccessible(true);
        field.set(instance, 100);       //将类实例instance的成员字段i设置为100

        Method method = clazz.getMethod("test");
        method.invoke(instance);
    }
}

现在我们已经知道,反射几乎可以把一个类的老底都给扒出来,任何属性,任何内容,都可以被反射修改,无论权限修饰符是什么,那么,如果我的字段被标记为final呢?现在在字段i前面添加final关键字,我们再来看看效果:

private final int i = 10;

这时,当字段为final时,就修改失败了!当然,通过反射可以直接将final修饰符直接去除,去除后,就可以随意修改内容了,我们来尝试修改Integer的value值:

我们可以发现,反射非常暴力,就连被定义为final字段的值都能强行修改,几乎能够无视一切阻拦。我们来试试看修改一些其他的类型:

实际上,整个ArrayList体系由于我们的反射操作,导致被破坏,因此它已经无法正常工作了!

再次强调,在进行反射操作时,必须注意是否安全,虽然拥有了创世主的能力,但是我们不能滥用,我们只能把它当做一个不得已才去使用的工具!

自定义ClassLoader加载类

我们可以自己手动将class文件加载到JVM中吗?先写好我们定义的类:

package com.test;

public class Test {
    public String text;

    public void test(String str) {
        System.out.println(text + " > 我是测试方法!" + str);
    }
}

通过javac命令,手动编译一个.class文件:
在这里插入图片描述
在这里插入图片描述
编译后,得到一个class文件,我们把它放到根目录下,然后编写一个我们自己的ClassLoader,因为普通的ClassLoader无法加载二进制文件,因此我们编写一个自己的来让它支持:
在这里插入图片描述
现在,我们就将此class文件读取并解析为Class了,现在我们就可以对此类进行操作了(注意,我们无法在代码中直接使用此类型,因为它是我们直接加载的),我们来试试看创建一个此类的对象并调用其方法:

我们来试试看修改成员字段之后,再来调用此方法:

通过这种方式,我们就可以实现外部加载甚至是网络加载一个类,只需要把类文件传递即可,这样就无需再将代码写在本地,而是动态进行传递,不仅可以一定程度上防止源代码被反编译(只是一定程度上,想破解你代码有的是方法),而且在更多情况下,我们还可以对byte[]进行加密,保证在传输过程中的安全性。

注解

其实我们在之前就接触到注解了,比如@Override表示重写父类方法(当然不加效果也是一样的,此注解在编译时会被自动丢弃)注解本质上也是一个类,只不过它的用法比较特殊。

注解可以被标注在任意地方,包括方法上、类名上、参数上、成员属性上、注解定义上等,就像注释一样,它相当于我们对某样东西的一个标记。而与注释不同的是,注解可以通过反射在运行时获取,注解也可以选择是否保留到运行时

预设注解

JDK预设了以下注解,作用于代码:

  • @Override - 检查(仅仅是检查,不保留到运行时)该方法是否是重写方法。如果发现其父类,或者是引用的接口中并没有该方法时,会报编译错误。
  • @Deprecated - 标记过时方法。如果使用该方法,会报编译警告。
  • @SuppressWarnings - 指示编译器去忽略注解中声明的警告(仅仅编译器阶段,不保留到运行时)
  • @FunctionalInterface - Java 8 开始支持,标识一个匿名函数或函数式接口
  • @SafeVarargs - Java 7 开始支持,忽略任何使用参数为泛型变量的方法或构造函数调用产生的

元注解

元注解是作用于注解上的注解,用于我们编写自定义的注解:

  • @Retention - 标识这个注解怎么保存,是只在代码中,还是编入class文件中,或者是在运行时可以通过反射访问。
  • @Documented - 标记这些注解是否包含在用户文档中。
  • @Target - 标记这个注解应该是哪种 Java 成员。
  • @Inherited - 标记这个注解是继承于哪个注解类(默认 注解并没有继承于任何子类)
  • @Repeatable - Java 8 开始支持,标识某注解可以在同一个声明上使用多次。

看了这么多预设的注解,你们肯定眼花缭乱了,那我们来看看@Override是如何定义的:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {}

该注解由@Target限定为只能作用于方法上ElementType是一个枚举类型,用于表示此枚举的作用域,一个注解可以有很多个作用域。@Retention表示此注解的保留策略,包括三种策略,在上述中有写到,而这里定义为只在代码中。一般情况下,自定义的注解需要定义1个@Retention和1-n个@Target。

既然了解了元注解的使用和注解的定义方式,我们就来尝试定义一个自己的注解:
在这里插入图片描述

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {
}

这里我们定义一个Test注解,并将其保留到运行时,同时此注解可以作用于方法或是类上:

@Test
public class Main {
    @Test
    public static void main(String[] args) {

    }
}

这样,一个最简单的注解就被我们创建了。

注解的使用

我们还可以在注解中定义一些属性,注解的属性也叫做成员变量,注解只有成员变量,没有方法。注解的成员变量在注解的定义中以“无形参的方法”形式来声明,其方法名定义了该成员变量的名字,其返回值定义了该成员变量的类型:

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {
    String value();
}

默认只有一个属性时,我们可以将其名字设定为value,否则,我们需要在使用时手动指定注解的属性名称,使用value则无需填入:

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {
    String test();
}

@Test(test = "")
public static void main(String[] args) {

}

我们也可以使用default关键字来为这些属性指定默认值:

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {
    String value() default "lbwnb";
    String test();
}

@Test(test = "")

当属性存在默认值时,使用注解的时候可以不用传入属性值。当属性为数组时呢?

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {
    String[] value();
}

当属性为数组,我们在使用注解传参时,如果数组里面只有一个内容,我们可以直接传入一个值,而不是创建一个数组:

@Test("lbwnb")
@Test({"value1", "value2"}) // 多个值就用花括号括起来

反射获取注解

既然我们的注解可以保留到运行时,那么我们来看看,如何获取我们编写的注解,我们需要用到反射机制:

通过反射机制,我们可以快速获取到我们标记的注解,同时还能获取到注解中填入的值,那么我们来看看,方法上的标记是不是也可以通过这种方式获取注解:

无论是方法、类、还是字段,都可以使用getAnnotations()方法(还有几个同名的)来快速获取我们标记的注解。

所以说呢,这玩意学来有啥用?丝毫get不到这玩意的用处。其实不是,现阶段你们还体会不到注解带来的快乐,在接触到Spring和SpringBoot等大型框架后,就能感受到注解带来的魅力了。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值