简单聊一聊项目中用反射来做过啥【Java基础题】

1.什么是反射机制

反射允许(在运行时动态地)对封装类的字段、方法、构造函数的信息进行编程访问

在我们的代码中,使用构造器直接生成对象、直接访问对象、对象的成员等方式,是清晰直观的。但在有些场景中,需要在运行时动态地操作这些成员,比如在运行时根据数据库中提供的类名或方法名,或者基于字符串变量,来动态实例化对象或调用方法时,直接访问的方式就不再适用了。反射机制恰好提供了解决这类需求的能力,其关键在于一个特殊的对象,我们称之为类对象--->Class Object

A.Class对象:

要学习反射,首先要对Class对象有一个清晰的认识,Class对象是什么?

Class是个类(java.lang.Class),用来描述字节码文件的。Class对象是由Java虚拟机JVM在加载类时,自动创建的,用于存储类的信息。

Class对象用于存储关于类的元数据,包括但不限于以下信息:

1. 类的名称:包括简单名称和完全限定名(包含包名)。
2. 访问修饰符:类的访问级别(public, private, protected)。
3. 包信息:类所属的包。
4. 父类信息:该类的直接父类(如果有)。
5. 接口信息:该类实现的所有接口。
6. 构造方法:类的所有构造器信息,包括它们的参数类型。
7. 字段信息:类中声明的所有字段,包括公有、保护、默认(包)访问和私有字段。
8. 方法信息:类中声明的所有方法,包括公有、保护、默认(包)访问和私有方法。
9. 注解信息:类、字段、方法上的注解信息。
10. 类型参数:如果类是泛型的,包含泛型的类型参数信息。
11. 静态成员:包括静态方法和静态字段。
12. 枚举常量:如果类是一个枚举类型,包含所有枚举常量的信息。
13. 是否为接口、数组、枚举或注解类型:`Class`对象提供方法来检查被表示的类或接口是否为特定的类型(如接口、数组、枚举或注解)。
14. 类加载器信息:加载该类的类加载器的信息。

通过Class对象,我们就能访问类的结构,以及对类本身和它的实例进行操作。虚拟机创建Class对象的过程是这样的:当我们编写一个类并完成编译之后,编译器会将其转化成字节码,存储在.class后缀的文件中,接下来在类的加载过程中,虚拟机利用Class Loader读取这个.class文件,将其中的字节码加载到内存方法区(Metaspace)中,并基于这些信息创建相应的Class对象。由于每个类在JVM中只加载一次(在这个类的生命周期内),所以每个类都对应着一个唯一的Class对象。

虽然每个类只加载一次,但这并不意味着它们永远不会被垃圾回收。一个类可以被卸载,但这种情况相对罕见,发生在类的ClassLoader被垃圾回收时。这通常意味着该类的所有实例都已经被垃圾回收,并且加载该类的类加载器也没有其他引用。如果一个类被卸载(其类加载器被垃圾回收),然后你的应用程序再次尝试使用该类,JVM将需要重新加载该类。这是因为从JVM的角度来看,先前加载的类版本已经不再存在。类的重新加载通常是通过新的类加载器实例进行的,这意味着即使是相同的类文件,也被视为一个全新的类。

要使用反射要先获取class对象,获取class对象的三种方式:

  1. Class.forName("全类名"):

    使用CLass 的forName静态方法,这种方法用于在运行时动态加载指定的类,并返回该类的Class对象实例,通常用于类名在编译时不可知的场景中,通过这种方法会触发类的初始化。

     public static Class<?> toClassConfident(String name) {
            try {
                return Class.forName(name, false, getDefaultClassLoader());
            } catch (ClassNotFoundException e) {
                try {
                    return Class.forName(name);
                } catch (ClassNotFoundException ex) {
                    throw ExceptionUtils.mpe("找不到指定的class!请仅在明确确定会有 class 的时候,调用该方法", e);
                }
            }
        }

  2. 使用类字面常量,也就是 类的名称+.class。这也是获取CLass对象最直接的方式,它在编译时就确定了具体的类,属于静态引用。使用这种方式获得类的Class对象时,不会立即触发类的初始化(类中的静态初始化块内的代码不会被执行),只有在你访问类的静态成员或者创建该类的实例的时候,才会被触发。
  3. 对象.getClass():如果你已经有了某个类的实例对象,可以调用该对象的getClass方法来获取它的Class对象。我们在编译阶段无法准确地判断Class对象的确切类型。

B.Class类中常用的API:

在`java.lang.Class`类中,有多个方法可以用来获取类的构造器、成员方法和成员变量。这些方法允许你在运行时反射地访问类的这些成员。下面是这些方法的概览:

### 获取构造器 可以获取构造函数的修饰符--->public和private等有对应的常量字段值

- `getConstructor(Class<?>... parameterTypes)`:返回一个`Constructor`对象,它反映了此`Class`对象所表示的类的指定公共构造器。
- `getConstructors()`:返回包含某些`Constructor`对象的数组,这些对象反映了此`Class`对象表示的类的所有公共构造器。
- `getDeclaredConstructor(Class<?>... parameterTypes)`:返回一个`Constructor`对象,它反映此`Class`对象表示的类或接口的指定构造器。
- `getDeclaredConstructors()`:返回包含某些`Constructor`对象的数组,这些对象反映了此`Class`对象表示的类声明的所有构造器。

### 获取成员方法

- `getMethod(String name, Class<?>... parameterTypes)`:返回一个`Method`对象,它反映此`Class`对象所表示的类或接口的指定公共成员方法。
- `getMethods()`:返回一个包含`Method`对象的数组,这些对象反映了此`Class`对象所表示的类或接口的所有公共方法,包括由类或接口声明的那些方法以及从超类和超接口继承的方法。
- `getDeclaredMethod(String name, Class<?>... parameterTypes)`:返回一个`Method`对象,它反映此`Class`对象表示的类或接口的指定声明的方法。
- `getDeclaredMethods()`:返回包含`Method`对象的数组,这些对象反映了此`Class`对象表示的类或接口声明的所有方法。

### 获取成员变量

- `getField(String name)`:返回一个`Field`对象,它反映此`Class`对象所表示的类或接口的指定公共成员变量。
- `getFields()`:返回一个包含`Field`对象的数组,这些对象反映了此`Class`对象所表示的类或接口的所有可访问的公共字段。
- `getDeclaredField(String name)`:返回一个`Field`对象,它反映此`Class`对象表示的类或接口的指定声明的字段。
- `getDeclaredFields()`:返回包含`Field`对象的数组,这些对象反映此`Class`对象表示的类或接口声明的所有字段。

这些反射API方法使得在运行时动态地访问和操作类的构造器、方法和字段成为可能。使用这些方法时,需要处理`NoSuchMethodException`、`NoSuchFieldException`等异常,这些异常在请求的成员不存在时抛出。此外,访问私有成员(如私有构造器、方法或字段)时,还需要通过`setAccessible(true)`方法来取消Java语言访问检查使用反射无法在编译时对错误进行检测,要特别注意其中的错误处理机制

2.框架中使用反射的案例

注解和反射是所有框架的底层:

JAVA EE中用到反射的地方:

以下是一些典型的使用反射的Java EE功能:

1. 依赖注入(DI)

依赖注入是Java EE平台的核心特性之一,允许运行时动态地向组件提供其依赖。这通常是通过反射来实现的,框架利用反射来检查组件的注解(如`@Inject`),然后动态地将实例注入到组件的属性或方法中。

 2. 数据持久化(JPA)

Java 持久化 API(JPA)利用注解(如`@Entity`, `@Column`等)来映射Java对象到数据库表。框架在运行时使用反射来读取这些注解,从而了解如何将对象的属性映射到对应的数据库列。此外,反射还被用于动态创建查询结果的实例。

3. Bean验证(Bean Validation)

Java EE提供了一套基于注解的约束声明和验证机制,允许开发者在实体类上声明约束(如`@NotNull`, `@Size`等),并在运行时自动验证这些约束。这个过程涉及到使用反射读取这些注解并根据它们来验证对象的状态。

 4. Web服务(JAX-RS和JAX-WS)

Java EE支持创建RESTful(通过JAX-RS)和SOAP(通过JAX-WS)Web服务。这些框架使用反射来处理使用特定注解(如`@Path`, `@GET`, `@WebService`等)标记的类和方法,以动态地将HTTP请求映射到相应的服务方法上。

 5. 拦截器(Interceptors)

拦截器允许在业务方法执行前后自动执行代码,这是通过标记方法或类与特定的注解(如`@Interceptor`)来实现的。Java EE容器在运行时使用反射来识别和处理这些拦截器。

 6. 事件和监听器

Java EE支持基于事件的编程模型,允许组件监听和响应应用程序中发生的事件。这包括使用注解(如`@Observes`)标记监听方法,容器将使用反射来调用相应的方法响应事件。

 7. 管理和配置

Java EE平台还支持使用注解来配置和管理应用程序组件,如EJB的`@Stateless`和`@Singleton`。容器通过反射读取这些注解来管理组件的生命周期和依赖关系。

Spring中用到反射的地方:

1. 依赖注入(DI)

Spring框架的核心功能之一是依赖注入。Spring容器通过反射来创建bean的实例,并注入所需的依赖。具体来说,Spring使用反射来:

- 调用构造函数创建对象实例。
- 通过setter方法或直接访问字段来注入依赖。
- 调用初始化方法和销毁方法。

2. 面向切面编程(AOP)

Spring AOP使用代理模式来实现横切关注点的模块化,这通常涉及到反射。例如,当使用JDK动态代理时,Spring通过反射调用目标对象的方法。对于基于CGLIB的代理,虽然主要通过字节码增强实现,但在创建代理对象和调用方法时仍可能用到反射。

3. 事件处理

Spring的事件发布/订阅模型允许bean监听和响应应用程序事件。Spring使用反射来调用事件监听器上的处理方法。

4. 数据绑定

Spring MVC等模块通过反射支持将请求参数绑定到控制器方法的参数上,以及将对象属性绑定到表单字段等。这包括使用反射来动态调用getter和setter方法,以及直接访问对象的字段。

5. Bean属性的访问和操作

Spring使用`BeanWrapper`接口来操作bean的属性,这背后也是基于反射实现的。这使得Spring能够动态地读取和修改bean的属性值,无论是通过直接字段访问还是通过访问器方法。

6. 注解处理

Spring大量使用注解来提供配置信息。Spring容器在启动时使用反射来读取类、方法、字段上的注解,以解析这些元数据并据此执行相应的逻辑,如标注`@Autowired`进行自动装配,`@Transactional`管理事务等。

7. 方法执行

Spring表达式语言(SpEL)、事务管理、JDBC模板等多个地方使用反射来动态执行方法。例如,SpEL可以评估对对象属性和方法的调用,这需要反射来实现。

总结

反射是Spring框架实现其灵活、动态特性的基石之一。它允许Spring在运行时动态地创建对象、注入依赖、处理注解、执行方法等,大大增强了Spring的功能和灵活性。不过,反射的使用也可能带来性能上的考量,因此Spring在内部实现中做了大量优化,以确保其性能。

其他运用到反射的:idea的代码提示、EasyExcel框架等框架

Java中有许多库和框架在其内部实现中广泛使用反射机制来提供灵活、动态的功能。这些库利用反射实现了从简单的对象操作到复杂的框架功能,如动态代理、依赖注入、序列化和反序列化等。以下是一些常见的Java库和框架,它们在不同程度上使用了反射:

1. Spring Framework

Spring框架在其核心功能如依赖注入(DI)、面向切面编程(AOP)、数据访问等方面广泛使用反射。例如,Spring通过反射来动态创建bean、注入依赖、以及管理bean的生命周期。

2. Hibernate / JPA

Hibernate是一个对象关系映射(ORM)框架,它允许开发者通过Java对象来操作数据库。Hibernate使用反射来动态地映射实体类到数据库表,以及执行查询结果到对象实例的转换。

3. MyBatis

MyBatis是另一个流行的持久层框架,它提供了对象和SQL之间的映射。MyBatis使用反射来填充对象的属性值,以及在执行动态SQL时处理参数。

4. Gson / Jackson

Gson和Jackson是两个广泛使用的JSON处理库,它们支持对象的序列化和反序列化。这些库使用反射来动态地访问对象的属性,实现对象与JSON之间的转换。

5. JUnit

JUnit是一个流行的单元测试框架,它使用反射来动态地调用测试方法。JUnit利用反射来查找并执行标记有`@Test`注解的方法,以及处理`@Before`和`@After`等生命周期相关的注解。

6. Mockito

Mockito是一个用于模拟对象行为的库,广泛用于单元测试中。它使用反射来动态创建模拟对象(mocks)和存根对象(stubs),以及拦截对这些对象方法的调用。

7. Java Reflection API

尽管不是一个“第三方库”,但值得一提的是Java自身的反射API(`java.lang.reflect`包),它提供了检查和修改运行时Java应用程序行为的能力。许多上述提到的框架和库底层都依赖于这个API。

总结

反射是Java编程中一个强大的工具,允许运行时的动态类型查询和操作。许多流行的Java库和框架都利用了反射来提供灵活、动态的功能,使得开发者能够以更简洁、高效的方式构建应用程序。然而,反射也应谨慎使用,因为它可能导致性能下降和安全性问题。

3.项目中使用到反射的案例

  • getTaoBaoFields方法
    public static String getTaoBaoFields(String path) throws Exception {
            StringBuilder stringBuilder = new StringBuilder();
            Class clazz = Class.forName(path);
            for (Field field : clazz.getDeclaredFields()) {
                if (field.getDeclaredAnnotations().length == 0) {
                    continue;
                }
                ApiField annotation = field.getAnnotation(ApiField.class);
                stringBuilder.append(annotation.value()).append(",");
    
                ApiListField annotationList = field.getAnnotation(ApiListField.class);
                if (annotationList != null) {
                    stringBuilder.append(annotationList.value()).append(",");
                }
            }
            return stringBuilder.deleteCharAt(stringBuilder.length() - 1).toString();
        }
    
    #在调用淘宝API的时候要用到 订单相关接口
    TradeFullinfoGetRequest request = new TradeFullinfoGetRequest();
    request.setFields(BusinessUtil.getTaoBaoFields(Trade.class.getName()));
    
    #退款相关接口
     RefundMessagesGetRequest req = new RefundMessagesGetRequest();
                req.setRefundId(Long.valueOf(refundId));
                req.setPageNo(1L);
                req.setPageSize(100L);
                req.setFields(BusinessUtil.getTaoBaoFields("com.taobao.api.domain.RefundMessage"));
    
    #业务逻辑:调用淘宝不同的业务的API需要传入不同的字段,通过工具类传入类名拼接不同的字段

    这个`getTaoBaoFields`方法设计用于提取和拼接指定类中被`ApiField`或`ApiListField`注解标记的字段名称。这种方法通常用于与阿里巴巴淘宝API交互的场景,其中`ApiField`和`ApiListField`可能是自定义注解,用于标记字段对应的API参数或字段名称。方法的工作流程如下:

    ### 输入参数

    - `String path`:这是一个字符串参数,代表一个完全限定的类名(即包括包名的类名)。方法将使用这个参数来加载对应的类。

    ### 过程

    1. **加载类**:通过`Class.forName(path)`动态加载传入路径指定的类。这一步会抛出`ClassNotFoundException`如果给定的类路径无法找到,这是由`throws Exception`在方法签名中声明的一部分来捕获的。

    2. **遍历字段**:使用`clazz.getDeclaredFields()`获取这个类声明的所有字段(包括私有字段,但不包括继承的字段)。

    3. **检查注解**:对于每个字段,首先检查它是否有声明的注解。如果一个字段没有任何注解(`field.getDeclaredAnnotations().length == 0`),则跳过该字段。

    4. **处理`ApiField`注解**:如果字段被`ApiField`注解标记,使用`field.getAnnotation(ApiField.class)`获取这个注解实例,并将注解的`value`值追加到`StringBuilder`对象中。`ApiField`注解的`value`属性可能表示与该字段对应的API参数名

    5. **处理`ApiListField`注解**:类似地,检查字段是否被`ApiListField`注解标记,并处理。`ApiListField`注解的`value`属性可能表示一组与该字段相关的API参数名

    6. **拼接字符串**:字段名称(通过注解指定的值)之间用逗号(`,`)分隔。

    ### 输出

    - 方法返回一个字符串,包含了类中所有被`ApiField`或`ApiListField`注解标记的字段名称(或指定的API参数名),字段名之间用逗号分隔。

    ### 异常处理

    - 方法签名中的`throws Exception`表示调用者需要处理可能发生的所有异常,包括反射操作中可能抛出的异常(如`ClassNotFoundException`)。

    ### 示例用途

    这个方法可能用于生成请求淘宝开放平台API时需要的字段列表字符串。通过反射和注解,可以动态地根据类的定义来构建这个列表,而不需要硬编码,这样做提高了代码的灵活性和可维护性。

  • 反射工具类
    @NoArgsConstructor(access = AccessLevel.PRIVATE)
    public final class ReflectionUtils {
    
        private static final Field[] NO_FIELDS = {};
    
        /**
         * Cache for {@link Class#getDeclaredFields()}, allowing for fast iteration.
         */
        private static final Map<Class<?>, Field[]> declaredFieldsCache = new ConcurrentReferenceHashMap<>(256);
    
        /**
         * This variant retrieves {@link Class#getDeclaredFields()} from a local cache
         * in order to avoid the JVM's SecurityManager check and defensive array copying.
         *
         * @param clazz the class to introspect
         * @return the cached array of fields
         * @see Class#getDeclaredFields()
         */
        public static Field[] getDeclaredFields(Class<?> clazz) {
            Assert.notNull(clazz, "Class must not be null");
            Field[] result = declaredFieldsCache.get(clazz);
            if (result == null) {
                result = clazz.getDeclaredFields();
                declaredFieldsCache.put(clazz, (result.length == 0 ? NO_FIELDS : result));
            }
            return result;
        }
    
        /**
         * Make the given field accessible, explicitly setting it accessible if
         * necessary. The {@code setAccessible(true)} method is only called
         * when actually necessary, to avoid unnecessary conflicts with a JVM
         * SecurityManager (if active).
         *
         * @param field the field to make accessible
         * @see java.lang.reflect.Field#setAccessible
         */
        public static void makeAccessible(Field field) {
            if ((!Modifier.isPublic(field.getModifiers()) ||
                    !Modifier.isPublic(field.getDeclaringClass().getModifiers()) ||
                    Modifier.isFinal(field.getModifiers())) && !field.isAccessible()) {
                field.setAccessible(true);
            }
        }
    
        /**
         * 把clazz所有字段(不包括内嵌对象的字段)用 "," 拼接起来
         */
        public static String getDeclaredFieldsStr(Class<?> clazz) {
            Field[] fields = getDeclaredFields(clazz);
            return Stream.of(fields)
                    .peek(ReflectionUtils::makeAccessible)
                    .map(Field::getName)
                    .collect(Collectors.joining(","));
        }
    
        /**
         * 把clazz所有字段(包括内嵌对象的字段)用 "," 拼接起来
         */
        public static String getAllDeclaredFieldsStr(Class<?> clazz) {
            Set<Field> allFields = getAllDeclaredFields(clazz);
            return allFields.stream()
                    .peek(ReflectionUtils::makeAccessible)
                    .map(Field::getName)
                    .collect(Collectors.joining(","));
        }
    
        /**
         * 获取clazz所有字段(包括类属性为对象的字段)
         */
        public static Set<Field> getAllDeclaredFields(Class<?> clazz) {
            Field[] fields = getDeclaredFields(clazz);
            Set<Field> allFields = Sets.newHashSet(Arrays.asList(fields));
            Stream.of(fields)
                    .filter(v -> !BASE_TYPE.test(v.getType()))
                    .forEach(v -> {
                        allFields.addAll(getBeanTypeFields(v));
                        allFields.addAll(getCollectionTypeFields(v));
                    });
            return allFields;
        }
    
        private static Set<Field> getBeanTypeFields(Field field) {
            Class<?> clazz = field.getType();
            if (BASE_TYPE.test(clazz) || COLLECTION_TYPE.test(clazz)) {
                return Collections.emptySet();
            }
            return getAllDeclaredFields(clazz);
        }
    
        private static Set<Field> getCollectionTypeFields(Field field) {
            Class<?> clazz = field.getType();
            if (!COLLECTION_TYPE.test(clazz)) {
                return Collections.emptySet();
            }
    
            Type genericType = field.getGenericType();
            // fixed java 11
    //        if (genericType instanceof ParameterizedTypeImpl) {
    //            ParameterizedTypeImpl parameterizedType = (ParameterizedTypeImpl) genericType;
    //            Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
    //            Type actualTypeArgument = actualTypeArguments[0];
    //            String typeName = actualTypeArgument.getTypeName();
    //            if (!StringUtils.hasText(typeName)) {
    //                return Collections.emptySet();
    //            }
    //
    //            try {
    //                return getAllDeclaredFields(Class.forName(typeName));
    //            } catch (ClassNotFoundException e) {
    //            }
    //        }
            return Collections.emptySet();
        }
    
        public static final SafePredicate<Class<?>> BASE_TYPE = input -> {
            if (input.equals(byte.class) || input.equals(Byte.class)) {
                return true;
            }
            if (input.equals(short.class) || input.equals(Short.class)) {
                return true;
            }
            if (input.equals(int.class) || input.equals(Integer.class)) {
                return true;
            }
            if (input.equals(long.class) || input.equals(Long.class)) {
                return true;
            }
            if (input.equals(float.class) || input.equals(Float.class)) {
                return true;
            }
            if (input.equals(double.class) || input.equals(Double.class)) {
                return true;
            }
            if (input.equals(boolean.class) || input.equals(Boolean.class)) {
                return true;
            }
            if (input.equals(char.class)) {
                return true;
            }
            if (input.equals(String.class)) {
                return true;
            }
            return false;
        };
    
        public static final SafePredicate<Class<?>> COLLECTION_TYPE = input -> {
            if (input.equals(List.class)) {
                return true;
            }
            if (input.equals(Set.class)) {
                return true;
            }
            return false;
        };
    
    }

    这个`ReflectionUtils`类是一个工具类,使用Java反射API为操作Java类的字段(`Field`)提供了便利方法。这个类被设计为final,并且带有一个私有的构造函数(通过`@NoArgsConstructor(access = AccessLevel.PRIVATE)`注解实现),这样做是为了防止创建它的实例,明确表明这是一个纯粹的工具类。下面是这个类提供的一些关键功能的解释:

    ### 缓存反射字段

    - **字段缓存**:使用`ConcurrentReferenceHashMap`来缓存类的声明字段,提高了反复访问同一类字段时的性能。这避免了频繁调用`Class.getDeclaredFields()`方法,后者可能会因为JVM的安全管理器检查和防御性数组复制而产生性能开销。

    ### 字段访问

    - **`getDeclaredFields(Class<?> clazz)`**:获取给定类的所有声明字段(不包括继承的字段),结果从本地缓存获取,以提高性能。
    - **`makeAccessible(Field field)`**:如果需要,将给定的字段设置为可访问的,即使它是私有的。这个方法在访问非公共字段时很有用,同时避免了不必要的`setAccessible(true)`调用,减少了与JVM安全管理器的冲突概率。
    - **`getDeclaredFieldsStr(Class<?> clazz)`和`getAllDeclaredFieldsStr(Class<?> clazz)`**:将类的所有声明字段(或包括嵌套对象字段)的名称用逗号分隔拼接成字符串。这可能用于生成日志、调试输出等。

    ### 支持复杂类型字段

    - **处理集合和自定义对象类型字段**:通过检查字段类型,`getAllDeclaredFields(Class<?> clazz)`方法能够递归地处理自定义对象类型的字段以及集合类型的字段。这允许深入地分析类的结构,获取所有相关字段,包括嵌套对象内的字段。

    ### 工具方法

    - **类型判断**:通过`BASE_TYPE`和`COLLECTION_TYPE`这两个`SafePredicate`(这里假定是某种自定义的或库中的断言接口),来判断一个类是否为基本类型或集合类型。这对于区分应如何处理不同类型的字段很有用,例如,避免尝试进一步分析基本类型或字符串类型的字段。

    整个`ReflectionUtils`类展示了如何利用Java反射API进行高级的反射操作,包括字段访问、权限修改和类型分析。这类工具类在需要深入处理Java对象内部结构的场景中非常有用,如框架开发、序列化/反序列化库、动态数据绑定等。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值