手写一个简单的spring框架


前言

这一期是我自己手写一个简单spring的一个记录, 里面的内容不一定和源码逻辑相同, 只是为了自己能手动实现spring的部分功能, 其中包括: @Component, @ComponentScan, @Scope, @Autowired注解, BeanDefinition类, ApplicationContext类以及BeanPostProcessor类.


初始化springframework包和user包

springframework包为自定义模拟spring功能的包
user包为用户自己使用的包
在这里插入图片描述

首先我们为spring创建一个包名为spirngframework, 里面创建一个MyApplicationContext类并定义构造方法和getBean()方法

package com.yve.springframe;

/**
 * @author 伟大的Yve菌
 * 我们自己定义的一个spring的ApplicationContext类, 我们通过ApplicationContext可以获取到任意bean对象
 */
public class MyApplicationContext {
    private Class appConfig;

    public MyApplicationContext(Class appConfig) {
        this.appConfig = appConfig;
    }

    public Object getBean(String beanName) {
        return null;
    }
}


之后再为我们user类创建一个AppConfig配置类, 一个Test运行类和service包以及一个UserService类, 在Test类中创建我们定义的MyApplicationContext

/**
 * @author 伟大的Yve菌
 * 用户的service类, 通过spring获取到Bean对象后调用test()方法测试
 */
package com.yve.user.service;

public class UserService {
    public void test() {
        System.out.println("userService");
    }
}
package com.yve.user;

import com.yve.springframe.MyApplicationContext;
import com.yve.user.service.UserService;

/**
 * @author 伟大的Yve菌
 * 通过Test类启动主方法, 内部声明一个spring的ApplicationContext对象调用bean的方法
 */
public class Test {
    public static void main(String[] args) {
        MyApplicationContext context = new MyApplicationContext(AppConfig.class);
        UserService userService = (UserService) context.getBean("userService");
        userService.test();
    }
}

我们现在通过getBean方法肯定无法获取userService, 一是我们需要通过传入MyApplicationContext中的AppConfig上的ComponentScan注解扫描路径, 其次是需要通过getBean()方法返回Bean对象.

我们接下来需要先定义一个@ComponentScan注解:

package com.yve.springframe;

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

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ComponentScan {
    String value() default "";
}

之后我们就可以往AppConfig类上添加@ComponentScan注解来定义扫描路径.

package com.yve.user;

import com.yve.springframe.ComponentScan;

/**
 * @author 伟大的Yve菌
 * AppConfig配置类
 */
@ComponentScan("com.yve.user.service.UserService")
public class AppConfig {
}

我们定义完了扫描注解, 现在需要定义一个@Component注解, 有@Component的类才能被spring扫描到

package com.yve.springframe;

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

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Component {
    String value() default "";
}

之后我们就可以在UserService类上添加@Component注解, 之后再spring进行扫描时UserService类就可以被加载为一个Bean对象了

@Component
public class UserService {

为了方便与UserService对比我们再创建一个OrderService类

package com.yve.user.service;
import com.yve.springframe.Component;

/**
 * @author 伟大的Yve菌
 * orderservice类, 方便与UserService对比
 */
public class OrderService {
    public void test() {
        System.out.println("OrderService");
    }
}

到现在基本的框架写好了, 我们通过创建ApplicationContext传入配置类AppConfig包, 之后spring就可以根据AppConfig包的@ComponentScan注解去扫描对应包, 之后把UserService类和OrderService类加载为bean. 但是目前我们并没有写扫描的逻辑, 我们接下来就去完成扫描相关的逻辑

定义扫描逻辑以及加载bean逻辑

我们需要在MyApplicationContext被创建的时候就去扫描对应路径然后把带有@Component的类加载为bean, 所以我们定义一个scan()方法

/**
     * @param appConfig 传入配置类
     * 此方法用于扫描classpath路径下需要被加载为bean的类, 并对他们进行存放.             
     */
private void scan(Class appConfig) {
        //首先判断当前类上是否有扫描路径注解
        if (appConfig.isAnnotationPresent(ComponentScan.class)) {
            //取出注解中的路径
            ComponentScan componentScan = (ComponentScan) appConfig.getAnnotation(ComponentScan.class);
            String path = componentScan.value(); //com.yve.user.service
            //目前的到的是文件位置, 但是我们需要修改为路径
            path = path.replace(".", "/"); //com/yve/user/service

            //我们需要解析的的实际上是编译之后的class类上的注解, 也就是classpath下的对应目录, 而path中是我们java文件的路径, 所以我们需要ClassLoader来读取到classpath的位置
            ClassLoader classLoader = MyApplicationContext.class.getClassLoader();
            URL resource = classLoader.getResource(path);
            //获取对应路径下的所有文件
            File file = new File(resource.getFile());
            //判断文件是否为目录
            if (file.isDirectory()) {
                for (File listFile : file.listFiles()) {
                    //获取到每个文件的绝对路径
                    String absolutePath = listFile.getAbsolutePath(); //两个文件, 这里为了方便就写一个,\\为单斜杠  D:\\tuling\\code\\spring\\mySpring\\target\\classes\\com\\yve\\user\\service\\UserService.class
                    absolutePath = absolutePath.substring(absolutePath.indexOf("com"), absolutePath.indexOf(".class")).replace("\\", ".");
                    //System.out.println(absolutePath); //com.yve.user.service.OrderService

                    try {
                        //判断每个文件是否有@Component注解
                        Class<?> clazz = classLoader.loadClass(absolutePath);
                        if (clazz.isAnnotationPresent(Component.class)) {
                            System.out.println(clazz); //class com.yve.user.service.UserService
                        }
                    } catch (ClassNotFoundException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

现在我们的scan方法可以获取到所有带有@Component注解的类了, 我们在UserService上添加了@Component注解, OrderService上并没有添加, 因此在打印clazz时只有UserService. 我们既然已经找到了哪些是要加载为Bean的类, 接下来就需要判断这些Bean的作用域, 因此我们先创建一个@Scope注解

package com.yve.springframe;

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

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Scope {
    String value() default "";
}

让我们接着回到scan()方法, 我们在确定了哪些类时bean之后还需要再次判断他们的作用域, 在真实的spring中有5中作用域, 在我们的spring中我们只设置两种, 一种为"singleton", 另一种为"prototype".

但是一个Bean中除了scope之外还有很多属性, 例如Bean的类型, 是否为懒加载等等, 我们不可能每次加载一个类都重新去判断, 因此我们可以创建一个BeanDefinition类, 将bean对象的内部属性全部存进去.

package com.yve.springframe;
/**
 * @author 伟大的Yve菌
 * Bean的内部属性类
 */
public class BeanDefinition {
    private Class type;
    private String scope;

    public Class getType() {
        return type;
    }

    public void setType(Class type) {
        this.type = type;
    }

    public String getScope() {
        return scope;
    }

    public void setScope(String scope) {
        this.scope = scope;
    }
}

我们继续回到判断是否存在@Component的逻辑, 我们现在就可以把带有@Component的类的信息放入到BeanDefinition了

					try {
                        //判断每个文件是否有@Component注解
                        Class<?> clazz = classLoader.loadClass(absolutePath);
                        if (clazz.isAnnotationPresent(Component.class)) {
                            BeanDefinition beanDefinition = new BeanDefinition();
                            beanDefinition.setType(clazz);

                            //如果该类有@Scope注解就保存为注解中的值, 否则默认为singleton
                            if (clazz.isAnnotationPresent(Scope.class)) {
                                beanDefinition.setScope(clazz.getAnnotation(Scope.class).value());
                            } else {
                                beanDefinition.setScope("singleton");
                            }
                        }
                    } catch (ClassNotFoundException e) {
                        e.printStackTrace();
                    }

我们现在已经可以判断bean的作用域和数据类型了, 我们现在需要定义一个map来存放这些bean, beanName作为key, beanDefinition作为value.
在这里插入图片描述
现在我们扫描完之后就该把扫描出来的信息放入到beanDefinitionMap中了.

/**
     * @param appConfig 传入配置类
     * 此方法用于扫描classpath路径下需要被加载为bean的类, 并对他们进行存放.
     */
    private void scan(Class appConfig) {
        //首先判断当前类上是否有扫描路径注解
        if (appConfig.isAnnotationPresent(ComponentScan.class)) {
            //取出注解中的路径
            ComponentScan componentScan = (ComponentScan) appConfig.getAnnotation(ComponentScan.class);
            String path = componentScan.value(); //com.yve.user.service
            //目前的到的是文件位置, 但是我们需要修改为路径
            path = path.replace(".", "/"); //com/yve/user/service

            //我们需要解析的的实际上是编译之后的class类上的注解, 也就是classpath下的对应目录, 而path中是我们java文件的路径, 所以我们需要ClassLoader来读取到classpath的位置
            ClassLoader classLoader = MyApplicationContext.class.getClassLoader();
            URL resource = classLoader.getResource(path);
            //获取对应路径下的所有文件
            File file = new File(resource.getFile());
            //判断文件是否为目录
            if (file.isDirectory()) {
                for (File listFile : file.listFiles()) {
                    //获取到每个文件的绝对路径
                    String absolutePath = listFile.getAbsolutePath(); //两个文件, 这里为了方便就写一个,\\为单斜杠  D:\\tuling\\code\\spring\\mySpring\\target\\classes\\com\\yve\\user\\service\\UserService.class
                    absolutePath = absolutePath.substring(absolutePath.indexOf("com"), absolutePath.indexOf(".class")).replace("\\", ".");
                    //System.out.println(absolutePath); //com.yve.user.service.OrderService

                    try {
                        //判断每个文件是否有@Component注解
                        Class<?> clazz = classLoader.loadClass(absolutePath);
                        if (clazz.isAnnotationPresent(Component.class)) {
                            String beanName = clazz.getAnnotation(Component.class).value();
                            //创建BeanDefinition保存Bean对象信息
                            BeanDefinition beanDefinition = new BeanDefinition();
                            beanDefinition.setType(clazz);

                            //如果该类有@Scope注解就保存为注解中的值, 否则默认为singleton
                            if (clazz.isAnnotationPresent(Scope.class)) {
                                beanDefinition.setScope(clazz.getAnnotation(Scope.class).value());
                            } else {
                                beanDefinition.setScope("singleton");
                            }

                            //把定义好的bean放入到beanDefinitionMap中
                            //如果@Component注解中没有值, 则取该类型首字母小写作为beanName
                            if(beanName.isEmpty()) {
                                beanName = Introspector.decapitalize(clazz.getSimpleName());
                            }
                            beanDefinitionMap.put(beanName, beanDefinition);
                        }
                    } catch (ClassNotFoundException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

接下来我们该完善getBean()方法了

  	/**
     *
     * @param beanName 通过beanName获取到bean对象
     * @return bean对象
     *
     */
    public Object getBean(String beanName) {
        //通过beanDefinitionMap获取到BeanDefinition并根据作用域来返回bean对象
        BeanDefinition beanDefinition = beanDefinitionMap.get(beanName);
        //如果没有beanName则证明该类型bean没有被声明
        if (beanDefinition == null) {
            return new NullPointerException("No bean definition");
        }
        if (beanDefinition.getScope().equals("singleton")) {
            //这里是singleton类型的bean
            
        } else {
            //这里是prototype类型的bean
            
        }
    }

我们通过以上步骤可以根据不同bean的作用域获得bean对象, 因为singleton为单例, 所以我们需要创建一个单例池来把所有的单例bean存放进来, 后续使用直接从单例池中取出来, 我们需要在扫描完成之后时就需要进行遍历bean, 并把所有的单例bean存入单例池.
在这里插入图片描述

/**
     *
     * @param appConfig 传入配置类
     * 配置类赋值, 扫描路径获取到bean对象之后把单例bean放入单例池
     */
    public MyApplicationContext(Class appConfig) {
        this.appConfig = appConfig;
        scan(appConfig);
        //遍历循环每个bean, 将所有的单例bean放入单例池中
        for (Map.Entry<String, BeanDefinition> entry : beanDefinitionMap.entrySet()) {
            //获取名字和信息
            String beanName = entry.getKey();
            BeanDefinition beanDefinition = entry.getValue();
            //如果是单例就创建并放入到单例池
            if (beanDefinition.getScope().equals("singleton")) {
                Object bean = createBean(beanName, beanDefinition);
                singleObjects.put(beanName, bean);
            }
        }
    }

createBean方法

/**
     *
     * @param beanName
     * @param beanDefinition
     * @return bean对象
     * 该方法通过beanDefinition中的type的构造方法创建一个bean对象
     */
    private Object createBean(String beanName, BeanDefinition beanDefinition) {
        //通过type获取到类
        Class clazz = beanDefinition.getType();
        Object instance = null;
        try {
            //通过构造方法创建对象
            instance = clazz.getConstructor().newInstance();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
        return instance;
    }

最后我们可以通过getBean()方法获取到bean对象了

 /**
     *
     * @param beanName 通过beanName获取到bean对象
     * @return bean对象
     *
     */
    public Object getBean(String beanName) {
        //通过beanDefinitionMap获取到BeanDefinition并根据作用域来返回bean对象
        BeanDefinition beanDefinition = beanDefinitionMap.get(beanName);
        //如果没有beanName则证明该类型bean没有被声明
        if (beanDefinition == null) {
            return new NullPointerException("No bean definition");
        }
        if (beanDefinition.getScope().equals("singleton")) {
            //这里是singleton类型的bean
            return singleObjects.get(beanName);
        } else {
            //这里是prototype类型的bean
            return createBean(beanName, beanDefinition);
        }
    }

现在我们基本完成了spring的功能, 我们可以通过@ComponentScan注解获取到扫描路径, 通过路径扫描找出所有@Component的类加载为bean对象, 并可以根据bean的作用域来获取. 我们验证一下

不设置作用域或者作用域设置为"Singleton"
在这里插入图片描述
在这里插入图片描述
作用域设置为"prototype"
在这里插入图片描述
在这里插入图片描述
我们现在就可以通过MyApplicationContext成功获取到我们的bean对象了, 这里我们的OrderService没有添加@Component注解, 因此扫描不到.

接下来我们在添加一些其他功能.首先添加@Autowired注解

package com.yve.springframe;

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 Autowired {
    
}

我们在UserService中添加一个OrderService属性并注解@Autowired, 当然现在我们没有定义内部逻辑, spring无法为OrderService赋值
在这里插入图片描述

我们在创建bean时同时为成员变量注入

 private Object createBean(String beanName, BeanDefinition beanDefinition) {
        //通过type获取到类
        Class clazz = beanDefinition.getType();
        Object instance = null;
        try {
            //通过构造方法创建对象
            instance = clazz.getConstructor().newInstance();

            //为添加了@Autowired注解的属性赋值
            for (Field field : clazz.getDeclaredFields()) {
                if (field.isAnnotationPresent(Autowired.class)) {
                    field.setAccessible(true);
                    //为属性赋值
                    field.set(instance, getBean(field.getName()));
                }
            }
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
        return instance;
    }

这样的逻辑可能会出现bug, 因为我们没有办法判断如果有@Autowired的属性的作用域是"singleton"时单例池中是否已经加载过这个单例bean(我们必须保证单例bean只能存在一个), 如果没有的话我们需要先将他加入单例池之后再赋值, 我们为getBean()方法添加一点判断逻辑.

    public Object getBean(String beanName) {
        //通过beanDefinitionMap获取到BeanDefinition并根据作用域来返回bean对象
        BeanDefinition beanDefinition = beanDefinitionMap.get(beanName);
        //如果没有beanName则证明该类型bean没有被声明
        if (beanDefinition == null) {
            return new NullPointerException("No bean definition");
        }
        if (beanDefinition.getScope().equals("singleton")) {
            //这里是singleton类型的bean
            Object singletonBean = singleObjects.get(beanName);
            //如果这个时候单例bean还没被加载就直接创建加载
            if (singletonBean == null) {
                singletonBean = createBean(beanName, beanDefinition);
                singleObjects.put(beanName, singletonBean);
            }
            return singletonBean;
        } else {
            //这里是prototype类型的bean
            return  createBean(beanName, beanDefinition);
        }
    }

我们现在通过getBean方法获取UserService的bean对象调用test()方法就可以顺利的获取到内部带有@Autowired的OrderService属性了(运行时记得在OrderService类上加上@Component注解, OrderService没有被加载成bean, 就无法注入)在这里插入图片描述

初始化以及前后操作

我们接下来完成bean的初始化操作. 我们创建一个InitializingBean接口并让UserService去实现

package com.yve.springframe;

/**
 * @author 伟大的Yve菌
 * 初始化Bean接口
 */
public interface InitializingBean {
    void afterPropertiesSet();
}
@Component("userService")
@Scope("prototype")
public class UserService implements InitializingBean {

    @Autowired
    private OrderService orderService;

    @Override
    public void afterPropertiesSet() {
    	//我们可以在进行任何想对bean的初始化操作, 在创建bean之前就会完成
        System.out.println("初始化userService");
    }

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

但是目前来说我们没办法在创建Bean时自动调用这个方法, 所以我们需要在CreateBean()方法中添加相应逻辑
在这里插入图片描述
初始化完成后我们将要进行定义一个BeanPostProcesser接口, 里面定义初始化前和初始化后的方法, 之后用户就可以自定义一个类去实现这个接口

package com.yve.springframe;

public interface BeanPostProcessor {
    default Object postProcessBeforeInitialization(Object bean, String beanName) { return bean;}

    default Object postProcessAfterInitialization(Object bean, String beanName) { return bean;}
}
package com.yve.user.service;
import com.yve.springframe.BeanPostProcessor;

/**
 * @author 伟大的Yve菌
 * 定义初始化前后的操作
 */
@Component
public class MyBeanPostProcessor implements BeanPostProcessor {
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) {
        System.out.println(beanName + "初始化后");
        return BeanPostProcessor.super.postProcessAfterInitialization(bean, beanName);
    }

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) {
        System.out.println(beanName + "初始化前");
        return BeanPostProcessor.super.postProcessBeforeInitialization(bean, beanName);
    }
}

现在我们要去在初始化操作的前后去添加这两个方法, 但是在这之前我们还需要在最开始扫描@Component注解时判断哪些带有@Component的类实现了BeanPostProcessor, 同时创建一个list来存放他
在这里插入图片描述
在scan判断@Component是添加判断逻辑在这里插入图片描述
这样我们就可以在初始化的前后添加对应的操作了
在这里插入图片描述
现在我们就可以取到完整的经过BeanPostProcessor以及初始化的bean对象了, 我们可以通过这些方法进行很多操作, 例如aop, value注解等等
在这里插入图片描述
这样我们一个基础的spring框架就搭建好了.


总结

那我们这次的手写简单spring框架就完成了, 如果需要源码的话可以私信我. 谢谢大家

  • 5
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: 首先,感谢您的提问。关于手写一个 Spring 框架,这是一个非常复杂的任务,需要深入了解 Spring 框架的原理和设计思想。如果您想要手写一个 Spring 框架,建议您先学习 Spring 框架的基本原理和核心组件,例如 IoC 容器、AOP、MVC 等。然后,您可以根据自己的需求和业务场景,逐步实现自己的 Spring 框架。当然,这需要您具备较强的 Java 编程能力和系统设计能力。希望我的回答能够帮助到您。 ### 回答2: 手写一个Spring框架是一项复杂且庞大的任务,需要深入理解Spring框架的核心概念和原理。以下是一个简要的步骤指南: 1. 了解Spring框架的核心概念和原理,包括依赖注入、面向切面编程、控制反转等。 2. 创建一个独立的IoC容器,用于管理对象的创建和依赖注入。可以通过反射机制实现对象的自动注入,同时考虑循环依赖的情况。 3. 实现一个简单的AOP框架,通过动态代理机制实现方法拦截和增强。可以使用JDK自带的动态代理或者CGLIB库等。 4. 设计和实现一个MVC框架,用于处理Web请求和返回结果。可以使用Servlet作为底层技术,设计并实现自己的DispatcherServlet。 5. 创建一个配置文件解析器,用于解析Spring配置文件中的Bean定义和依赖关系。可以使用DOM、SAX、XML解析器等。 6. 编写一个Bean工厂,负责管理Bean的生命周期和依赖关系。可以在Bean创建时对其进行初始化和销毁的处理。 7. 实现一个简单的事务管理器,用于处理数据库事务。可以使用JDBC或者类似的持久化框架手写一个Spring框架需要深入理解Java反射、动态代理、设计模式等知识,并具备一定的编程和架构能力。以上只是一个简要的步骤指南,实际的开发过程中还需要考虑到更多的细节和边界情况。因此,对于初学者来说,手写一个完整的Spring框架可能是一个挑战,建议先熟悉和使用官方提供的Spring框架,通过实践逐步深入理解其原理和内部实现,再考虑手写一个简化的版本。 ### 回答3: 手写一个Spring框架是一项庞大而复杂的任务,需要深入理解Spring的设计原理和内部机制。以下是手写一个简化版Spring框架的高级步骤: 第一步:实现IoC容器 IoC(控制反转)是Spring框架的核心概念,通过实现一个简单的IoC容器,来管理对象的创建和依赖注入。容器可以采用单例模式,在初始化时读取配置文件,通过反射实例化并注入依赖。容器会根据配置文件中的Bean定义,创建并管理对象之间的依赖关系。 第二步:实现依赖注入 依赖注入是Spring框架的另一个重要概念,通过实现依赖注入功能,实现对象之间的解耦。可以通过反射遍历对象的属性,扫描注解或配置文件,将依赖的对象自动注入到属性中。 第三步:实现AOP功能 AOP(面向切面编程)是Spring框架中提供的另一个重要功能,通过实现AOP功能,能够在不修改原有代码的情况下,对业务逻辑进行增强。可以使用动态代理等机制,在目标方法的前后增加额外的操作。 第四步:实现Web MVC功能 Spring框架还提供了强大的Web MVC功能,通过实现简化版的请求处理、路由和视图解析等功能,来实现一个基本的Web应用。可以通过Servlet或者自定义HttpHandler来处理请求,并使用模板引擎等技术进行视图解析和渲染。 第五步:实现其他特性 除了上述核心功能外,Spring框架还提供了诸如事务管理、数据访问、安全等一系列扩展功能。可以根据实际需要,逐步实现这些特性。 最后,为了保持代码的可维护性和可扩展性,建议使用设计模式、模块化的方式来实现框架的各个功能。此外,持续学习和了解Spring框架的最新发展和技术变化,可以帮助优化和提升手写框架的质量和性能。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值