Spring 容器启动

目录

1. Spring 容器启动过程

1.1 AnnotationConfigApplicationContext

1.1.1 有参数构造方法

1.1.2 无参数构造

1.2 配置类读取对象 AnnotatedBeanDefinitionReader 构造

1.3 配置类解析 Register(解析配置类 Appconfig)

2. refresh 

Spring 容器启动整体流程


Spring启动过程详解_Life journey的博客-CSDN博客_spring启动

1. Spring 容器启动过程

        Spring 启动入口有很多,在 XML 中有 XML 的方式,在注解中有注解的方式,在 web 中的方式也有 web 的注解启动方式。AnnotationConfigApplicationContext 是以注解的配置类的方式启动,就是传入一个配置类,这个配置类包含了需要注册到容器中的 bean 的一些信息,比如扫描类路径信息。但这个启动入口类是不支持容器的重复刷新的,也就是 refresh() 方法只能调用一次,而  AnnotationWebConfigApplicationContext 这个是支持重复刷新的容器。下面以 AnnotationConfigApplicationContext 为例讲解 Spring 的启动流程

public void test06(){
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
    ac.registerShutdownHook();
    System.out.println(ac.getBean(Person.class));
}

1.1 AnnotationConfigApplicationContext

       AnnotationConfigApplicationContext 是以注解的方式启动,设置一个配置类,比如AppConfig,然后这个 AppConfig 可以配置 @Bean,也可以设置 @Compoentscan(该注解里面可以配置要扫描的类路径信息)。

1.1.1 有参数构造方法

public AnnotationConfigApplicationContext(Class<?>... componentClasses) {

   //调用自己的另外一个无参数的构造方法,去初始化BeanFactory,创建配置类读取对象AnnotatedBeanDefinitionReader以及classpath扫描对象
   this();

   /**
    * 注册配置类信息,这register就是调用reader将我们定义的配置类扫描成一个配置类,配置类中有@CompoentScan配置的扫描路径
    * 都会解析成一个BeanDefinition,然后在bean工厂后置处理器中会读取这个配置类,然后进行扫描,将路径下的所有符合条件的普通类
    * 都扫描成一个BeanDefinition对象然后放入到beanDefinitionMap中,其中包括了factoryBean、我们自己定义的beanFactory后置处理器、bean的后置处理器
    */
   register(componentClasses);

   /**
    * 刷新工厂,目前所在的这个类AnnotationConfigApplicationContext是不支持重复刷新的
    * 这里理解是刷新工厂,其实就是spring的一个启动过程,在前面将bean工厂的后置处理器和后面的一些bean的后置处理器都加到了
    * beanFactory这种,下面的refresh就是开始对spring的容器进行启动,扫描、注册、创建单例池、国际化以及事件的监听的相关操作
    */
   refresh();
}

1.1.2 无参数构造

public AnnotationConfigApplicationContext() {
   /**
    * 1.调用父类GenericApplicationContext的构造方法实例化一个bean工厂DefaultListableBeanFactory
    * 2.调用父类初始化ASM 读取class的相关对象
    * 3.在IOC容器中初始化一个 注解bean读取器AnnotatedBeanDefinitionReader
    *  3.1初始化注解读取器的时候将spring的原生态的一些系统处理类放入工厂(DefaultListableBeanFactory)的一个map里面
    *  3.2 初始化的spring原生类是作为spring的默认后置处理器
    *  AnnotatedBeanDefinitionReader是可以将我们的一个普通类注册成一个BeanDefinition
    *  AnnotatedBeanDefinitionReader还将一些bean的后置处理器放入到bean后置处理器列表中
    */
   this.reader = new AnnotatedBeanDefinitionReader(this);
   /**
    * 在IOC容器中初始化一个 按类路径扫描注解bean的 扫描器
    * 请注意,这里的扫描器是我们在外层给定一个包路径,它来扫描,
    * 如果我们仅仅是通过register()和refresh()方法的话,是不会用到scanner来扫描的
    * 而spring底层在refresh容器的时候读取CommponetnScan中包路径的时候扫描是通过自己构建一个新的
    * ClassPathBeanDefinitionScanner来扫描的
    * 所以ClassPathBeanDefinitionScanner是可以将我们的一个类路径下的所有的符合条件的普通类扫描成一个一个的BeanDefinition
    * 然后注册到beanDefintionMap中
    */
   this.scanner = new ClassPathBeanDefinitionScanner(this);
}

      上面的两个构造方法其实都可以使用,使用无参数的构造,那么你要手动注册一个配置类,也就是我们上面的 AppConfig;如果调用有参数的构造,那么 Spring 会默认调用无参数的构造进行 Spring 的容器初始化,比如工厂的创建,扫描器对象的创建、配置类读取器对象的创建以及一些工具类、beanfactory 的后置处理器以及 bean 的一些后置处理器的创建,都是在无参数的构造里面创建了,说的简单点都是在 AnnotatedBeanDefinitionReader 中创建的;而 Bean 工厂的创建是在父类 GenericApplicationContext 中创建的。

1.2 配置类读取对象 AnnotatedBeanDefinitionReader 构造

public AnnotatedBeanDefinitionReader(BeanDefinitionRegistry registry, Environment environment) {
   Assert.notNull(registry, "BeanDefinitionRegistry must not be null");
   Assert.notNull(environment, "Environment must not be null");
   this.registry = registry;
   this.conditionEvaluator = new ConditionEvaluator(registry, environment, null);
   /**
    * 扫描spring 原生的后置处理器放入工厂的bd map中(bean工厂的beanDefinitionMap)
    * spring底层的bd都是RootBeanDefinition
    *      1.添加ConfigurationClassPostProcessor成一个BeanDefinition;
    *      2.添加AutowiredAnnotationBeanPostProcessor成一个BeanDefinition;
    *         3.添加CommonAnnotationBeanPostProcessor成一个BeanDefinition;
    *         4.添加EventListenerMethodProcessor成一个BeanDefinition;
    *         5.添加DefaultEventListenerFactory成一个BeanDefinition;
    */
   AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);
}
/**
    * Register all relevant annotation post processors in the given registry.
    * @param registry the registry to operate on
    * @param source the configuration source element (already extracted)
    * that this registration was triggered from. May be {@code null}.
    * @return a Set of BeanDefinitionHolders, containing all bean definitions
    * that have actually been registered by this call
    * 这个方法主要是根据当前的bean工厂做一些设置:
    * 1.添加一个默认的比较器
    * 2.设置一个上下文的筛选器(主要对bean的查找进行筛选的类)
    * 3.添加ConfigurationClassPostProcessor成一个BeanDefinition;
    * 4.添加AutowiredAnnotationBeanPostProcessor成一个BeanDefinition;
    * 5.添加CommonAnnotationBeanPostProcessor成一个BeanDefinition;
    * 6.添加EventListenerMethodProcessor成一个BeanDefinition;
    * 7.添加DefaultEventListenerFactory成一个BeanDefinition;
    * 在没有启用JPA的情况下,胡添加5个BeanDefinition(后置处理器),一个比较器BeanDefinition,一个bean筛选器(BeanDefinition)
    */
   public static Set<BeanDefinitionHolder> registerAnnotationConfigProcessors(
         BeanDefinitionRegistry registry, @Nullable Object source) {

      //先得到一个工厂Bean工厂,这个Bean工厂是之前初始化好的,是一个DefaultListableBeanFactory
      DefaultListableBeanFactory beanFactory = unwrapDefaultListableBeanFactory(registry);
      if (beanFactory != null) {
         //这里的Bean工厂肯定不能为空,这里是设置比较器的,如果说你没有设置比较器之类的,这里设置一个默认的比较器,这个比较器
         //可以在使用BeanDefinition排序的时候使用,比如说你实现了Order接口或者PriorityOrdered的时候,BeanDefinition的执行
         //顺序可以使用它来进行排序
         if (!(beanFactory.getDependencyComparator() instanceof AnnotationAwareOrderComparator)) {
            beanFactory.setDependencyComparator(AnnotationAwareOrderComparator.INSTANCE);
         }
         //这里就是之前我们看的bean中的依赖注入的时候,先byType的时候,对找到的多个bean有筛选,比如先byType,再进行是否启用了
         //自动注入候选者,泛型的判断以及Qualifier的筛选
         /**
          * ContextAnnotationAutowireCandidateResolver中的父类是QualifierAnnotationAutowireCandidateResolver
          * QualifierAnnotationAutowireCandidateResolver主要是对泛型的筛选和Qualifier的bean进行筛选,而ContextAnnotationAutowireCandidateResolver
          * 是QualifierAnnotationAutowireCandidateResolver它的子类,主要提供了一些代理工厂的创建,延迟加载的一些判断
          */
         if (!(beanFactory.getAutowireCandidateResolver() instanceof ContextAnnotationAutowireCandidateResolver)) {
            beanFactory.setAutowireCandidateResolver(new ContextAnnotationAutowireCandidateResolver());
         }
      }

      Set<BeanDefinitionHolder> beanDefs = new LinkedHashSet<>(8);

       //这里添加一个ConfigurationClass的后置处理器到bd中
      if (!registry.containsBeanDefinition(CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME)) {
         /**
          * 这里添加的是ConfigurationClassPostProcessor这个BeanDefinition,这个BeanDefinition很重要
          * 它本身也是一个beanFactory的后置处理器,这里添加进去的意思就是说后面spring启动扫描的时候就是用这个后置处理器来
          * 扫描我们的配置类,比如我的配置类是Appconfig,那么这个后置处理器就是专门处理这个配置类配置的类路径信息
          * 所以说这个beanFactory后置处理器非常重要,简单来说就是对我们配置类路径进行扫描,扫描成一个一个的BeanDefinition
          * 然后放入beanDefinitonMap中,就是这个ConfigurationClassPostProcessor后置处理器来做的事情
          *
          * 这里生成的是一个RootBeanDefinition,看了spring的生命周期都知道,spring中的扫描成的BeanDefinition最后都会合并成
          * RootBeanDefiniton,意思就是它没有父类的bd了
          */
         RootBeanDefinition def = new RootBeanDefinition(ConfigurationClassPostProcessor.class);
         def.setSource(source);
         beanDefs.add(registerPostProcessor(registry, def, CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME));
      }
      //这里添加一个AutowiredAnnotationBeanPostProcessor,这个AutowiredAnnotationBeanPostProcessor在spring的生命周期中
      //非常重要,主要是处理依赖注入的@AutoWired
      if (!registry.containsBeanDefinition(AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME)) {
         RootBeanDefinition def = new RootBeanDefinition(AutowiredAnnotationBeanPostProcessor.class);
         def.setSource(source);
         beanDefs.add(registerPostProcessor(registry, def, AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME));
      }
      /**
       * 下面这个注册是CommonAnnotationBeanPostProcessor,这个bean的后置处理器主要处理@Resource、@PostConstruct
       * @PreDestory注解,也是依赖注入的一部分,这里先把这个bean的后置处理器加入到beanDefinitionMap中
       */
      // Check for JSR-250 support, and if present add the CommonAnnotationBeanPostProcessor.
      if (jsr250Present && !registry.containsBeanDefinition(COMMON_ANNOTATION_PROCESSOR_BEAN_NAME)) {
         RootBeanDefinition def = new RootBeanDefinition(CommonAnnotationBeanPostProcessor.class);
         def.setSource(source);
         beanDefs.add(registerPostProcessor(registry, def, COMMON_ANNOTATION_PROCESSOR_BEAN_NAME));
      }

      // Check for JPA support, and if present add the PersistenceAnnotationBeanPostProcessor.
      //如果你的系统中启用了JPA的方式,那么这里添加一个JPA的后置处理器
      if (jpaPresent && !registry.containsBeanDefinition(PERSISTENCE_ANNOTATION_PROCESSOR_BEAN_NAME)) {
         RootBeanDefinition def = new RootBeanDefinition();
         try {
            def.setBeanClass(ClassUtils.forName(PERSISTENCE_ANNOTATION_PROCESSOR_CLASS_NAME,
                  AnnotationConfigUtils.class.getClassLoader()));
         }
         catch (ClassNotFoundException ex) {
            throw new IllegalStateException(
                  "Cannot load optional framework class: " + PERSISTENCE_ANNOTATION_PROCESSOR_CLASS_NAME, ex);
         }
         def.setSource(source);
         beanDefs.add(registerPostProcessor(registry, def, PERSISTENCE_ANNOTATION_PROCESSOR_BEAN_NAME));
      }

//    事件方法的监听器BeanFactoryPostProcessor,是一个bean工厂的后置处理器
      if (!registry.containsBeanDefinition(EVENT_LISTENER_PROCESSOR_BEAN_NAME)) {
         RootBeanDefinition def = new RootBeanDefinition(EventListenerMethodProcessor.class);
         def.setSource(source);
         beanDefs.add(registerPostProcessor(registry, def, EVENT_LISTENER_PROCESSOR_BEAN_NAME));
      }
      //这里添加的是一个默认的事件监听工厂
      if (!registry.containsBeanDefinition(EVENT_LISTENER_FACTORY_BEAN_NAME)) {
         RootBeanDefinition def = new RootBeanDefinition(DefaultEventListenerFactory.class);
         def.setSource(source);
         beanDefs.add(registerPostProcessor(registry, def, EVENT_LISTENER_FACTORY_BEAN_NAME));
      }

      return beanDefs;
   }

      配置类读取对象 AnnotatedBeanDefinitionReader 的构造方法主要是根据当前的 bean 工厂做一些设置

   (1)添加一个默认的比较器;

   (2)设置一个上下文筛选器,主要对 bean 的查找进行筛选;

   (3)添加 ConfigurationClassPostProcessor 成一个 BeanDefifition(配置类解析器);

   (4)添加 AutowiredAnnotationBeanPostProcessor 成一个 BeanDefinition(@Autowired 依赖注入);

   (5)添加 CommonAnnotationBeanPostProcessor 成一个 BeanDefinition(@Resource 依赖注入、生命周期回调);

   (6)添加 EventListenerMethodProcessor 成一个BeanDefinition;

   (7)添加 DefaultEventListenerFactory 成一个BeanDefinition;

    在没有启用 JPA 的情况下,共添加5个 BeanDefinition(后置处理器)、一个比较器 BeanDefinition、一个 bean 筛选器(BeanDefinition)。

1.3 配置类解析 Register(解析配置类 Appconfig)

      register() 方法将我们的配置类 Appconfig 解析成一个 BeanDefinition,然后放入到 BeanDefinitionMap 中。AppConfig 配置类包含了我们的系统的类扫描路径,

public void register(Class<?>... componentClasses) {
   Assert.notEmpty(componentClasses, "At least one component class must be specified");
   /**
    * 这里开始注册我们的Bean,这个Bean是配置类的Bean,它告诉了spring需要扫哪些包,而本身这个配置类的bean最终也会成为一个BeanDefinition
    */
   this.reader.register(componentClasses);
}
private <T> void doRegisterBean(Class<T> beanClass, @Nullable String name,
      @Nullable Class<? extends Annotation>[] qualifiers, @Nullable Supplier<T> supplier,
      @Nullable BeanDefinitionCustomizer[] customizers) {

   /**
    * 将Bean配置类信息转成容器中AnnotatedGenericBeanDefinition数据结构, AnnotatedGenericBeanDefinition继承自BeanDefinition作用是定义一个bean的数据结构,
    * 下面的getMetadata可以获取到该bean上的注解信息
    * 下面的构造方法spring通过手段读取我们配置的配置类比如Appconfig这个类
    * 他会把这个类上的所有注解信息都读取到abd中
    */
   AnnotatedGenericBeanDefinition abd = new AnnotatedGenericBeanDefinition(beanClass);
   //@Conditional装配条件判断是否需要跳过注册
   if (this.conditionEvaluator.shouldSkip(abd.getMetadata())) {
      return;
   }
       //设置回调
   abd.setInstanceSupplier(supplier);
   //解析bean作用域(单例或者原型),如果有@Scope注解,则解析@Scope,没有则默认为singleton
   ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(abd);
   //作用域写回BeanDefinition数据结构, abd中缺损的情况下为空,将默认值singleton重新赋值到abd
   abd.setScope(scopeMetadata.getScopeName());
   //生成bean配置类beanName
   String beanName = (name != null ? name : this.beanNameGenerator.generateBeanName(abd, this.registry));
       //通用注解解析到abd结构中,主要是处理Lazy, primary DependsOn, Role ,Description这五个注解
   AnnotationConfigUtils.processCommonDefinitionAnnotations(abd);
   // @Qualifier特殊限定符处理,一般用这个注解的扫描类,是没有这些东西的,我猜其他子类的扫描器可能会有这些东西
   if (qualifiers != null) {
      for (Class<? extends Annotation> qualifier : qualifiers) {
         if (Primary.class == qualifier) {
            // 如果配置@Primary注解,则设置当前Bean为自动装配autowire时首选bean
            abd.setPrimary(true);
         }
         else if (Lazy.class == qualifier) {
            //设置当前bean为延迟加载
            abd.setLazyInit(true);
         }
         else {
            //其他注解,则添加到abd结构中
            abd.addQualifier(new AutowireCandidateQualifier(qualifier));
         }
      }
   }
   if (customizers != null) {
      for (BeanDefinitionCustomizer customizer : customizers) {
         customizer.customize(abd);
      }
   }
      //根据beanName和bean定义信息封装一个beanhold,heanhold其实就是一个 beanname和BeanDefinition的封装
   BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(abd, beanName);
   definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
   BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, this.registry);
}

       上面的代码就是将配置类解析成一个 BeanDefinition,其他没有什么可分析的。

     Spring 执行到配置类解析完成,BeanDefinitionMap 中一共有6个 BeanDefinition,有5个是 Spring 内置的 BeanDefinition,还有一个是我们的配置类。5个BeanDefinition分别是:

        01 ConfigurationClassPostProcessor:配置类解析器,后面扫描类

        02 EventListenerMethodProcessor、DefaultEventListenerFactory:事件监听器

        03 AutowiredAnnotationBeanPostProcessor:@AutoWired依赖注入

      04 CommonAnnotationBeanPostProcessor:@Resource依赖注入,生命周期回调。

 

2. refresh 

        下面来简单分析下 refresh 过程。

public void refresh() throws BeansException, IllegalStateException {
   synchronized (this.startupShutdownMonitor) {
      // Prepare this context for refreshing.
      //容器启动前准备工作,也就是设置容器当前的状态和记录启动开始时间以及初始化资源数据以及验证下我们需要验证的一些资源key是否存在
      prepareRefresh();

      // Tell the subclass to refresh the internal bean factory.
      /**获取一个默认的工厂,这个工厂在我们的构造实例化的时候就一个创建了一个默认的工厂
       * 这个工厂非常重要,我们spring的执行的开始阶段是先暴露一个工厂,这个工厂里面包括了spring
       * 之后执行的所有东西,其中我们的加了组件注解(如@Component @Reponsity等)的类,都会被扫描到
       * 而扫描到的类,目前还不是Bean,spring扫描到过后将它们变成Bd,一般是AnnotatedBeanDefinition
       * 也就是注解BD,然后将这个BD放入默认工厂DefaultListableBeanFactory中的bdmap
       */
      ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

      // Prepare the bean factory for use in this context.
      /**
       * 这里是准备工厂:
       * 1.设置BeanDefinition的类加载器
       * 2.设置spring容器默认的类型转换器
       * 3.设置spring解析el表达式的解析器
       * 4.添加一个Bean的后置处理器ApplicationContextAwareProcessor
       * 5.将bean工厂的一些类,比如ApplicationContext直接注册到单例池中
       * 6.去除一些在byType或byName的时候需要过滤掉的bean(spring在依赖注入的时候会先在这些默认注册的bean中进行byType查找。如果找到了,就加入到列表中,简单来说就是比如你在bean中依赖注入了ApplicationContext context,那么spring会在默认注册的这些bean中找到然后进行注册)。
       * 7.将系统的环境信息、spring容器的启动环境信息、操作系统的环境信息直接注册成一个单例的bean
       */
      prepareBeanFactory(beanFactory);

      try {
         // Allows post-processing of the bean factory in context subclasses.
         //这里是一个空壳方法,spring目前还没有对他进行实现,但是我们通过名字postProcessBeanFactory
         //其实后续可以添加一些用户自定义的或者默认的一些特殊的后置处理器工程到beanFactory中去
         //这个方法是留给子类去实现的
         postProcessBeanFactory(beanFactory);

         // Invoke factory processors registered as beans in the context.
         //这里就是调用后置处理器,程序执行到这里为止,还没有添加的有我们用户自定义的后置处理器,但是
         //spring添加了自己默认的后置处理器,比如ConfigurationClassBeanFactory,这个类用来解析和扫描
         //应用组件,比如加了@Compent或者相对的其他组件的类,将它们转成bd放入工厂的bdmap中
         //这里做的事情有:
         // 1.将我们标记为容器单例类扫描成bd放入bdmap
         // 2.处理@Import注解
         //3.如果我们的配置类是@Configuration的,那么会生成这个配置类的CGLIB代理类,如果没有加@Configuration,则就是一个普通Bean
         invokeBeanFactoryPostProcessors(beanFactory);

         // Register bean processors that intercept bean creation.
         /**
          * 上面的一个方法invokeBeanFactoryPostProcessors是将我们系统中所有符合条件的普通类都扫描成了一个BeanDefinition
          * 并且放入到了beanDefinitionMap中,包括业务的bean,ban的后置处理器、bean工厂的后置处理器等等
          * 也就是说所有的BeanDefinition都已经扫描完成了,下面这个方法做的事情就是从beanDefinitionMap中
          * 取出bean的后置处理器然后 放入到后置处理器的缓存列表中
          * 当然了,这里不仅仅去取出后置处理器,还进行了一些排序,具体怎么排序,可以看下里面的操作
          */
         registerBeanPostProcessors(beanFactory);

         // Initialize message source for this context.
         //初始化国际化资源信息

         initMessageSource();

         // Initialize event multicaster for this context.
         //事件注册器初始化
         initApplicationEventMulticaster();

         // Initialize other special beans in specific context subclasses.
         //空壳方法,留给子类实现
         onRefresh();

         // Check for listener beans and register them.
         //将容器中和BeanDefinitionMap中的监听器添加到事件监听器中
         registerListeners();

         // Instantiate all remaining (non-lazy-init) singletons.
         /**
          * 创建单例池,将容器中非懒加载的Bean,单例bean创建对象放入单例池中,包括容器的依赖注入
          */
         finishBeanFactoryInitialization(beanFactory);

         // Last step: publish corresponding event.
         //容器启动过后,发布事件
         finishRefresh();
      }

      catch (BeansException ex) {
         if (logger.isWarnEnabled()) {
            logger.warn("Exception encountered during context initialization - " +
                  "cancelling refresh attempt: " + ex);
         }

         // Destroy already created singletons to avoid dangling resources.
         destroyBeans();

         // Reset 'active' flag.
         cancelRefresh(ex);

         // Propagate exception to caller.
         throw ex;
      }

      finally {
         // Reset common introspection caches in Spring's core, since we
         // might not ever need metadata for singleton beans anymore...
         resetCommonCaches();
      }
   }
}

     上面的过程总体来说就是:工厂刷新的实现,Applicationcontext 是不支持重复刷新的。这里面所做的事情就比较多了,spring的容器启动也就是调用了refresh,

        1. 容器启动前的准备工作;

        2. 准备工厂,初始化bean工厂,设置一些后置处理器还有一些参数设置;

        3. 通过bean工厂的后置处理器去处理配置类信息,将配置类配置的路径下的所有普 通类扫描成BeanDefinition;

        4. 注册系统中的bean工厂后置处理器到后置处理器列表中;

        5. 初始化资源;

        6. 事件和监听的发布处理;

        7. 最后创建spirng的核心单例池;

        8. 容器启动完成,发布事件。

Spring 容器启动整体流程

        下面为 Spring 容器启动整体流程图,

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值