Spring Bean的生命周期及各个阶段的回调

本文已被收录至GitHub https://github.com/JavaLiuTongXue/JavaCoding 如果想要获取更多的干货文章,关注微信公众号:不会说话的刘同学

Spring在日常开发中用得比较多,对于Bean的创建及使用也非常的熟练,但是Bean在创建的过程中也会涉及到生命周期及各个阶段的回调,本片文章就来说说Spring Bean的生命周期及各个阶段的回调,看完绝对在日后的面试中或开发中有所帮助

Bean的配置阶段

Bean的配置阶段是我们在配置资源文件里配置的一些Bean的元数据信息,这也是我们使用Spring IOC容器管理Bean的第一步,一般我们通常都会在XML文件里或JavaConfig配置类里配置一些Bean的元数据信息,然后再通过 ApplicationContext 读取这些资源文件然后创建我们想要的Bean对象,当然除了XML、JavaConfig,还有YML文件、Properties文件等,但是用的比较少

对应的XML与JavaConfig配置方式分别如下:

<bean id="user" class="com.spring.ioc.dependency.domain.User" />
@Bean
public User user(){
    return new User();
}

现在项目开发中XML配置的也比较少了,现在比较多的就是通过JavaConfig方式类进行Bean的元信息配置

关于配置阶段这里就不做过多的说明,一般在配置的过程中也比较的简单

Bean的解析阶段

对于XML配置的Bean信息,我们可以通过 ClassPathXmlApplicationContext 创建并获取Bean对象;对于JavaConfig配置Bean信息,则是通过 AnnotationConfigApplicationContext 来创建并获取Bean对象

虽然从代码上看都是通过 ApplicationContext 来读取资源,但是实际 ApplicationContext 并不直接处理资源,而是直接将资源文件交由 BeanDefinitionReader 来进行处理,它们之间的处理层级关系可以如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YPN2mhE9-1668350445569)(https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3978b67ad2b0485eb002cd3ec76a1a47~tplv-k3u1fbpfcp-watermark.image?)]

XmlBeanDefinitionReader

XmlBeanDefinitionReader 主要是用来解析 XML 资源文件,将XML资源文件里配置的 Bean 元数据信息解析成BeanDefinition

XmlBeanDefinitionReader 主要就提供了一个构造方式,这个构造方法里也需要传入 BeanDefinitionRegistry ,这也就说明了 XmlBeanDefinitionReader 除了读取并解析XML资源文件外,另外还进行了注册

public XmlBeanDefinitionReader(BeanDefinitionRegistry registry) {
   super(registry);
}

另外 XmlBeanDefinitionReader 也提供了四个 loadBeanDefinitions 方法,这个方法主要就是用来解析XML资源,方法参数传参就需要传入目标资源的路径

public int loadBeanDefinitions(Resource resource):需要传入通过目标资源路径构造的Resource对象

public int loadBeanDefinitions(EncodedResource encodedResource):需要传入通过目标资源路径构造的EncodedResource对象,这个方法跟第一个方法主要区别就在于第一个方法内部其实还是通过调用这个方法进行加载

public int loadBeanDefinitions(InputSource inputSource):需要传入通过目标资源路径构造一个InputSource对象

public int loadBeanDefinitions(InputSource inputSource, @Nullable String resourceDescription):除了需要传入通过目标资源路径构造的InputSource对象之外,还需要传入 resourceDescription 参数,这个参数相当于是资源描述

XmlBeanDefinitionReader 继承了 AbstractBeanDefinitionReader 抽象类,这个抽象类里也提供了四个加载方法,分别如下:

public int loadBeanDefinitions(Resource… resources):参数是一个可变数组,也就是可以传入多个 Resource 对象

public int loadBeanDefinitions(String location):参数是一个资源路径

public int loadBeanDefinitions(String location, @Nullable Set< Resource> actualResources):除了资源路径外,还可以传入多个 Resource 对象

public int loadBeanDefinitions(String… locations):参数也是个可变数组,可以传入多个资源路径

既然 AbstractBeanDefinitionReader 已经有了四个加载资源的方法,为啥其子类还要写四个加载资源的方法呢,这里主要还是为了有更多的选择吧,这八个方法传入的参数都不一样,对应不同的使用场景

我们在使用的过程中,主要使用 AbstractBeanDefinitionReader 类里的 loadBeanDefinitions(String… locations) 方法

在下面的代码中就很好的使用了 XmlBeanDefinitionReader 来读取 XML 文件

public static void main(String[] args) {
    DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
    BeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory);
    int beanNum = beanDefinitionReader.loadBeanDefinitions("ioc-application.xml");
    User bean = beanFactory.getBean(User.class);
    System.out.println(bean);
}

这里 XmlBeanDefinitionReader 的构造参数为啥要传入DefaultListableBeanFactory呢

这是因为 XmlBeanDefinitionReader 的构造方法需要传入一个 BeanDefinitionRegistry 对象,而 DefaultListableBeanFactory 是实现了 BeanDefinitionRegistry 接口,因此可以把 DefaultListableBeanFactory 当作是一个 BeanDefinitionRegistry 第实现传入

我们通过断点调试发现 loadBeanDefinitions 方法最终会调用 XmlBeanDefinitionReader 里的 doLoadBeanDefinitions 方法,我们所有加载资源并注册BeanDefintion都是通过这个方法来完成的

protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource):加载XML文件并注册BeanDefinition

而 doLoadBeanDefinitions 方法内部直接调用registerBeanDefinitions(Document doc, Resource resource)

public int registerBeanDefinitions(Document doc, Resource resource):注册BeanDefinition

我们可以具体看下这个方法

public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
   BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
   int countBefore = getRegistry().getBeanDefinitionCount();
   documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
   return getRegistry().getBeanDefinitionCount() - countBefore;
}

这个方法的内部构造了一个 BeanDefinitionDocumentReader ,这个类是个接口,它有个具体的实现类—DefaultBeanDefinitionDocumentReader

DefaultBeanDefinitionDocumentReader

兜兜转转最终调用了这个类进行解析,这个类提供了一个 parseBeanDefinitions 方法,在这个方法的内部进一步调用了 parseDefaultElement 方法

我们可以看下这个 parseDefaultElement 方法

private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {
   if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) {
      importBeanDefinitionResource(ele);
   }
   else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) {
      processAliasRegistration(ele);
   }
   else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {
      // 解析Bean节点
      processBeanDefinition(ele, delegate);
   }
   else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) {
      doRegisterBeanDefinitions(ele);
   }
}

这个方法内部再一次调用了 processBeanDefinition 方法,我们的Bean节点的解析就是在这个方法内部进行的

我们可以看下这个方法

protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {
   BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);
   if (bdHolder != null) {
      bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);
      try {
         // Register the final decorated instance.
         BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());
      }
      catch (BeanDefinitionStoreException ex) {
         getReaderContext().error("Failed to register bean definition with name '" +
               bdHolder.getBeanName() + "'", ele, ex);
      }
      // Send registration event.
      getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder));
   }}

在这个方法的第一行代码就是通过 BeanDefinitionParserDelegate 这个类对Bean节点解析成 BeanDefinitionHolder ,为啥是BeanDefnitionHolder,而不是BeanDefnition呢,BeanDefnitionHolder就是对BeanDefinition进行了一次包装,我们可以看下内部属性

在这里插入图片描述

BeanDefinitionParserDelegate

BeanDefinitionParserDelegate 将XML文件里Bean节点解析成BeanDefnitionHolder,这个类在解析的过程中无疑是起到了一个最为关键的作用

这个类相当于是一个XML文件的委托类,就是在读取XML文件的时候会将XML文件解析成为这样子的一个类

这个类又是在哪里初始化的呢,在上面的讲过一个 DefaultBeanDefinitionDocumentReader 这样的一个类,这个类里有个 createDelegate 方法,会通过调用这个方法来创建 BeanDefinitionParserDelegate
在这里插入图片描述

那又是在哪里调用的呢

上面讲过 XmlBeanDefinitionReader 这个类提供了一个 registerBeanDefinitions 方法,在这个方法里会调用 DefaultBeanDefinitionDocumentReader 里的 doRegisterBeanDefinitions 方法, 创建 BeanDefinitionParserDelegate 对象实例就是在这个方法里创建的
在这里插入图片描述

BeanDefinitionParserDelegate 类里的 parseBeanDefinitionElement 方法就开始创建BeanDefnition并将BeanDefiniton封装成BeanDefnitionHolder

我们可以具体看下这个方法

public BeanDefinitionHolder parseBeanDefinitionElement(Element ele, @Nullable BeanDefinition containingBean) {

    ..........
    
    // 创建BeanDefnition
   AbstractBeanDefinition beanDefinition = parseBeanDefinitionElement(ele, beanName, containingBean);
 
    .......
 
   if (beanDefinition != null) {
       // 将BeanDefnition封装成BeanDefnitionHolder
       return new BeanDefinitionHolder(beanDefinition, beanName, aliasesArray);
    }
   return null;
}

这里我只贴出了关键的代码

Bean的注册阶段

解析成BeanDefnition之外,紧接着就是开始注册,注册其实也比较简单,就是把BeanDefnition往一个Map集合里存放

我们继续回到 DefaultBeanDefinitionDocumentReade 类里的 processBeanDefinition 方法

在这个方法里解析创建完BeanDefnition之后,会调用 BeanDefinitionReaderUtils 的
registerBeanDefinition 方法

public static void registerBeanDefinition(BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry):注册BeanDefnition,方法需要传入BeanDefnitionHolder,以及一个BeanDefinitionRegistry的实例

BeanDefinitionReaderUtils

这里 registerBeanDefinition 方法传入的 BeanDefinitionRegistry 实例就是我们的DefaultListableBeanFactory,这个我在上面就是说过

我们具体来看下这个方法

public static void registerBeanDefinition(
      BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry)
      throws BeanDefinitionStoreException {
 
      ........
      
    // 调用BeanDefinitionRegistry实例的registerBeanDefinition方法注册
    registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());
    
     ........
    
}

在这个方法里调用 BeanDefinitionRegistry实例的registerBeanDefinition方法时最终会来到 DefaultListableBeanFactory 里的 registerBeanDefinition 方法

那么我们可以具体看下这个方法

public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
      throws BeanDefinitionStoreException {

   Assert.hasText(beanName, "Bean name must not be empty");
   Assert.notNull(beanDefinition, "BeanDefinition must not be null");

   if (beanDefinition instanceof AbstractBeanDefinition) {
                ......
   }
   else {
      if (hasBeanCreationStarted()) {
         ......
      } else {
         // 注册BeanDefnition 
         this.beanDefinitionMap.put(beanName, beanDefinition);
         this.beanDefinitionNames.add(beanName);
         removeManualSingletonName(beanName);
      }
      this.frozenBeanDefinitionNames = null;
   } 
   
   ........
   
}

这里我也只贴出了最为关键的代码

Bean的解析与注册我们其实可以合并为一步,在整个从XML读取到Bean的解析注册过程中为了方便理解,我使用时序图画了下大致的流程

在这里插入图片描述

大家也可以照着代码及流程图通过断点调试一下,这样会更清晰

Bean的实例化阶段

BeanDefinition注册完成之后,就要开始实例化了,BeanDefnition里保存了Bean的class对象,因此对于Bean实例化可以直接通过 class.newInstance() 方式来进行实例化

我们可以看看 BeanDefnition 里的成员属性beanClass,由于 BeanDefnition 是个接口,我们只需要看它的子类 AbstractBeanDefinition

在这里插入图片描述

那么具体又是在哪实例化的呢

InstantiationAwareBeanPostProcessor

AbstractAutowireCapableBeanFactory 里提供了个 createBean 方法,关于Bean的实例化都是在这个方法里

这里的实例化又分为实例化前、实例化、实例化后

为啥要这样分呢,主要是因为在Bean实例化阶段Spring还给我们提供了一系列的后置处理接口,我们可以通过这个后置处理接口来做一些扩展—InstantiationAwareBeanPostProcessor
在这里插入图片描述

这个接口提供了两个方法 ---- postProcessBeforeInstantiation 和postProcessAfterInitialization 方法,这两个方法分别在实例化前调用和实例化后调用

对应的 createBean 方法如下

protected Object createBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)
      throws BeanCreationException {

      ..... 
      
      // 实例化前调用后置处理器
      Object bean = resolveBeforeInstantiation(beanName, mbdToUse);
      if (bean != null) {
         return bean;
      }
      
      ......

      // 创建Bean的实列
      Object beanInstance = doCreateBean(beanName, mbdToUse, args);
      if (logger.isTraceEnabled()) {
         logger.trace("Finished creating instance of bean '" + beanName + "'");
      }
      return beanInstance;
}

这里我也只贴出来了比较关键的代码

resolveBeforeInstantiation 方法主要就是去调用 InstantiationAwareBeanPostProcessor 里的 postProcessBeforeInstantiation 方法

调用完成之后,接着就回去调用 doCreateBean 方法,这个方法就是去实例化Bean,在这个方法里在实例完成之后会调用 InstantiationAwareBeanPostProcessor 里的
postProcessBeforeInitialization

我们可以具体看下这个 doCreateBean 方法:

protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
      throws BeanCreationException {
   
   // 创建实例
   final Object bean = instanceWrapper.getWrappedInstance();
   Class<?> beanType = instanceWrapper.getWrappedClass();
   if (beanType != NullBean.class) {
      mbd.resolvedTargetType = beanType;
   }
   
  // 对Bean里的属性进行赋值
  populateBean(beanName, mbd, instanceWrapper);
  
  // 实例化之后调用  postProcessAfterInitialization、各个Aware方法回调
  exposedObject = initializeBean(beanName, exposedObject, mbd);
 
   return exposedObject;
}

postProcessAfterInitialization 方法就在 populateBean 方法里被调用

我们可以使用这个后置处理器来在实例化阶段做一些扩展

public class MyPostProcessor implements InstantiationAwareBeanPostProcessor {

   public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException {
       System.out.println("实例化前.....");
       return null;
   }

   public boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException {
       System.out.println("实例化后.....");
       return false;
   }
}

这里我自定义了一个类并实现 InstantiationAwareBeanPostProcessor 接口,实现了这个接口里的两个方法,并在这两个方法里分别打印一句话

然后需要将我们自定义的后置处理器添加在我们所创建的BeanFacotry里,如下

beanFactory.addBeanPostProcessor(new MyPostProcessor());

我们再来看看执行结果
在这里插入图片描述

Bean属性赋值阶段

在实例化完成之后,一个最重要的一步就是属性赋值,在上面的 doCreateBean 方法里调用了个属性赋值方法—populateBean

protected void populateBean(String beanName, RootBeanDefinition mbd, @Nullable BeanWrapper bw) {

   .........

   if (hasInstAwareBpps) {
      for (BeanPostProcessor bp : getBeanPostProcessors()) {
      
         .........
        
            if (pvsToUse == null) {
    
              ..........
    
             // 赋值之前调用 InstantiationAwareBeanPostProcessor 接口的postProcessPropertyValues方法
               pvsToUse = ibp.postProcessPropertyValues(pvs, filteredPds, bw.getWrappedInstance(), beanName);
               if (pvsToUse == null) {
                  return;
               }
            }
            pvs = pvsToUse;
         }
      }
   }
  
}

InstantiationAwareBeanPostProcessor

Spring也给我们提供了一个 InstantiationAwareBeanPostProcessor 接口在我们属性赋值之前做一些扩展

接口提供了个 postProcessProperties 方法,会在属性赋值前被调用

我们也可以自定义个一个类实现 InstantiationAwareBeanPostProcessor 接口,实现 postProcessProperties 方法,并在方法里打印一句话

代码如下

public class MyInstantiationAwareBeanPostProcessor implements InstantiationAwareBeanPostProcessor {
    @Override
    public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) throws BeansException {
        System.out.println("属性赋值前....");
        return null;
    }
}

然后需要将我们自定义的后置处理器添加在我们所创建的BeanFacotry里,如下

beanFactory.addBeanPostProcessor(new MyInstantiationAwareBeanPostProcessor());

我们可以继续看下运行结果
在这里插入图片描述

Sring Bean Aware接口回调阶段

Spring在赋值完之后,还会调用一系列的Aware接口,也是为了更够更好的扩展

Aware接口还是在 initializeBean 方法完成回调

BeanNameAware

BeanNameAware接口里提供了 setBeanName 方法,主要是用来获取Bean名称,我们可以继续

public interface BeanNameAware extends Aware {

   void setBeanName(String name);

}

这里我们如果要进行扩展的话就需要在XML里配置的Bean里来实现这个接口

代码如下

// 自定义Bean,一定要在XML里配置了才能生效
public class User implements BeanNameAware {

    @Override
    public void setBeanName(String name) {

    }
}

BeanClassLoaderAware

BeanClassLoaderAware接口可以用来获取 ClassLoader

public interface BeanClassLoaderAware extends Aware {

   void setBeanClassLoader(ClassLoader classLoader);

}

同样这里我们如果要进行扩展的话就需要在XML里配置的Bean里来实现这个接口,就可以直接获取到加载Bean的ClassLoader了

BeanFactoryAware

BeanFactoryAware 接口毋庸置疑肯定就是用来获取BeanFactory

public interface BeanFactoryAware extends Aware {
   void setBeanFactory(BeanFactory beanFactory) throws BeansException;
}

同样这里我们如果要进行扩展的话就需要在XML里配置的Bean里来实现这个接口,就可以直接获取到BeanFactory

Bean初始化阶段

Bean实例化完成及属性赋值完成之后,立马就要做最后的初始化工作了

这里的初始化是指在Bean实例化完以及属性赋值完之后所做的工作,这里的初始化工作同样还是在 initializeBean 方法里完成

我们可以具体看下这个方法

protected Object initializeBean(final String beanName, final Object bean, @Nullable RootBeanDefinition mbd) {
   if (System.getSecurityManager() != null) {
         ......
   } else {
      // 各个Aware接口回调
      invokeAwareMethods(beanName, bean);
   }
   Object wrappedBean = bean;
   if (mbd == null || !mbd.isSynthetic()) {
      // 初始化前调用后置处理器 BeanPostProcessor
      wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
   }

   try {
      // 初始化处理
      invokeInitMethods(beanName, wrappedBean, mbd);
   }
   catch (Throwable ex) {
   
        ......
        
   }
   if (mbd == null || !mbd.isSynthetic()) {
     // 初始化后调用后置处理器 BeanPostProcessor
      wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
   }

   return wrappedBean;
}

这里初始化阶段分为初始化前阶段、初始化阶段、初始化后阶段、初始化完成阶段,因为在初始化阶段Spring也同样给我们提供了两个后置处理器----BeanPostProcessor和SmartInitializingSingleton

BeanPostProcessor

其实上面讲的 InstantiationAwareBeanPostProcessor 接口就继承了 BeanPostProcessor

BeanPostProcessor只提供了 postProcessBeforeInitialization 和postProcessAfterInitialization 两个方法,分别在初始化前调用和初始化后调用

为了方便理解,还是用图来描述:
在这里插入图片描述

这个图可以完全和上面的那个 InstantiationAwareBeanPostProcessor 阶段那个图合并起来一起看

我们同样也可以自定义类来进行扩展

public class MyBeanPostProcessor implements BeanPostProcessor {
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        if("user".equals(beanName)){
             User user = (User) bean;
             user.setName("李四");
        }
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if("user".equals(beanName)){
            User user = (User) bean;
            user.setId("abcdefg");
        }
        return bean;
    }
}

这里两个方法里传入两个参数–Bean实例化后的对象和Bean的名字,这里传入的Bean对象是一个完整的Bean

在这样的一个后置处理器里我们可以对这个完整的Bean对象做一些操作

比如在上面代码中,我对这个已经生成User这个Bean对象做了些修改操作

然后需要将我们自定义的后置处理器添加在我们所创建的BeanFacotry里,如下

beanFactory.addBeanPostProcessor(new MyBeanPostProcessor());

添加完后看运行结果

在这里插入图片描述

SmartInitializingSingleton

在Bean初始化完成时,最后还会调用一个 SmartInitializingSingleton 接口,这个接
口只提供了一个方法 afterSingletonsInstantiated

这个接口我们如果要做扩展的时候就要有别于上面的后置处理器接口了

这里如果要做扩展的话就需要我们在XML里配置的Bean来实现这个接口了,就跟上面的Aware接口差不多

具体代码如下:

// 此Bean一定是要在XML里配置了才能生效
public class User implements SmartInitializingSingleton {
    @Override
    public void afterSingletonsInstantiated() {
          System.out.println("初始化完成了....");
    }
}

配置完成之后,我们还需要在代码的原基础加一个东西,那就是在 getBean 方法之后显式调用 preInstantiateSingletons 方法才能生效

User bean = beanFactory.getBean(User.class);
System.out.println(bean);
beanFactory.preInstantiateSingletons();

我们来看下结果

在这里插入图片描述

Bean销毁阶段

对于Bean的销毁,BeanFactory给我们提供了 destroyBean 方法

在销毁阶段,Spring也给我们提供了两个扩展接口----DestructionAwareBeanPostProcessor和DisposableBean,这个销毁接口都会在Bean对象销毁前调用

DestructionAwareBeanPostProcessor

DestructionAwareBeanPostProcessor 接口继承了 BeanPostProcessor,我们可以使用这个接口在销毁前做一些扩展

这种后置处理器跟上面的扩展都差不多

public class MyDestructionAwareBeanPostProcessor implements DestructionAwareBeanPostProcessor {
    @Override
    public void postProcessBeforeDestruction(Object bean, String beanName) throws BeansException {
        System.out.println("销毁了.........");
    }
}

然后需要将我们自定义的后置处理器添加在我们所创建的BeanFacotry里,如下:

beanFactory.addBeanPostProcessor(new MyDestructionAwareBeanPostProcessor());
User bean = beanFactory.getBean(User.class);
System.out.println(bean);
beanFactory.destroyBean("user",bean);

这里同样也需要在 getBean 方法后调用 destroyBean 方法

我们看下结果
在这里插入图片描述

DisposableBean

这个接口也跟我们之前的Aware接口差不多,在扩展的时候需要使用XML配置的Bean实现这个接口

// 此Bean一定是要在XML里配置了才能生效
public class User implements DisposableBean {
    @Override
    public void destroy() throws Exception {
        System.out.println("销毁了。。。。。");
    }
}

然后我们只需要调用 destroyBean 就行了

beanFactory.destroyBean("user",bean);
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值