Spring原理学习(一):BeanFactory和ApplicationContext的原理和实现

目录

一、BeanFactory和ApplicationContext的关系

二、BeanFactory的功能

三、ApplicationContext的功能

3.1 MessageSource

3.2 ResourcePatternResolver

3.3  EnvironmentCapable

3.4 ApplicationEventPublisher

3.4.1 ApplicationEventPublisher功能体验

 3.4.2 事件有什么用

四、BeanFactory的实现

4.1 DefaultListableBeanFactory

         4.2 BeanFactory的后处理器

        4.3 Bean的后处理器

        4.4 总结

五、ApplicationContext的实现

5.1 ClassPathXmlApplicationContext

5.1.1 使用

        5.1.2 原理

 5.2 FileSystemXmlApplicationContext

5.2.1 使用

5.2.2 原理

5.3 AnnotationConfigApplicationContext

5.3.1 使用

5.4 AnnotationConfigServletWebServerApplication


一、BeanFactory和ApplicationContext的关系

        先看下springboot的引导类:

@SpringBootApplication
public class A01 {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException {
        SpringApplication.run(A01.class, args);
    }
}

        run方法的返回值是springboot容器,我们来看一下:

ConfigurableApplicationContext context = SpringApplication.run(A01.class, args);

        那么ConfigurableApplicationContext是什么呢?咱们来看看类图:

        可以看到,ConfigurableApplicationContext是ApplicationContext的子类,而ApplicationContext又间接继承了BeanFactory。

        BeanFactory才是Spring的核心容器,主要的ApplicationContext实现都“组合”了BeanFactory的功能。

二、BeanFactory的功能

        先看下BeanFactory有哪些接口:

image-20220323145908937

         从表面上看,似乎只有getBean方法,但实际上我们还需要看他的实现类:控制反转、基本的依赖注入、直至 Bean 的生命周期的各种功能, 都由它的实现类提供。

        那么,想要看实现类的功能,我们该从何找起呢?

        我们先查看一下springboot中默认的ConfigurableApplicationContext类中的BeanFactory的实际类型,代码如下:

ConfigurableApplicationContext context = SpringApplication.run(A01Application.class, args);
//org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();

// 查看实际类型
// class org.springframework.beans.factory.support.DefaultListableBeanFactory
System.out.println(beanFactory.getClass());

        从打印结果可以了解到实际类型为DefaultListableBeanFactory(详见4.1),所以这里以BeanFactory的一个实现类DefaultListableBeanFactory作为出发点,进行分析。他的类图:

image-20220323150712761

        我们先看看DefaultListableBeanFactory的父类DefaultSingletonBeanRegistry,看看他的源码:

image-20220323150929451

         可以看到有个成员变量singletonObjects,其实这个变量里面保存了springboot所有的单例,我们可以通过反射拿到singletonObjects后将其打印出来,就能看到所有的单例了。

三、ApplicationContext的功能

        我们已经了解到,ApplicationContext是BeanFactory的子类,并且我们已经了解到了BeanFactory的功能,那么,我们将着重看看ApplicationContext比BeanFactory多了哪些功能。

image-20220324115647260

         可以看到,ApplicationContext除了继承自BeanFactory之外,还继承了以下四个类:

  • MessageSource:国际化功能,支持多种语言
  • ResourcePatternResolver:通配符匹配资源路径
  • EnvironmentCapable:环境信息,系统环境变量,*.properties、*.application.yml等配置文件中的值
  • ApplicationEventPublisher:发布事件对象

        下面我们来分别研究一下这四个类

3.1 MessageSource

       MessageSource拥有国际化功能,支持多种语言。

       与MessageSource有关的国际化功能的文件在springboot中默认放在message打头的文件中,我们先建好这些文件:

        然后在这些文件里面定义同名的key。比如在message_en.properties中定义hi=hello,在messages_ja.propertes中定义hi=こんにちは,在messages_zh中定义hi=你好,这样在代码中就可以根据这个key hi和不同的语言类型**获取不同的值了。 

        编写好代码后,运行起来之后就能看到结果:

image-20220324181409040

        Locale.CHINA、Locale.ENGLISH等值在实际项目中会由前端解析到界面所用的语言后传过来。

3.2 ResourcePatternResolver

        ResourcePatternResolver可以通过通配符来匹配资源路径。

        例1:获取类路径下的messages开头的配置文件:

Resource[] resources = context.getResources("classpath:messages*.properties");
for (Resource resource : resources) {
    System.out.println(resource);
}

         例2:获取spring相关jar包中的spring.factories配置文件:

resources = context.getResources("classpath*:META-INF/spring.factories");
for (Resource resource : resources) {
    System.out.println(resource);
}

3.3  EnvironmentCapable

        EnvironmentCapable可以获取系统环境信息或系统环境变量里的值,比如环境变量、*.properties、*.application.yml等配置文件中的值。

//获取系统环境变量中的java_home
System.out.println(context.getEnvironment().getProperty("java_home"));
//获取项目的application.yml中的server.port属性
System.out.println(context.getEnvironment().getProperty("server.port"));

3.4 ApplicationEventPublisher

        ApplicationEventPublisher可以用来发布事件。

3.4.1 ApplicationEventPublisher功能体验

        想要试试发布事件的功能,我们需要准备三个部分:事件发送类、事件接收(监听)类、事件类。

        先看事件类,他继承自ApplicationEvent:

public class UserRegisteredEvent extends ApplicationEvent {
    public UserRegisteredEvent(Object source) {
        super(source);
    }
}

        再定义一个事件接受(监听)类,用于监听用户注册事件。类上需要加@Component注解,将该类交给spring管理。spring中任意个容器都可以作为监听器。然后定义一个处理事件的方法,参数类型为事件类的对象,方法头上需要加上@EventListener注解。

@Component
@Slf4j
public class UserRegisteredListener {
    @EventListener
    public void userRegist(UserRegisteredEvent event) {
        System.out.println("UserRegisteredEvent...");
        log.debug("{}", event);
    }
}

        再定义一个发送事件的类,就是使用ApplicationEventPublisher的实例对象调用pubulishEvent方法发送,传入的参数是我们刚刚定义好的事件类:

@Component
@Slf4j
public class UserService {
    @Autowired
    private ApplicationEventPublisher context;
    public void register(String username, String password) {
        log.debug("新用户注册,账号:" + username + ",密码:" + password);
        context.publishEvent(new UserRegisteredEvent(this));
    }
}

        然后在主启动类中调用一下就可以了:

UserService userService = context.getBean(UserService.class);
userService.register("张三", "123456");

 3.4.2 事件有什么用

        事件最主要的功能就是解耦。

        譬如我们使用事件来做一个用户注册的功能,功能里有用户注册类UserService,用来发送事件,也有用户注册监听类UserRegisteredListener,用于接收事件。由于用户注册后我们有多种后续操作,比如给用户发短信、给用户发邮件或给用户发微信公众号提醒。这样就要求我们的系统有良好的可扩展性,UserService类和UserRegisteredListener类不能耦合在一起,而我们使用事件(如上),就能够实现UserService类和UserRegisteredListener类的解耦:用户注册类UserService发送事件后,我们可以用不同的监听类来接收,不同的监听类做不同的事情;比如我们可以用UserRegisteredListener1来给用户发短信,用UserRegisteredListener2来给用户发邮件。

        使用事件进行解耦是一种新的解耦方式,他与AOP方式有什么不同呢?这个值得我们思考。

四、BeanFactory的实现

4.1 DefaultListableBeanFactory

        BeanFactory的实现类十分多,我们需要抓住一个重点的实现类去看,这个类就是DefaultListableBeanFactory。 

         接二(BeanFactory的功能)中所言,Spring底层创建实体类就是依赖于DefaultListableBeanFactory,所以,他是BeanFactory的实现类中最重要的一个。我们有必要使用一下这个类,来模拟Spring使用DefaultListableBeanFactory创建其他实体类对象的过程。

public class TestBeanFactory {
    public static void main(String[] args) {

        //先创建bean工厂,刚创建的时候是没有任何bean的,我们需要往里面添加bean的定义
        DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
        // bean 的定义(即bean的一些描述信息,包含class:bean是哪个类,scope:单例还是多例,初始化、销毁方法等)
        AbstractBeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition(Config.class).setScope("singleton").getBeanDefinition();
        //把beanDefinition这个bean定义注册进bean工厂,第一个参数是给它起的名字
        beanFactory.registerBeanDefinition("config", beanDefinition);

        
        // 打印BeanFactory中Bean
        for (String name : beanFactory.getBeanDefinitionNames()) {
            System.out.println(name);
        }
    }

    @Configuration
    static class Config {
        @Bean
        public Bean1 bean1() {
            return new Bean1();
        }

        @Bean
        public Bean2 bean2() {
            return new Bean2();
        }
    }

    // bean1依赖于bean2
    @Slf4j
    static class Bean1 {
        @Autowired
        private Bean2 bean2;

        public Bean2 getBean2() {
            return bean2;
        }

        public Bean1() {
            log.debug("构造 Bean1()");
        }
    }

    @Slf4j
    static class Bean2 {
        public Bean2() {
            log.debug("构造 Bean2()");
        }
    }

    interface Inter {

    }
}

        这个时候打印出bean工厂中有多少bean,结果只有一个,就是我们刚刚注册进去的config。

        那么问题来了,我们在spring实战的知识中得知:当加上@Configuration和@Bean时,容器中会注册这些bean;换句话说,我们此时打印bean工厂的所有bean,理应看到bean1和bean2,而不是只有config。此时只有一个解释能成立:@Configuration和@Bean都没有被解析。那么解析这些注解的功能由谁提供呢?

         4.2 BeanFactory的后处理器

        BeanFactory本身实现的功能并不多,他的许多功能都是由BeanFactory的后处理器进行扩展的。

AnnotationConfigUtils.registerAnnotationConfigProcessors(beanFactory);

        使用上面的工具类可以为bean工厂添加一些常用的后处理器,此时我们打印bean工厂里的所有bean:

        可以看到现在多了一些后处理器从名字也可以大致猜出,他们是处理@Configuration的、@Autowired的……,现在只是把他们加进了bean工厂,还需要让他们工作起来。

public class TestBeanFactory {

    public static void main(String[] args) {
        DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
        // bean 的定义(class, scope, 初始化, 销毁)
        AbstractBeanDefinition beanDefinition =
                BeanDefinitionBuilder.genericBeanDefinition(Config.class).setScope("singleton").getBeanDefinition();
        beanFactory.registerBeanDefinition("config", beanDefinition);

        // 给 BeanFactory 添加一些常用的后处理器
        AnnotationConfigUtils.registerAnnotationConfigProcessors(beanFactory);

    beanFactory.getBeansOfType(BeanFactoryPostProcessor.class).values().forEach(beanFactoryPostProcessor -> {
            beanFactoryPostProcessor.postProcessBeanFactory(beanFactory);
        });

        // 打印BeanFactory中Bean
        for (String name : beanFactory.getBeanDefinitionNames()) {
            System.out.println(name);
        }
    }

    @Configuration
    static class Config {
        @Bean
        public Bean1 bean1() {
            return new Bean1();
        }

        @Bean
        public Bean2 bean2() {
            return new Bean2();
        }
    }

    static class Bean1 {
        private static final Logger log = LoggerFactory.getLogger(Bean1.class);

        public Bean1() {
            log.debug("构造 Bean1()");
        }

        @Autowired
        private Bean2 bean2;

        public Bean2 getBean2() {
            return bean2;
        }
    }

    static class Bean2 {
        private static final Logger log = LoggerFactory.getLogger(Bean2.class);

        public Bean2() {
            log.debug("构造 Bean2()");
        }
    }
}

        此时再打印所有的bean,就可以看到bean1和bean2已经出现了。

        4.3 Bean的后处理器

        在4.2中我们添加了一些后处理器,比如internalConfigurationAnnotationProcessor是处理@Configuration的,它属于BeanFactory的后处理器;而internalAutowiredAnnotationProcessor和internalCommonAnnotationProcessor就属于bean的后处理器,他们是针对 bean 的生命周期的各个阶段提供扩展, 例如 internalAutowiredAnnotationProcessor用于解析@Autowired、internalCommonAnnotationProcessor用于解析@Resource。

        如果我们需要让@Autowired 和 @Resource注解也发挥作用,则需要:

// Bean 后处理器, 针对 bean 的生命周期的各个阶段提供扩展, 例如 @Autowired @Resource ...
        beanFactory.getBeansOfType(BeanPostProcessor.class).values().stream()
                .sorted(beanFactory.getDependencyComparator())
                .forEach(beanPostProcessor -> {
            System.out.println(">>>>" + beanPostProcessor);
            beanFactory.addBeanPostProcessor(beanPostProcessor);
        });

        这样我们的@Autowired 和 @Resource注解就发挥作用了。请注意,我们这里getBeansOfType()方法传递的参数是BeanPostProcessor.class,是bean的后处理器。

        4.4 总结

        BeanFactory是一个比较基础的类,他本身并没有特别多的功能,这些事情它不会去做:

  • 不会主动调用BeanFactory后处理器
  • 不会主动添加Bean后处理器
  • 不会主动初始化单例
  • 不会解析#{}、${}等

五、ApplicationContext的实现

        先看看ApplicationContext的实现类有哪些:

image-20220325174005606

        今天我们来介绍四个比较重要的实现类:

  • ClassPathXmlApplicationContext
  • FileSystemXmlApplicationContext
  • AnnotationConfigApplicationContext
  • AnnotationConfigServletWebServerApplication

5.1 ClassPathXmlApplicationContext

        较为经典的容器, 基于 classpath(类路径)下 xml 格式的配置文件来创建ApplicationContext.

5.1.1 使用

        创建一个测试类 

private static void testClassPathXmlApplicationContext() {
    ClassPathXmlApplicationContext context = 
                                    new ClassPathXmlApplicationContext("a02.xml");

    //看一下ApplicationContext中有多少bean
    for (String name : context.getBeanDefinitionNames()) {
        System.out.println(name);
    }

    //看看bean2中有没有成功注入bean1
    System.out.println(context.getBean(Bean2.class).getBean1());
}

static class Bean1 {
}

static class Bean2 {

    private Bean1 bean1;

    public void setBean1(Bean1 bean1) {
        this.bean1 = bean1;
    }

    public Bean1 getBean1() {
        return bean1;
    }
}

         创建xml配置文件,并在文件中定义bean

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- 控制反转, 让 bean1 被 Spring 容器管理 -->
    <bean id="bean1" class="com.itheima.a02.A02.Bean1"/>

    <!-- 控制反转, 让 bean2 被 Spring 容器管理 -->
    <bean id="bean2" class="com.itheima.a02.A02.Bean2">
        <!-- 依赖注入, 建立与 bean1 的依赖关系 -->
        <property name="bean1" ref="bean1"/>
    </bean>
</beans>

        运行结果:

  

5.1.2 原理

        我们模拟一下加载xml文件的过程即可明白原理了:先初始化出DefaultListableBeanFactory,然后通过 XmlBeanDefinitionReader 到xml文件中读取bean的配置信息,将这些bean加载到bean工厂中。 

public static void main(String[] args) {
        //先实现DefaultListableBeanFactory
        DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
        System.out.println("读取之前...");
        for (String name : beanFactory.getBeanDefinitionNames()) {
            System.out.println(name);
        }

        System.out.println("读取之后...");
        //然后到xml文件中读取bean的定义信息
        XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory);
        reader.loadBeanDefinitions(new ClassPathResource("a02.xml"));
        for (String name : beanFactory.getBeanDefinitionNames()) {
            System.out.println(name);
        }
    }

 5.2 FileSystemXmlApplicationContext

        基于磁盘路径下 xml 格式的配置文件来创建ApplicationContext。

5.2.1 使用

        编写一个测试类:

private static void testFileSystemXmlApplicationContext() {
        FileSystemXmlApplicationContext context =
                new FileSystemXmlApplicationContext(
                        "src\\main\\resources\\a02.xml");
        for (String name : context.getBeanDefinitionNames()) {
            System.out.println(name);
        }

        //看看bean2中有没有成功注入bean1
        System.out.println(context.getBean(Bean2.class).getBean1());
    }

static class Bean1 {
}

static class Bean2 {

    private Bean1 bean1;

    public void setBean1(Bean1 bean1) {
        this.bean1 = bean1;
    }

    public Bean1 getBean1() {
        return bean1;
    }
}

        xml文件与5.1.1中为同一个。运行结果也同5.1.1 

5.2.2 原理

        我们模拟一下加载xml文件的过程即可明白原理了:先初始化出DefaultListableBeanFactory,然后通过 XmlBeanDefinitionReader 到xml文件中读取bean的配置信息,将这些bean加载到bean工厂中。  

public static void main(String[] args) {
        DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
        System.out.println("读取之前...");
        for (String name : beanFactory.getBeanDefinitionNames()) {
            System.out.println(name);
        }

        System.out.println("读取之后...");
        //然后到xml文件中读取bean的定义信息
        XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory);
        reader.loadBeanDefinitions(new FileSystemResource("src\\main\\resources\\a02.xml"));
        for (String name : beanFactory.getBeanDefinitionNames()) {
            System.out.println(name);
        }
    }

5.3 AnnotationConfigApplicationContext

5.3.1 使用

        较为经典的容器, 基于 java 配置类来创建ApplicationContext。 

private static void testAnnotationConfigApplicationContext() {
    AnnotationConfigApplicationContext context =
                new AnnotationConfigApplicationContext(Config.class);

    for (String name : context.getBeanDefinitionNames()) {
        System.out.println(name);
    }

    System.out.println(context.getBean(Bean2.class).getBean1());
}

@Configuration
static class Config {
    @Bean
    public Bean1 bean1() {
        return new Bean1();
    }

    @Bean
    public Bean2 bean2(Bean1 bean1) {
        Bean2 bean2 = new Bean2();
        bean2.setBean1(bean1);
        return bean2;
    }
}

static class Bean1 {
}

static class Bean2 {

    private Bean1 bean1;

    public void setBean1(Bean1 bean1) {
        this.bean1 = bean1;
    }

    public Bean1 getBean1() {
        return bean1;
    }
}

        运行结果:

         可以看到,结果与5.1.1和5.2.1有不同,因为我们的配置类Config也默认为一个bean注入进了bean工厂;除此之外,AnnotationConfigApplicationContext 还自动帮我们加了五个后处理器。

5.4 AnnotationConfigServletWebServerApplication

        较为经典的容器, 基于 java 配置类来创建ApplicationContext, 用于 web 环境。

        我们创建一个测试类来使用它:

    private static void testAnnotationConfigServletWebServerApplicationContext() {
        AnnotationConfigServletWebServerApplicationContext context =
                new AnnotationConfigServletWebServerApplicationContext(WebConfig.class);
        for (String name : context.getBeanDefinitionNames()) {
            System.out.println(name);
        }
    }

    @Configuration
    static class WebConfig {
        //tomcat容器
        @Bean
        public ServletWebServerFactory servletWebServerFactory(){
            return new TomcatServletWebServerFactory();
        }
        //前控制器
        @Bean
        public DispatcherServlet dispatcherServlet() {
            return new DispatcherServlet();
        }
        //让前控制器运行在Tomcat容器中
        @Bean
        public DispatcherServletRegistrationBean registrationBean(DispatcherServlet dispatcherServlet) {
            return new DispatcherServletRegistrationBean(dispatcherServlet, "/");
        }
        //控制器
        @Bean("/hello")
        public Controller controller1() {
            return (request, response) -> {
                response.getWriter().print("hello");
                return null;
            };
        }
    }

        可以看到后台打印出很多的bean。

        从这里可以了解到:

  • springboot内嵌了Tomcat,我们无需手动添加Tomcat容器,也可以运行bean
  • springboot的所有请求都要经过dispatchServlet(前控制器),然后再走到我们自己的控制器中
  • 通过DispatcherServletRegistrationBean 可以将DispatcherServlet 注册到Tomcat中

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
面试高级开发的期间整理的面试题目,记录我面试遇到过的spring题目以及答案 目录 spring ThreadLocal的底层对象; 为什么@Service和@Repository放到实现类上面而不是接口类上面; spring 三种注入(就是从spring容器中将bean放入对象属性值中) Spring下描述依赖关系@Resource, @Autowired和@Inject的区别与联系 SpringBeanFactoryApplicationContext的区别 谈谈Spring IOC的理解,原理实现? bean的生命周期,详细看上面 SpringBoot自动装配的过程的原理spring的缓存; spring是如何解决的循环依赖; BeanFactoryFactoryBean有什么区别; Spring中用到的设计模式; SPI 机制(Java SPI 实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制), 很多地方有用到: AOP Spring的AOP的底层实现原理; 为什么jdk动态代理是必须是接口 两种动态代理的区别 AOP实现方式:aop注解或者xml配置;后来工具jar包aspects; aop的属性 事务 事务编码方式: 事务注意事项; 为什么同一个类A调用b方法事务,A方法一定要有事务(编码式的不用) @transaction多个数据源事务怎么指定数据源 传播特性有几种?7种; 某一个事务嵌套另一个事务的时候怎么办? REQUIRED_NEW和REQUIRED区别 Spring的事务是如何回滚的,实现原理; 抽象类和接口的区别,什么时候用抽象类什么时候用接口; StringBuilder和StringBuffer的区别 java值传递和引用传递
### 回答1: BeanFactoryApplicationContext 是在 Spring 框架中用于管理 Bean 对象的两个重要接口。 BeanFactorySpring 框架中最基本的容器,它主要提供了对 Bean 的实例化、配置和管理的功能。 ApplicationContextBeanFactory 的一个子接口,它在 BeanFactory 的基础上提供了更多高级的特性,如国际化、事件传递等。 总的来说,如果只需要基本的 Bean 管理功能,可以使用 BeanFactory;如果需要使用更多高级特性,则可以使用 ApplicationContext。 ### 回答2: BeanFactoryApplicationContext都是Spring框架中用来管理Bean的容器,但它们之间也有一些不同之处。 BeanFactorySpring框架最基本的容器接口,它提供了最基本的IOC容器功能。 它的主要责任是实例化、配置和管理应用程序中的对象。它通过读取配置文件或者注解来创建和管理Bean对象,可以单例或者多例的方式提供Bean。它使用延迟初始化的方式进行Bean的实例化,即只有在真正使用Bean时才会进行实例化,从而避免了不必要的资源消耗。BeanFactory是一个底层的接口,对于开发者而言使用的更多的是其子类ApplicationContextApplicationContextBeanFactory的子接口,它提供了更丰富的功能和更方便的使用方式。ApplicationContext除了提供BeanFactory的功能外,还可以直接集成AOP、事件传播、国际化信息等功能。它支持更多类型的Bean,比如单例、多例以及原型等。它提供了更多的钩子方法,比如Bean的初始化和销毁操作等。ApplicationContext还支持通过容器的扩展机制来实现定制化需求,比如自定义Bean的创建方式、属性注入方式等。 总结起来,BeanFactorySpring框架最基本的IOC容器接口,提供了实例化和管理Bean的基本功能。而ApplicationContextBeanFactory的子接口,提供了更多高级功能和更方便的使用方式。在实际开发中,我们更多地会使用ApplicationContext来管理Bean,因为它提供了更多的功能和灵活性,能够更好地满足我们的需求。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值