深入挖掘Spring系列 -- 依赖查找背后的细节

依赖查找是什么

使用过Spring框架的同学应该都清楚,Spring会将我们所需要使用到的Bean按照一定规则存储到一个应用上下文中(ApplicationContext)。所谓的依赖查找就是根据规则从Spring容器中获取对应的Bean。
流程有点类似于下图:
在这里插入图片描述
客户端程序希望获取一个BeanA,那么就向Spring中发起一个请求,从容器中获取对应的BeanA。
其实在Spring发明之前,jdk内部也有类似依赖查找的这种功能。下边我们来看看这几个类的属性。

java.beans.beancontext.BeanContext
java.beans.beancontext.BeanContextServices

在这两个类里面存有着和Spring设计类似的一些管理bean的基本接口和功能,这一点上可以看出,其实Spring的依赖查找设计是有对Jdk内部实现做了一定借鉴的。

依赖查找归纳
按照自己的经验和一些资料帮助,下边大体将依赖查找划分为了三种类型:

  • 单一类型查找
  • 集合类型查找
  • 层次类型查找

接下来便是相关代码的实战案例,通过代码实践来深入理解这些依赖查找。

Spring内部的单一类型查找

关于spring内部的单一查找这里我将常见的几种类型给大家罗列出来。

Object getBean(String name) throws BeansException;

<T> T getBean(Class<T> requiredType) throws BeansException;

<T> T getBean(String name, Class<T> requiredType) throws BeansExceptio

在高版本的Spring中还提供了对于Spring内部Bean的延迟查找功能

<T> ObjectProvider<T> getBeanProvider(Class<T> requiredType);

<T> ObjectProvider<T> getBeanProvider(ResolvableType requiredType);

ResolvableType解释下:因为jdk内部的类不仅仅只有Class类型,还有例如说Type类型,FIeld类型,所以Spring又增加了一种类型参数作为暴露的Api服务。

实践案例:

配置Bean:Pig

package org.idea.spring.look.up.factory;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @Author linhao
 * @Date created in 9:26 下午 2021/4/10
 */
@Configuration
public class Config {

    public class Pig{
        int id;

        public Pig() {
            System.out.println("this is pig");
        }

        @Override
        public String toString() {
            return "Pig{" +
                    "id=" + id +
                    '}';
        }
    }

    @Bean(name = "pig")
    public Pig getPig(){
        return new Pig();
    }
}

测试入口:

package org.idea.spring.look.up;

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;


/**
 * @Author linhao
 * @Date created in 9:02 下午 2021/4/10
 */
public class SpringSingleLookUpDemo {

    @Bean
    public String HelloWorld(){ //bean 注解里面的name或者value如果没有定义的话,这里默认就是 "helloWorld"
        System.out.println("THIS IS INIT");
        return "Hello.World";
    }



    public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext =new AnnotationConfigApplicationContext();
        //此时相当于声明当前这个类是一个配置类,所以不需要进行额外注解的声明
        applicationContext.register(SpringSingleLookUpDemo.class);
        applicationContext.refresh();

        //单一查找的一种实现方式
        String bean = (String) applicationContext.getBean("Pig");
        System.out.println(bean);
        //spring 4.1 高版本中的一种实现方式 这里只是支持单一查找类型
        ObjectProvider<Config.Pig> objectProvider = applicationContext.getBeanProvider(Config.Pig.class);
        System.out.println(objectProvider.getObject());
        applicationContext.close();
    }
}

Spring内部的集合类型查找

根据 Bean 类型查找
获取同类型 Bean 名称列表

String[] getBeanNamesForType(@Nullable Class<?> type);

String[] getBeanNamesForType(@Nullable Class<?> type, boolean includeNonSingletons, boolean allowEagerInit);

获取同类型 Bean 实例列表

getBeansOfType(Class) 以及重载方法 //不建议使用
通过注解类型查找

//Spring 3.0 - 获取标注类型 Bean 名称列表
String[] getBeanNamesForAnnotation(Class<? extends Annotation> annotationType);

//Spring 3.0 - 获取标注类型 Bean 实例列表
Map<String, Object> getBeansWithAnnotation(Class<? extends Annotation> annotationType) throws BeansException;

//Spring 3.0 - 获取指定名称+标注类型 Bean 实例
<A extends Annotation> A findAnnotationOnBean(String beanName, Class<A> annotationType)
      throws NoSuchBeanDefinitionException;

实验案例:

package org.idea.spring.look.up;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
 * @Author linhao
 * @Date created in 10:03 下午 2021/4/10
 */
class Text {
    int id;
    public Text(int id) {
        System.out.println("init");
        this.id = id;
    }
    public Text(){
        System.out.println("no arg init");
    }
    @Override
    public String toString() {
        return "Text{" +
                "id=" + id +
                '}';
    }
}
class A {
   void doJob(){
       System.out.println("do  Job  A");
   }
}
class B extends A {
    @Override
    void doJob() {
        System.out.println("do Job B");
    }
}
public class SpringListableLookUpDemo {
    @Bean(name = "text")
    public Text getText() {
        return new Text(1);
    }
    public static void main(String[] args) {
        AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext();
        annotationConfigApplicationContext.register(SpringListableLookUpDemo.class);
        annotationConfigApplicationContext.register(B.class);
        annotationConfigApplicationContext.register(A.class);
        annotationConfigApplicationContext.refresh();
        ListableBeanFactory listableBeanFactory = annotationConfigApplicationContext;
        listableBeanFactory.getBeansWithAnnotation(Component.class);
        Text text = (Text) listableBeanFactory.getBean("text");
        Map<String, Text> map = listableBeanFactory.getBeansOfType(Text.class);
        System.out.println(map);
        //如果查找的是一个父类,会顺便将其子类也查询出来
        String[] beanNameArr = listableBeanFactory.getBeanNamesForType(A.class);
        for (String beanName : beanNameArr) {
            System.out.println(beanName);
        }
    }
}

在这段代码程序中,有几个点需要注意一下:bean的依赖查找一般是需要结合BeanDefinition来使用,这一步的操作是发生在了Spring容器的上下文启动之前进行的。
对于判断某个bean是否存在,建议可以使用(一般是借助BeanDefinition这种关于类的元信息辅助类判断,此时可以避免提早实现bean的初始化)
例如:

listableBeanFactory.getBeanNamesForType(Text.class);

而不是以下方法(可能会涉及到bean的提前初始化操作)

listableBeanFactory.getBeansOfType(Text.class);

getBeanNamesForType其中的底层逻辑是从BeanDefinitionNames 集合中去遍历查询,并不会触发bean的初始化步骤;
在这里插入图片描述
getBeansOfType的底层内部可能会涉及到bean的初始化操作。
图片
不过如果在使用这两个方法之前,Spring容器都已经进行了上下文的初始化,那我觉得其实用谁都可以。

Spring内部的层次性查找

Spring内部的上下文其实是分有等级的,来看看下边这张图:

在这里插入图片描述

在Spring容器内部,A容器内部含有Bean-A,B容器继承了A容器,那么按道理来说,B容器也应该有权利获取到A容器内部的Bean。这样的好处在于减少了额外存储Bean的开销。

接下来我们通过实战案例来深入理解下层次性Spring容器是个怎么样的存在。

通过这段代码来建立一个父亲级别Spring容器:

package org.idea.spring.look.up.factory;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;


/**
 * 父类ioc容器 这里面的ioc容器只包含有ParentBean这个类
 *
 * @Author linhao
 * @Date created in 8:46 上午 2021/4/11
 */
public class ParentIocContainer {


    public static AnnotationConfigApplicationContext applicationContext = null;

    class ParentBean {
        int id;

        public ParentBean(){
            System.out.println("this is no arg init");
        }

        @Override
        public String toString() {
            return "ParentBean{" +
                    "id=" + id +
                    '}';
        }
    }

    public ApplicationContext getAndStartApplicationContext(){
       applicationContext = new AnnotationConfigApplicationContext();
       applicationContext.register(ParentIocContainer.class);
       //需要支持无参构造函数
       applicationContext.registerBean("parentBean",ParentBean.class);
       applicationContext.refresh();
       return applicationContext;
    }

    public static void main(String[] args) {
        ParentIocContainer parentIocContainer = new ParentIocContainer();
        ApplicationContext applicationContext = parentIocContainer.getAndStartApplicationContext();
        String[] str = applicationContext.getBeanNamesForType(ParentBean.class);
        for (String beanName : str) {
            System.out.println(beanName);
        }
    }
}

然后通过一个案例代码分析层次级别的bean查找案例:

package org.idea.spring.look.up.factory;

import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.HierarchicalBeanFactory;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

/**
 * 层次性的依赖查找 {@link org.springframework.beans.factory.HierarchicalBeanFactory}
 *
 * @Author linhao
 * @Date created in 10:55 下午 2021/4/10
 */
public class SpringHierarchicalLookUpDemo {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
        applicationContext.register(SpringHierarchicalLookUpDemo.class);
        applicationContext.refresh();

        ParentIocContainer parentIocContainer = new ParentIocContainer();
        ApplicationContext parentApplicationContext = parentIocContainer.getAndStartApplicationContext();
        // ConfigurableListableBeanFactory -> ConfigurableBeanFactory -> HierarchicalBeanFactory
        ConfigurableListableBeanFactory configurableListableBeanFactory = applicationContext.getBeanFactory();
        System.out.println("此时的父类BeanFactory为:" + configurableListableBeanFactory.getParentBeanFactory());
        configurableListableBeanFactory.setParentBeanFactory(parentApplicationContext);
        System.out.println("此时的父类BeanFactory为:" + configurableListableBeanFactory.getParentBeanFactory());
        ParentIocContainer.ParentBean parentBean = (ParentIocContainer.ParentBean) configurableListableBeanFactory.getBean("parentBean");
        System.out.println(parentBean);
        isContainedBean(configurableListableBeanFactory, "parentBean");
        displayContainsBean(configurableListableBeanFactory, "parentBean");
    }

  /**
     * 这里是子类可以获取自己和父类层次内部的bean,如果是使用containsLocalBean方法的话就只能判断当前所在层次的容器上下文
     *
     * @param beanFactory
     * @param beanName
     */
    public static void isContainedBean(HierarchicalBeanFactory beanFactory, String beanName) {
        System.out.println("getBean is " + beanFactory.getBean(beanName));
        System.out.printf("contained is [%s] ,beanFactory is [%s],beanName is [%s]\n", beanFactory.containsLocalBean(beanName), beanFactory, beanName);
    }


    /**
     * 查找关于父类容器内部的bean
     *
     * @param beanFactory
     * @param beanName
     */
    private static void displayContainsBean(HierarchicalBeanFactory beanFactory, String beanName) {
        System.out.printf("contained is [%s] ,beanFactory is [%s],beanName is [%s]\n", isContainedBeanInHoldApplication(beanFactory, beanName), beanFactory, beanName);
    }

    /**
     * 使用递归判断 -- 自上到下判断父类容器是否含有bean
     *
     * @param hierarchicalBeanFactory
     * @param beanName
     * @return
     */
    public static boolean isContainedBeanInHoldApplication(HierarchicalBeanFactory hierarchicalBeanFactory, String beanName) {
        BeanFactory parentBeanFactory = hierarchicalBeanFactory.getParentBeanFactory();
        if (parentBeanFactory instanceof HierarchicalBeanFactory) {
            HierarchicalBeanFactory parentHierarchicalBeanFactory = HierarchicalBeanFactory.class.cast(parentBeanFactory);
            if (isContainedBeanInHoldApplication(parentHierarchicalBeanFactory, beanName)) {
                return true;
            }
        }
        return hierarchicalBeanFactory.containsBean(beanName);
    }
}

代码里面有几个点我这里做些细节剖析:
ConfigurableListableBeanFactory的类结构图设计如下:
在这里插入图片描述
从名字可以看出,这是一个BeanFactory的子类,而且支持扩展(Configurable)和集合查找(Listable),同时还继承了层次性(HierarchicalBeanFactory)的特点。
在这里插入图片描述
在ConfigurableBeanFactory中支持这么一个功能:

void setParentBeanFactory(BeanFactory parentBeanFactory) throws IllegalStateException;

可以手动设置当前的BeanFactory的父类BeanFactory。
层次性的BeanFactory具有这么两个特点:
在这里插入图片描述从他的基本接口设计就可以看出两点:

  1. 支持获取当前Spring容器的父容器
  2. 支持判断当前容器所处的层级是否包含某个Bean

在案例代码中的对于父类工厂的递归遍历设计思路实际上参考了Spring5中的org.springframework.beans.factory.BeanFactoryUtils#beanNamesForTypeIncludingAncestors(org.springframework.beans.factory.ListableBeanFactory, java.lang.Class<?>) 的设计思路,设计目的也是为了对父类进行递归判断某些Bean是否存在。

什么场景下会使用到层次性的HierarchicalBeanFactory?

比如在 Spring MVC 中,展现层 Bean 位于一个子容器中,而业务层和持久层的 Bean 位于父容器中。这样,展现层 Bean 就可以引用业务层和持久层的 Bean,而业务层和持久层的 Bean 则看不到展现层的 Bean。

Spring内部的延迟查找

Bean 延迟依赖查找接口

org.springframework.beans.factory.ObjectFactory
org.springframework.beans.factory.ObjectProvider

Spring 5 对 Java 8 特性扩展–函数式接口

getIfAvailable(Supplier)
ifAvailable(Consumer)
Stream 扩展 - stream()
getIfAvailable

我们先在代码中通过注解定义一个 User

 @Bean
    public User user() {
        return User.createUser("ifAvailable-user");
    }

下面方法中User::createUser 只是提供兜底实现,当获取的对象为空时不会出现异常抛出。

    private static void lookupGetIfAvailable(AnnotationConfigApplicationContext applicationContext) {
        ObjectProvider<User> beanProvider = applicationContext.getBeanProvider(User.class);
//        User ifAvailable = beanProvider.getIfAvailable(()->User.createUser());
        User ifAvailable = beanProvider.getIfAvailable(User::createUser);
        System.out.println(ifAvailable);
    }

ifAvailable
通过 Consumer 的方式消费掉,我们这里直接打印

private static void lookupIfAvailable(AnnotationConfigApplicationContext applicationContext) {
        ObjectProvider<User> beanProvider = applicationContext.getBeanProvider(User.class);
        beanProvider.ifAvailable(System.out::println);
    }

Stream 扩展

    private static void lookupByStreamOps(AnnotationConfigApplicationContext applicationContext) {
        ObjectProvider<String> beanProvider = applicationContext.getBeanProvider(String.class);
//        Iterable<String> stringIterable = beanProvider;
//        for (String str : stringIterable) {
//            System.out.println(str);
//        }
        beanProvider.stream().forEach(System.out::println);
    }

完整的代码案例:

package org.idea.spring.look.up.lazy;

import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;

import java.util.Iterator;

/**
 * @Author linhao
 * @Date created in 3:58 下午 2021/4/11
 */
public class LazyLoadBean {

    static class Bus {
        int id;

        @Override
        public String toString() {
            return "Bus{" +
                    "id=" + id +
                    '}';
        }

        public Bus(int id) {
            this.id = id;
        }

        public static Bus buildBus(){
            return new Bus(1);
        }
    }

    @Bean
    @Primary
    public String helloWorld(){
        return "hello world";
    }

    @Bean
    public String message(){
        return "message";
    }

    public static void main(String[] args) {
        AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext();
        annotationConfigApplicationContext.register(LazyLoadBean.class);
//        annotationConfigApplicationContext.register(Bus.class);
        annotationConfigApplicationContext.refresh();
        // ObjectProvider 可以不需要通过initLazy这种配置方式处理懒加载问题,借助程序实现
        lookupByObjectProvider(annotationConfigApplicationContext);
        lookUpByPrimary(annotationConfigApplicationContext);
        lookUpIter(annotationConfigApplicationContext);
        lookUpStream(annotationConfigApplicationContext);
    }

    public static void lookupByObjectProvider(AnnotationConfigApplicationContext annotationConfigApplicationContext){
        ObjectProvider<Bus> objectProvider = annotationConfigApplicationContext.getBeanProvider(Bus.class);
        //类型安全的策略
        Bus bus = objectProvider.getIfAvailable(Bus::buildBus);
        System.out.println(bus.toString());
    }

    public static void lookUpByPrimary(AnnotationConfigApplicationContext annotationConfigApplicationContext){
        ObjectProvider<String> objectProvider = annotationConfigApplicationContext.getBeanProvider(String.class);
        System.out.println(objectProvider.getObject());
    }

    public static void lookUpIter(AnnotationConfigApplicationContext annotationConfigApplicationContext) {
        ObjectProvider<String> objectProvider = annotationConfigApplicationContext.getBeanProvider(String.class);
        Iterator<String> iterator = objectProvider.iterator();
        while(iterator.hasNext()) {
            String item = iterator.next();
            System.out.println(item);
        }
    }

    public static void lookUpStream(AnnotationConfigApplicationContext annotationConfigApplicationContext) {
        ObjectProvider<String> objectProvider = annotationConfigApplicationContext.getBeanProvider(String.class);
        objectProvider.stream().forEach(System.out::println);
    }
}

依赖查找安全性问题

关于Spring内部的bean依赖查找我们上边基本上已经梳理完毕了,主要分为了Bean的单一查找,集合查找,层次性查找三种类型。那么在使用Spring内部的Api进行bean查找的时候,如果没有对应的bean,内部框架是否会有异常抛出的情况发生,这一点就需要另外实践分析。
以下是一个实践的代码案例供大家参考:

package org.idea.spring.look.up.safe;
import org.apache.catalina.User;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
/**
 * bean的依赖查找中安全性对比案例
 *
 * @Author linhao
 * @Date created in 8:58 上午 2021/4/12
 */
public class BeanLookUpSafe {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
        applicationContext.register(BeanLookUpSafe.class);
        applicationContext.refresh();
        displayBeanFactoryGetBean(applicationContext);
        displayObjectFactoryGetBean(applicationContext);
        displayObjectProviderGetIfAvailable(applicationContext);
        displayListableBeanFactoryGetBean(applicationContext);
        displayObjectFactoryGetBeanStream(applicationContext);
        applicationContext.close();
    }

    private static void displayObjectFactoryGetBeanStream(ApplicationContext applicationContext) {
        printBeansException("displayObjectFactoryGetBeanStream",() ->{
            ObjectProvider<User> userObjectProvider = applicationContext.getBeanProvider(User.class);
            userObjectProvider.stream().forEach(System.out::println);
        });
    }

    private static void displayListableBeanFactoryGetBean(ListableBeanFactory listableBeanFactory) {
        printBeansException("displayListableBeanFactoryGetBean",() ->{listableBeanFactory.getBeansOfType(User.class);});
    }

    private static void displayObjectProviderGetIfAvailable(AnnotationConfigApplicationContext applicationContext) {
        ObjectProvider<User> objectProvider = applicationContext.getBeanProvider(User.class);
        printBeansException("displayObjectProviderGetIfAvailable",() ->{objectProvider.getIfAvailable();});
    }

    private static void displayObjectFactoryGetBean(BeanFactory beanFactory) {
        ObjectFactory<User> objectFactory = beanFactory.getBeanProvider(User.class);
        printBeansException("displayObjectFactoryGetBean",()->{ objectFactory.getObject();});
    }

    private static void displayBeanFactoryGetBean(BeanFactory beanFactory) {
        printBeansException("displayBeanFactoryGetBean",()->{ beanFactory.getBean(User.class);});
    }

    private static void printBeansException(String source,Runnable runnable){
        try {
            System.err.println("==========================");
            System.err.println("Source is from :" + source);
            runnable.run();
        }catch (BeansException exception){
            exception.printStackTrace();
        }
    }
}

依赖查找可能抛出的异常
这里我整理了以下集中常见的Bean异常:

public static void main(String[] args) {
    //bean不在ioc容器中 继承了BeansException
    NoSuchBeanDefinitionException noSuchBeanDefinitionException;
    // 继承了NoSuchBeanDefinitionException的运行时候异常 通常只需要对bean标注@Primary注解即可
    // 但是这种设计会有语义性设计的不足,例如无法精确判断是没有该bean还是因为bean过多导致的
    NoUniqueBeanDefinitionException noUniqueBeanDefinitionException;
    //初始化的时候会抛出异常
    BeanInstantiationException beanInstantiationException;
    //初始化进行方法回调的时候会有异常抛出
    BeanCreationException beanCreationException;
    //一些常见的xml异常,例如说某些xml解析异常
    BeanDefinitionStoreException beanDefinitionStoreException;
}

为了更好地进行实践,这里贴了几个实践出来的代码供大家进行分析思考:
BeanInstantiationException
bean在进行初始化过程中出现的异常,案例代码如下:

package org.idea.spring.look.up.exception;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
/**
 * @Author linhao
 * @Date created in 9:59 下午 2021/4/12
 */
public class BeanInstantiationExceptionDemo {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext();
        //注册一个 BeanDefinitionBuilder
        BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(CharSequence.class);
        //Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [java.lang.CharSequence]: Specified class is an interface
        annotationConfigApplicationContext.registerBeanDefinition("error bean",beanDefinitionBuilder.getBeanDefinition());
        annotationConfigApplicationContext.refresh();
        annotationConfigApplicationContext.close();
    }
}

BeanDefinitionStoreException
bean已经初始化结束了,在初始化回调接口环节出现的异常,案例代码如下:

package org.idea.spring.look.up.exception;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.CommonAnnotationBeanPostProcessor;

import javax.annotation.PostConstruct;

/**
 * @Author linhao
 * @Date created in 10:01 下午 2021/4/12
 */
public class BeanCreationExceptionDemo {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext annotationConfigApplicationContext  = new AnnotationConfigApplicationContext();
        annotationConfigApplicationContext.register(POJO.class);
        annotationConfigApplicationContext.refresh();
        annotationConfigApplicationContext.close();
    }



    static class POJO implements InitializingBean {

        //这里的基于注解的回调是通过 CommonAnnotationBeanPostProcessor 来实现的
        @PostConstruct
        public void init(){
            throw  new RuntimeException(" init has error");
        }

        @Override
        public void afterPropertiesSet() throws Exception {
            throw  new RuntimeException(" afterPropertiesSet : has error");
        }
    }
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值