Java注解以及自定义注解

Java注解以及自定义注解

要深入学习注解,我们就必须能定义自己的注解,并使用注解,在定义自己的注解之前,我们就必须要了解Java为

我们提供的元注解和相关定义注解的语法。

1、注解

1.1 注解的官方定义

注解是一种元数据形式。即注解是属于java的一种数据类型,和类、接口、数组、枚举类似。

注解用来修饰,类、方法、变量、参数、包。

注解不会对所修饰的代码产生直接的影响。

1.2 注解的使用范围

注解有许多用法,其中有:为编译器提供信息 - 注解能被编译器检测到错误或抑制警告。编译时和部署时的处理 -

软件工具能处理注解信息从而生成代码,XML文件等等。运行时的处理 - 有些注解在运行时能被检测到。

2、元注解

一个最最基本的注解定义就只包括了两部分内容:1、注解的名字;2、注解包含的类型元素。但是,我们在使用

JDK自带注解的时候发现,有些注解只能写在方法上面(比如@Override);有些却可以写在类的上面(比如

@Deprecated)。当然除此以外还有很多细节性的定义,那么这些定义该如何做呢?接下来就该元注解出场了!

元注解:专门修饰注解的注解。它们都是为了更好的设计自定义注解的细节而专门设计的。Java5.0定义了4个标准

的meta-annotation类型,它们被用来提供对其它 annotation类型作说明。

Java5.0 定义的元注解:

1、@Target

2、@Retention

3、@Documented

4、@Inherited

这些类型和它们所支持的类在java.lang.annotation包中可以找到。下面我们看一下每个元注解的作用和相应

分参数的使用说明。

2.1 @Target

@Target注解,是专门用来限定某个自定义注解能够被应用在哪些Java元素上面的。

@Target说明了Annotation所修饰的对象范围:Annotation可被用于 packages、types(类、接口、枚举、

Annotation类型)、类型成员(方法、构造方法、成员变量、枚举值)、方法参数和本地变量(如循环变量、

catch参数)。在Annotation类型的声明中使用了target可更加明晰其修饰的目标。

作用:用于描述注解的使用范围(即被描述的注解可以用在什么地方)

取值(ElementType)有:

1、CONSTRUCTOR:用于描述构造器

2、FIELD:用于描述域

3、LOCAL_VARIABLE:用于描述局部变量

4、METHOD:用于描述方法

5、PACKAGE:用于描述包

6、PARAMETER:用于描述参数

7、TYPE:用于描述类、接口(包括注解类型) 或enum声明

它使用一个枚举类型定义如下:

public enum ElementType {
    /** 类,接口(包括注解类型)或枚举的声明 */
    TYPE,

	/** 属性的声明 */
	FIELD,

	/** 方法的声明 */
	METHOD,

	/** 方法形式参数声明 */
	PARAMETER,

	/** 构造方法的声明 */
	CONSTRUCTOR,

	/** 局部变量声明 */
	LOCAL_VARIABLE,

	/** 注解类型声明 */
	ANNOTATION_TYPE,

	/** 包的声明 */
	PACKAGE,
        
    TYPE_PARAMETER,
    
    TYPE_USE
}

使用实例:

package com.test3;

import java.lang.annotation.ElementType;
import java.lang.annotation.Target;

@Target(ElementType.TYPE)
public @interface Table {
    /**
     * 数据表名称注解,默认值为类名称
     * @return
     */
    public String tableName() default "className";
}
package com.test3;

import java.lang.annotation.ElementType;
import java.lang.annotation.Target;

@Target(ElementType.FIELD)
public @interface NoDBColumn {
}
package com.test3;

import java.lang.annotation.ElementType;
import java.lang.annotation.Target;

//@CherryAnnotation被限定只能使用在类、接口或方法上面
@Target(value = {ElementType.TYPE, ElementType.METHOD})
public @interface CherryAnnotation {
    String name();

    int age() default 18;

    int[] array();
}

注解Table可以用于注解类、接口(包括注解类型) 或enum声明,而注解NoDBColumn仅可用于注解类的成员变

量。

2.2 @Retention

@Retention注解,翻译为持久力、保持力。即用来修饰自定义注解的生命力。

注解的生命周期有三个阶段:1、Java源文件阶段;2、编译到class文件阶段;3、运行期阶段。同样使用了

RetentionPolicy 枚举类型定义了三个阶段:

作用:表示需要在什么级别保存该注释信息,用于描述注解的生命周期(即被描述的注解在什么范围内有效)

取值(RetentionPoicy)有:

1、SOURCE:在源文件中有效(即源文件保留)

2、CLASS:在class文件中有效(即class保留)

3、RUNTIME:在运行时有效(即运行时保留)

Retention meta-annotation类型有唯一的value作为成员,它的取值来自

java.lang.annotation.RetentionPolicy的枚举类型值。

public enum RetentionPolicy {
    
    // 注解将被编译器忽略掉
    SOURCE,
        
	// 注解将被编译器记录在class文件中,但在运行时不会被虚拟机保留,这是一个默认的行为
	CLASS,

	// 注解将被编译器记录在class文件中,而且在运行时会被虚拟机保留,因此它们能通过反射被读取到
	RUNTIME
}

我们再详解一下:

如果一个注解被定义为RetentionPolicy.SOURCE,则它将被限定在Java源文件中,那么这个注解即不会参与编

译也不会在运行期起任何作用,这个注解就和一个注释是一样的效果,只能被阅读Java文件的人看到;

如果一个注解被定义为RetentionPolicy.CLASS,则它将被编译到Class文件中,那么编译器可以在编译时根据

注解做一些处理动作,但是运行时JVM(Java虚拟机)会忽略它,我们在运行期也不能读取到;

如果一个注解被定义为RetentionPolicy.RUNTIME,那么这个注解可以在运行期的加载阶段被加载到Class对象

中。那么在程序运行阶段,我们可以通过反射得到这个注解,并通过判断是否有这个注解或这个注解中属性的值,

从而执行不同的程序代码段。我们实际开发中的自定义注解几乎都是使用的RetentionPolicy.RUNTIME;在默认

的情况下,自定义注解是使用的RetentionPolicy.CLASS

具体实例如下:

package com.test3;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Column {
    public String name() default "fieldName";

    public String setFuncName() default "setField";

    public String getFuncName() default "getField";

    public boolean defaultDBValue() default false;
}

Column注解的的RetentionPolicy的属性值是RUNTIME,这样注解处理器可以通过反射,获取到该注解的属性

值,从而去做一些运行时的逻辑处理。

2.3 @Documented

@Documented注解,是被用来指定自定义注解是否能随着被定义的java文件生成到JavaDoc文档当中,因此可以

被例如javadoc此类的工具文档化。

Documented是一个标记注解,没有成员。

package com.test3;

import java.lang.annotation.*;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Column1 {
    public String name() default "fieldName";

    public String setFuncName() default "setField";

    public String getFuncName() default "getField";

    public boolean defaultDBValue() default false;
}

2.4 @Inherited

@Inherited注解,是指定某个自定义注解如果写在了父类的声明部分,那么子类的声明部分也能自动拥有该注

解。@Inherited注解只对那些@Target被定义为ElementType.TYPE的自定义注解起作用。

@Inherited 元注解是一个标记注解,@Inherited阐述了某个被标注的类型是被继承的。如果一个使用了

@Inherited修饰的annotation类型被用于一个class,则这个annotation将被用于该class的子类。

注意:@Inherited annotation类型是被标注过的class的子类所继承。类并不从它所实现的接口继承annotation,

方法并不从它所重载的方法继承annotation。

当@Inherited annotation类型标注的annotation的Retention是RetentionPolicy.RUNTIME,则反射API增强了这

种继承性。如果我们使用java.lang.reflect去查询一个@Inherited annotation类型的annotation时,反射代码检查

将展开工作:检查class和其父类,直到发现指定的annotation类型被发现,或者到达类继承结构的顶层。

实例代码:

package com.test3;

import java.lang.annotation.Inherited;

@Inherited
public @interface Greeting {
    public enum FontColor {BULE, RED, GREEN};

    String name();

    FontColor fontColor() default FontColor.GREEN;
}

注解的继承依赖如下一个因素:

1、首先要想Annotation能被继承,需要在注解定义的时候加上@Inherited,并且如果要被反射应用的话,还

需要@Retention(RetentionPolicy.RUNTIME)标识。

2、JDK文档中说明的是:只有在类上应用Annotation才能被继承,而实际应用结果是:除了类上应用的

Annotation能被继承外,没有被重写的方法的Annotation也能被继承。

3、当方法被重写后,Annotation不会被继承。

4、Annotation的继承不能应用在接口上。

3、自定义注解

使用@interface自定义注解时,自动继承了java.lang.annotation.Annotation接口,由编译程序自动完成其他细

节。在定义注解时,不能继承其他的注解或接口。@interface用来声明一个注解,其中的每一个方法实际上是声

明了一个配置参数。方法的名称就是参数的名称,返回值类型就是参数的类型(返回值类型只能是基本类型、

Class、String、enum)。可以通过default来声明参数的默认值。

3.1 定义注解格式

public @interface 注解名 {定义体}
public @interface CherryAnnotation {
}

根据我们在自定义类的经验,在类的实现部分无非就是书写构造、属性或方法。但是,在自定义注解中,其实现

只能定义一个东西:注解类型元素(annotation type element)。语法:

public @interface CherryAnnotation {
	public String name();
	public int age();
	public int[] array();
}
public @interface CherryAnnotation {
	public String name();
	public int age() default 18;
	public int[] array();
}

3.2 注解参数的可支持数据类型

1、所有基本数据类型(int,float,boolean,byte,double,char,long,short)

2、String类型

3、Class类型

4、enum类型

5、Annotation类型

6、以上所有类型的数组

注解里面定义的是:注解类型元素!

3.3 定义注解类型元素时需要注意如下几点

1、只能用public或默认(default)这两个访问权修饰,例如String value();这里把方法设为default默认类型。

2、参数成员只能用基本类型byte,short,char,int,long,float,double,boolean八种基本数据类型和 String,

Enum,Class,annotations等数据类型,以及这一些类型的数组。

3、如果只有一个参数成员,最好把参数名称设为value,后加小括号。

4、()不是定义方法参数的地方,也不能在括号中定义任何参数,仅仅只是一个特殊的语法。

5、default代表默认值,值必须和第2点定义的类型一致。

6、如果没有默认值,代表后续使用注解时必须给该类型元素赋值。

可以看出,注解类型元素的语法非常奇怪,即又有属性的特征(可以赋值),又有方法的特征(打上了一对括

号)。但是这么设计是有道理的,我们在后面的章节中可以看到:注解在定义好了以后,使用的时候操作元素类型

像在操作属性,解析的时候操作元素类型像在操作方法。

3.4 简单的自定义注解和使用注解实例

package com.test;

import java.lang.annotation.*;

/**
 * 水果名称注解
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FruitName {
    String value() default "";
}
package com.test;

import java.lang.annotation.*;

/**
 * 水果颜色注解
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FruitColor {

    /**
     * 颜色枚举
     */
    public enum Color {
        BULE, RED, GREEN
    }

    ;

    /**
     * 颜色属性
     *
     * @return
     */
    Color fruitColor() default Color.GREEN;

}
package com.test;


public class Apple {

    @FruitName("Apple")
    private String appleName;

    @FruitColor(fruitColor = FruitColor.Color.RED)
    private String appleColor;

    public void setAppleColor(String appleColor) {
        this.appleColor = appleColor;
    }

    public String getAppleColor() {
        return appleColor;
    }

    public void setAppleName(String appleName) {
        this.appleName = appleName;
    }

    public String getAppleName() {
        return appleName;
    }
}

3.5 注解元素的默认值

注解元素必须有确定的值,要么在定义注解的默认值中指定,要么在使用注解时指定,非基本类型的注解元素的值

不可为null。因此,使用空字符串或0作为默认值是一种常用的做法。这个约束使得处理器很难表现一个元素的存

在或缺失的状态,因为每个注解的声明中,所有元素都存在,并且都具有相应的值,为了绕开这个约束,我们只能

定义一些特殊的值,例如空字符串或者负数,一次表示某个元素不存在,在定义注解时,这已经成为一个习惯用

法。

package com.test;


import java.lang.annotation.*;

/**
 * 水果供应者注解
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FruitProvider {

    /**
     * 供应商编号
     *
     * @return
     */
    public int id() default -1;

    /**
     * 供应商名称
     *
     * @return
     */
    public String name() default "";

    /**
     * 供应商地址
     *
     * @return
     */
    public String address() default "";
}

3.6 特殊语法

特殊语法一

如果注解本身没有注解类型元素,那么在使用注解的时候可以省略(),直接写为:@注解名,它和标准语法

@注解名()等效!

@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.TYPE})
@Documented
public @interface FirstAnnotation {
}
//等效于 @FirstAnnotation()
@FirstAnnotation
public class JavaBean{
}

特殊语法二

如果注解本身只有一个注解类型元素,而且命名为value,那么在使用注解的时候可以直接使用:

@注解名(注解值),其等效于:@注解名(value = 注解值)

@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.TYPE})
@Documented
public @interface SecondAnnotation {
	String value();
}
//等效于 @SecondAnnotation(value = "this is second annotation")
@SecondAnnotation("this is annotation")
public class JavaBean{
}

特殊用法三

如果注解中的某个注解类型元素是一个数组类型,在使用时又出现只需要填入一个值的情况,那么在使用注解时可

直接写为:@注解名(类型名 = 类型值),它和标准写法:@注解名(类型名 = {类型值})等效!

@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.TYPE})
@Documented
public @interface ThirdAnnotation {
	String[] name();
}
//等效于 @ThirdAnnotation(name = {"this is third annotation"})
@ ThirdAnnotation(name = "this is third annotation")
public class JavaBean{
}

特殊用法四

如果一个注解的@Target是定义为Element.PACKAGE,那么这个注解是配置在package-info.java中的,而不能

直接在某个类的package代码上面配置。

上面三节定义了注解,并在需要的时候给相关类,类属性加上注解信息,如果没有响应的注解信息处理流程,注解

可以说是没有实用价值。如何让注解真真的发挥作用,主要就在于注解处理方法,下一步我们将学习注解信息的获

取和处理!

4、自定义注解的配置使用

基于上一节,已对注解有了一个基本的认识:注解其实就是一种标记,可以在程序代码中的关键节点(类、方法、

变量、参数、包)上打上这些标记,然后程序在编译时或运行时可以检测到这些标记从而执行一些特殊操作。因此

可以得出自定义注解使用的基本流程:

第一步,定义注解——相当于定义标记;

第二步,配置注解——把标记打在需要用到的程序代码中;

第三步,解析注解——在编译期或运行时检测到标记,并进行特殊操作。

在这里插入图片描述

4.1 在具体的Java类上使用注解

首先,定义一个注解和一个供注解修饰的简单Java类。

package com.test1;

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.METHOD})
@Documented
public @interface CherryAnnotation {

    String name();// 类型元素

    int age() default 18;

    int[] score();
}
package com.test1;

public class Student {
    public void study(int times) {
        for (int i = 0; i < times; i++) {
            System.out.println("Good Good Study, Day Day Up!");
        }
    }
}

简单分析下:

CherryAnnotation的@Target定义为ElementType.METHOD,那么它书写的位置应该在方法定义的上方,即:

public void study(int times)之上。由于我们在CherryAnnotation中定义的有注解类型元素,而且有些元素是没有

默认值的,这要求我们在使用的时候必须在标记名后面打上(),并且在()内以“元素名=元素值“的形式挨个填上所有

没有默认值的注解类型元素(有默认值的也可以填上重新赋值),中间用“,”号分割。

所以最终书写形式如下:

package com.test1;

public class Student {
    @CherryAnnotation(name = "cherry-peng", age = 23, score = {99, 66, 77})
    public void study(int times) {
        for (int i = 0; i < times; i++) {
            System.out.println("Good Good Study, Day Day Up!");
        }
    }
}

4.2 自定义注解的运行时解析(反射操作获取注解)

这一节是使用注解的核心,读完此节即可明白,如何在程序运行时检测到注解,并进行一系列特殊操作!

只有当注解的保持力处于运行阶段,即使用@Retention(RetentionPolicy.RUNTIME)修饰注解时,才能在JVM

运行时,检测到注解,并进行一系列特殊操作。

在运行期探究和使用编译期的内容(编译期配置的注解),要用到Java中的灵魂技术——反射!

Java SE5扩展了反射机制的API,以帮助程序员快速的构造自定义注解处理器。

package com.test1;

import java.lang.reflect.Method;

/**
 * @author zhangshixing
 * @date 2021年11月01日 9:32
 */
public class TestAnnotation {
    public static void main(String[] args){
        try {
            //获取Student的Class对象
            Class stuClass = Class.forName("com.test1.Student");
            //说明一下,这里形参不能写成Integer.class,应写为int.class
            Method stuMethod = stuClass.getMethod("study",int.class);
            if(stuMethod.isAnnotationPresent(CherryAnnotation.class)){
                System.out.println("Student类上配置了CherryAnnotation注解!");
                //获取该元素上指定类型的注解
                CherryAnnotation cherryAnnotation = stuMethod.getAnnotation(CherryAnnotation.class);
                System.out.println("name: " + cherryAnnotation.name() + ", age: " + cherryAnnotation.age()
                        + ", score: " + cherryAnnotation.score()[0]);
            }else{
                System.out.println("Student类上没有配置CherryAnnotation注解!");
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
    }
}
# 程序输出
Student类上配置了CherryAnnotation注解!
name: cherry-peng, age: 23, score: 99

4.3 注解处理器类库(java.lang.reflect.AnnotatedElement)

Java使用Annotation接口来代表程序元素前面的注解,该接口是所有Annotation类型的父接口。除此之外,Java

java.lang.reflect 包下新增了AnnotatedElement接口,该接口代表程序中可以接受注解的程序元素,该接

口主要有如下几个实现类:

Class:类定义

Constructor:构造器定义

Field:类的成员变量定义

Method:类的方法定义

Package:类的包定义

java.lang.reflect包下主要包含一些实现反射功能的工具类,实际上,java.lang.reflect 包所有提供的反射API

扩充了读取运行时Annotation信息的能力。当一个Annotation类型被定义为运行时的Annotation后,该注解才能

是运行时可见,当class文件被装载时被保存在class文件中的Annotation才会被虚拟机读取。

AnnotatedElement 接口是所有程序元素(Class、Method和Constructor)的父接口,所以程序通过反射获取了

某个类的AnnotatedElement对象之后,程序就可以调用该对象的如下四个个方法来访问Annotation信息:

isAnnotationPresent(Class<? extends Annotation> annotationClass)方法是专门判断该元素上是否配

置有某个指定的注解;

getAnnotation(Class<A> annotationClass)方法是获取该元素上指定的注解。之后再调用该注解的注解类型

元素方法就可以获得配置时的值数据;如果该类型注解不存在,则返回null。

反射对象上还有一个方法getAnnotations(),该方法可以获得该对象身上配置的所有的注解。它会返回给我们

一个注解数组,需要注意的是该数组的类型是Annotation类型,这个Annotation是一个来自于

java.lang.annotation包的接口。

Annotation[] getDeclaredAnnotations():返回直接存在于此元素上的所有注解。与此接口中的其他方法不

同,该方法将忽略继承的注解。(如果没有注解直接存在于此元素上,则返回长度为零的一个数组)。该方法的调

用者可以随意修改返回的数组;这不会对其它调用者返回的数组产生任何影响。

如果我们要获得的注解是配置在方法上的,那么我们要从Method对象上获取;如果是配置在属性上,就需要从该

属性对应的Field对象上去获取,如果是配置在类型上,需要从Class对象上去获取。总之在谁身上,就从谁身上去

获取!

一个简单的注解处理器

/***********注解声明***************/

package com.test2;

import java.lang.annotation.*;

/**
 * 水果名称注解
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FruitName {
    String value() default "";
}
package com.test2;

import java.lang.annotation.*;

/**
 * 水果颜色注解
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FruitColor {
    /**
     * 颜色枚举
     *
     * @author peida
     */
    public enum Color {
        BULE, RED, GREEN
    }

    ;

    /**
     * 颜色属性
     *
     * @return
     */
    Color fruitColor() default Color.GREEN;

}
package com.test2;

import java.lang.annotation.*;

/**
 * 水果供应者注解
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FruitProvider {
    /**
     * 供应商编号
     *
     * @return
     */
    public int id() default -1;

    /**
     * 供应商名称
     *
     * @return
     */
    public String name() default "";

    /**
     * 供应商地址
     *
     * @return
     */
    public String address() default "";
}
/***********注解使用***************/

package com.test2;

public class Apple {

    @FruitName("Apple")
    private String appleName;

    @FruitColor(fruitColor = FruitColor.Color.RED)
    private String appleColor;

    @FruitProvider(id = 1, name = "陕西红富士集团", address = "陕西省西安市延安路89号红富士大厦")
    private String appleProvider;

    public void setAppleColor(String appleColor) {
        this.appleColor = appleColor;
    }

    public String getAppleColor() {
        return appleColor;
    }

    public void setAppleName(String appleName) {
        this.appleName = appleName;
    }

    public String getAppleName() {
        return appleName;
    }

    public void setAppleProvider(String appleProvider) {
        this.appleProvider = appleProvider;
    }

    public String getAppleProvider() {
        return appleProvider;
    }

    public void displayName() {
        System.out.println("水果的名字是:苹果");
    }
}
/***********注解处理器***************/

package com.test2;

import java.lang.reflect.Field;

public class FruitInfoUtil {
    public static void getFruitInfo(Class<?> clazz) {

        String strFruitName = " 水果名称:";
        String strFruitColor = " 水果颜色:";
        String strFruitProvicer = "供应商信息:";

        Field[] fields = clazz.getDeclaredFields();

        for (Field field : fields) {
            if (field.isAnnotationPresent(FruitName.class)) {
                FruitName fruitName = (FruitName) field.getAnnotation(FruitName.class);
                strFruitName = strFruitName + fruitName.value();
                System.out.println(strFruitName);
            } else if (field.isAnnotationPresent(FruitColor.class)) {
                FruitColor fruitColor = (FruitColor) field.getAnnotation(FruitColor.class);
                strFruitColor = strFruitColor + fruitColor.fruitColor().toString();
                System.out.println(strFruitColor);
            } else if (field.isAnnotationPresent(FruitProvider.class)) {
                FruitProvider fruitProvider = (FruitProvider) field.getAnnotation(FruitProvider.class);
                strFruitProvicer = " 供应商编号:" + fruitProvider.id() + " 供应商名称:" + fruitProvider.name() + " 供应商地址:" + fruitProvider.address();
                System.out.println(strFruitProvicer);
            }
        }
    }
}
/***********输出结果***************/

package com.test2;

public class FruitRun {

    /**
     * @param args
     */
    public static void main(String[] args) {

        FruitInfoUtil.getFruitInfo(Apple.class);

    }

}
 # 程序输出
 水果名称:Apple
 水果颜色:RED
 供应商编号:1 供应商名称:陕西红富士集团 供应商地址:陕西省西安市延安路89号红富士大厦
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值