spring Ioc容器详解

初识 IoC在 Spring 中,我们常说 IoC,那么什么是 IoC,IoC 又在 Spring 中起着什么样的作用呢?IoC(Inverse of Control),意为控制反转,那么这里又面临着两个问题,何为控制?何为反转?先来看一个案例。这是一个普通的 Bean 类:public class Person { private String name; private int age; public void teach(){ System.out.
摘要由CSDN通过智能技术生成

初识 IoC
在 Spring 中,我们常说 IoC,那么什么是 IoC,IoC 又在 Spring 中起着什么样的作用呢?

IoC(Inverse of Control),意为控制反转,那么这里又面临着两个问题,何为控制?何为反转?先来看一个案例。
这是一个普通的 Bean 类:

public class Person {

    private String name;
    private int age;

    public void teach(){
        System.out.println("教学");
    }

    public int getAge() {
        return age;
    }

    public String getName() {
        return name;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

现在有这样的一个需求,某学校需要招聘一位老师,为此,每位来应聘的老师都需要展示一下他的教学功底:


public static void main(String[] args) {
    Person person = new Person();
    person.setName("张三");
    person.setAge(25);
    person.teach();
    System.out.println(person);
}

而当第二位老师开始讲课时就需要重新编写所有代码:


public static void main(String[] args) {
    Person person = new Person();
    person.setName("李四");
    person.setAge(30);
    person.teach();
    System.out.println(person);
}

这样老师和教学这一活动高度的耦合在了一起,显然不是一个很好的选择,为此,我们可以定义一个接口,并通过接口执行教学活动,而具体由谁去执行就看你创建的是谁的对象,比如:


//定义接口
public interface Teach {

    void teach();
}

//定义具体的实现
public class Teacher implements Teach {

    private String name;
    private int age;

    @Override
    public void teach() {
        System.out.println("教学");
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "ZhangSan{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

现在你可以这样创建张三对象:


public static void main(String[] args) {
    Teach zs = new Teacher("zhangsan",25);
    zs.teach();
    System.out.println(zs);
}

而李四就可以这样创建:


public static void main(String[] args) {
    Teach ls = new Teacher("Lisi",25);
    ls.teach();
    System.out.println(ls);
}

这样便降低了教学活动与老师之间的耦合度,教学这一活动只需要知道是由老师来执行的,但具体是由哪位老师则是由具体的实现类决定的。

到这里,我们就可以来说说 IoC 容器的控制反转了,先说说何为控制?
控制指的是去指定哪位老师执行教学活动的过程,比如我指定张三教学:

public static void main(String[] args) {
    Teach zs = new Teacher("zhangsan",25);
    zs.teach();
    System.out.println(zs);
}

或者指定李四教学:

public static void main(String[] args) {
    Teach ls = new Teacher("Lisi",25);
    ls.teach();
    System.out.println(ls);
}

而反转指的是将这一控制权交出去,不由自己处理,什么意思呢?举个很简单的例子:
今天家里来了客人需要买些好菜,你就得带上菜篮子和钱去菜市场或者超市采购,但通过反转后,你无需再自己去买了,只需要将菜篮子往家门口一放,就会有人自动将菜放到你的篮子里。

对于程序来说,通过控制反转后,你无需再去指定究竟由哪位老师来执行教学活动,而是将这一过程交给第三方,也就是 IoC 容器来处理。

当然了,控制反转还有一个比较容易理解的名词——DI(依赖注入),对于 IoC 和 DI 的关系,IoC 是一种控制反转的思想,而 DI 是该思想的一种实现。

依赖注入
从字面意思来看,依赖注入的意思即 IoC 容器会将我们想要的依赖自动注入给我们以达到控制反转的效果,而事实也是如此,它有三种注入方式:

•构造方法注入
•属性注入
•接口注入

先来看看构造方法注入:


public class ConstructorTest {

    private Teach teach;

    public ConstructorTest(Teach teach) {
        this.teach = teach;
    }

    public void tech(){
        teach.teach();
    }
}

这里采用构造方法注入,对于 ConstructorTest 类,它并不知道具体由谁去执行 teach() 方法:


public static void main(String[] args) {
    Teach teach = new Teacher("zhangsan",26);
    ConstructorTest ct = new ConstructorTest(teach);
    ct.tech();
}

然后我们创建张三对象,并将其作为构造参数传入 ConstructorTest,运行结果:

zhangsan 教学

然后是属性注入,它也非常地简单:


public class FieldTest {

    private Teach teach;

    public void setTeach(Teach teach) {
        this.teach = teach;
    }

    public void tech(){
        teach.teach();
    }
}

通过其 setTeach()传入参数即可:


public static void main(String[] args) {
    Teach teach = new Teacher("zhangsan",26);
    FieldTest ft = new FieldTest();
    ft.setTeach(teach);
    ft.tech();
}

最后是接口注入:

public interface InterDi {
    void injectTeach(Teach teach);
}

首先需要定义一个接口,用于将参数传入,对于该接口,它是不知道具体的执行对象的:


public class InterfaceTest implements InterDi {

    private Teach teach;

    @Override
    public void injectTeach(Teach teach) {
        this.teach = teach;
    }

    public void tech(){
        teach.teach();
    }
}

然后去实现该接口,并执行 teach() 方法,它同样不知道具体的执行对象,然后:

public static void main(String[] args) {
    InterfaceTest it = new InterfaceTest();
    Teach teach = new Teacher("zhangsan",25);
    it.injectTeach(teach);
    it.tech();
}

先通过 InterfaceTest 的 injectTeach() 方法将 Teach 对象传入,然后就可以执行教学活动了。这是通过自己编写的方式实现的控制反转,下面我们将这一过程转交给 IoC 容器来处理。

新建一个 Maven 工程,并导入 Spring 的核心包:


<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>4.3.18.RELEASE</version>
</dependency>

然后创建 Spring 的配置文件,编写如下内容:

<?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">

    <bean id="person" class="com.wwj.ioc.bean.Person">
        <constructor-arg value="zhangsan"></constructor-arg>
        <constructor-arg value="25"></constructor-arg>
    </bean>
</beans>

这是通过构造方法注入一个 Bean 的属性,然后我们就可以直接从 IoC 容器中获取该对象了:


public static void main(String[] args) {
    ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml");
    Person person = (Person) context.getBean("person");
    person.teach();
}

当然还有属性注入:


<?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">

    <bean id="person" class="com.wwj.ioc.bean.Person">
        <property name="name" value="zhangsan"/>
        <property name="age" value="25"/>
    </bean>
</beans>

可以看到,在 Spring 中,我们要想获取一个对象只需要在配置文件中配置一下,然后就可以直接从容器中获取,那么它是如何做到的呢?
反射机制
这个问题涉及到 Java 的反射机制,也许眼尖的同学一眼就发现了,我们在配置文件中必须要配置一个非常重要的属性,它就是 class 属性,值为 Bean 的带包全类名,通过它我们就能够利用反射机制获取类的信息并设置类的信息。

那么首先我们就来了解一下 Java 的反射机制,看一个例子:


public static void main(String[] args) throws ClassNotFoundException {
    Class pClass = Class.forName("com.wwj.ioc.bean.Person");
    Constructor[] constructors = pClass.getConstructors();
    for (Constructor constructor : constructors) {
        System.out.println(constructor);
    }
}

运行结果:


public com.wwj.ioc.bean.Person()
public com.wwj.ioc.bean.Person(java.lang.String,int)

非常好理解,这里通过全类名获取到了 Person 的 Class 对象,然后通过 getConstructors() 方法可以获取到该类的所有构造方法,但其实私有方法是无法获取到的,所以可以取消 Java 的访问检查以获取到所有的构造方法:
而对于属性和其它方法也是可以获取到的:


public static void main(String[] args) throws ClassNotFoundException {
    Field[] fields = pClass.getFields();
    for (Field field : fields) {
        System.out.println(field);
    }
    Method[] methods = pClass.getMethods();
    for (Method method : methods) {
        System.out.println(method);
    }
}
public java.lang.String com.wwj.ioc.bean.Person.toString()
public java.lang.String com.wwj.ioc.bean.Person.getName()
public void com.wwj.ioc.bean.Person.setName(java.lang.String)
public void com.wwj.ioc.bean.Person.teach()
public void com.wwj.ioc.bean.Person.setAge(int)
public int com.wwj.ioc.bean.Person.getAge()
public final void java.lang.Object.wait() throws java.lang.InterruptedException
public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
public boolean java.lang.Object.equals(java.lang.Object)
public native int java.lang.Object.hashCode()
public final native java.lang.Class java.lang.Object.getClass()
public final native void java.lang.Object.notify()
public final native void java.lang.Object.notifyAll()

既然能够获取到类的属性、构造方法和成员方法,那么就能够通过反射创建对象并赋值,比如通过构造方法创建:


public static void main(String[] args) throws Exception {
    Class pClass = Class.forName("com.wwj.ioc.bean.Person");
    Constructor constructor = pClass.getConstructor(String.class, int.class);
    Object obj = constructor.newInstance("zhangsan",25);
    System.out.println(obj);
}

也可以直接创建对象,并通过属性赋值:


public static void main(String[] args) throws Exception {
    Class pClass = Class.forName("com.wwj.ioc.bean.Person");
    Field name = pClass.getDeclaredField("name");
    Field age = pClass.getDeclaredField("age");
    name.setAccessible(true);
    age.setAccessible(true);
    Object obj = pClass.newInstance();
    name.set(obj,"张三");
    age.set(obj,25);
    System.out.println(obj);
}

因为成员变量一般是私有的,所以这里通过 getDeclaredField() 方法获取,并 setAccessible(true) 取消 Java 的访问检查,最后调用 Field 的 set() 方法完成赋值。

巧妙的是,你在创建对象的时候,是通过对象 .setXXX() 方法对变量进行赋值,而反射刚好相反,它通过变量 .set() 方法进行赋值。

现在我们大概地了解了一下 Java 的反射机制,那么接下来的事情就非常简单了,比如这样的一段配置:

<?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">

    <bean id="person" class="com.wwj.ioc.bean.Person">
        <constructor-arg value="zhangsan"></constructor-arg>
        <constructor-arg value="25"></constructor-arg>
    </bean>
</beans>

很显然,它是通过构造方法进行的注入,所以我们可以通过 XML 解析,将里面的关键信息抽取出来,再通过反射将对象创建出来并放在一个容器中,这样就自己实现了一个非常简单的 IoC 容器,下面具体实现一下:


public static void main(String[] args) throws Exception {
    DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
    DocumentBuilder builder = factory.newDocumentBuilder();
    ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
    //获取 Spring 的配置文件
    Resource resource = resolver.getResource("classpath:spring-config.xml");
    InputStream inputStream = resource.getInputStream();
    //解析文档
    Document document = builder.parse(inputStream);
    //获取所有的 bean 节点
    NodeList beans = document.getElementsByTagName("bean");
    Node node = beans.item(0);
    //获取 bean 节点下的属性集合
    NamedNodeMap attributes = node.getAttributes();
    //获取 class 属性值
    String classValue = attributes.item(0).getNodeValue();
    System.out.println(classValue);
    //获取 id 属性值
    String idValue = attributes.item(1).getNodeValue();
    System.out.println(idValue);

    //获取 bean 节点下的子节点
    NodeList childNodes = node.getChildNodes();
    Node fChild = childNodes.item(1);
    Node sChild = childNodes.item(3);
    //获取子节点的属性值
    String fChildValue = fChild.getAttributes().item(0).getNodeValue();
    String sChildValue = sChild.getAttributes().item(0).getNodeValue();
    System.out.println(fChildValue);
    System.out.println(sChildValue);
}

这里采用了 DOM 解析的方式,看看运行结果:


com.wwj.ioc.bean.Person
person
zhangsan
25

可以看到配置文件中的信息都已经被提取出来了,那么接下来就非常简单了:


public static void main(String[] args){
    Map<String,Object> applicatioContext = new HashMap<>();
    Class objClass = Class.forName(classValue);
    //获取成员属性的类型
    Field name = objClass.getDeclaredField("name");
    Field age = objClass.getDeclaredField("age");
    Class<?> nameType = name.getType();
    Class<?> ageType = age.getType();
    //通过构造方法注入值
    Constructor constructor = objClass.getConstructor(nameType, ageType);
    Object obj = constructor.newInstance(fChildValue,Integer.valueOf(sChildValue));
    //将对象放入容器中
    applicatioContext.put(idValue,obj);

    //从容器中取出值
    Person person = (Person) applicatioContext.get("person");
    System.out.println(person);
}

这里用一个 map 集合模拟 IoC 容器,并通过反射将对象创建赋值,最后放入 map 中,这样我们就能够通过 map 获取这个对象了。

IoC 底层机制
刚才我们学习了 Java 的反射机制并自己实现了一个简易的 IoC 容器,下面一起来看看 Spring 框架中的 IoC 容器是如何运行的,以下面这段程序举例:


public static void main(String[] args) throws Exception {
    ApplicationContext context = new ClassPathXmlApplicationContext("spring-config.xml");
    Person person = (Person) context.getBean("person");
    System.out.println(person);
}

首先创建 ApplicationContext,程序会调用 ClassPathXmlApplicationContext 类的构造方法:


public ClassPathXmlApplicationContext(String configLocation) throws BeansException {
    this(new String[]{configLocation}, true, (ApplicationContext)null);
}

该方法又调用了带三个参数的构造方法:


public ClassPathXmlApplicationContext(String[] configLocations, boolean refresh, ApplicationContext parent) throws BeansException {
    super(parent);
    this.setConfigLocations(configLocations);
    if (refresh) {
        this.refresh();
    }
}

该方法又调用了 AbstractApplicationContext 类的 refresh()方法:


public void refresh() throws BeansException, IllegalStateException {
    synchronized(this.startupShutdownMonitor) {
        this.prepareRefresh();
        // 第一步
        ConfigurableListableBeanFactory beanFactory = this.obtainFreshBeanFactory();
        this.prepareBeanFactory(beanFactory);

        try {
            this.postProcessBeanFactory(beanFactory);
            //第二步
            this.invokeBeanFactoryPostProcessors(beanFactory);
            //第三步
            this.registerBeanPostProcessors(beanFactory);
            //第四步
            this.initMessageSource();
            //第五步
            this.initApplicationEventMulticaster();
            //第六步
            this.onRefresh();
            //第七步
            this.registerListeners();
            //第八步
            this.finishBeanFactoryInitialization(beanFactory);
            //第九步
            this.finishRefresh();
        } catch (BeansException var9) {
            if (this.logger.isWarnEnabled()) {
                this.logger.warn("Exception encountered during context initialization - cancelling refresh attempt: " + var9);
            }

            this.destroyBeans();
            this.cancelRefresh(var9);
            throw var9;
        } finally {
            this.resetCommonCaches();
        }

    }
}

我们来仔细看看这个方法,第一步,它调用了自身的 obtainFreshBeanFactory() 方法:


protected ConfigurableListableBeanFactory obtainFreshBeanFactory() {
    this.refreshBeanFactory();
    ConfigurableListableBeanFactory beanFactory = this.getBeanFactory();
    if (this.logger.isDebugEnabled()) {
        this.logger.debug("Bean factory for " + this.getDisplayName() + ": " + beanFactory);
    }

    return beanFactory;
}

该方法又首先调用自身的 refreshBeanFactory() 方法来刷新 BeanFactory,然后调用 getBeanFactory() 方法获取 FactoryBean。

然后回到 refresh() 方法中,它接着调用了 prepareBeanFactory() 和 postProcessBeanFactory() 方法,这两个方法是用来做一些准备工作的,比如设置和注册一些加载器。

接着是第二步,调用了 invokeBeanFactoryPostProcessors() 方法,这是 BeanFactory 的后置处理器:


protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) {
    PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, this.getBeanFactoryPostProcessors());
    if (beanFactory.getTempClassLoader() == null && beanFactory.containsBean("loadTimeWeaver")) {
        beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory));
        beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader()));
    }

}

该方法通过反射获取到了所有实现了 BeanFactoryPostProcessor 接口的 Bean,并调用 postProcessBeanFactory() 方法。

然后是第三步,调用了 registerBeanPostProcessors() 方法:


protected void registerBeanPostProcessors(ConfigurableListableBeanFactory beanFactory) {
    PostProcessorRegistrationDelegate.registerBeanPostProcessors(beanFactory, this);
}

该方法用于将所有实现了 BeanPostProcessor 接口的 Bean 注册到容器 Bean 后置处理器的注册表中。

第四步,调用了 initMessageSource() 方法:

protected void initMessageSource() {
    ConfigurableListableBeanFactory beanFactory = this.getBeanFactory();
    if (beanFactory.containsLocalBean("messageSource")) {
        this.messageSource = (MessageSource)beanFactory.getBean("messageSource", MessageSource.class);
        if (this.parent != null && this.messageSource instanceof HierarchicalMessageSource) {
            HierarchicalMessageSource hms = (HierarchicalMessageSource)this.messageSource;
            if (hms.getParentMessageSource() == null) {
                hms.setParentMessageSource(this.getInternalParentMessageSource());
            }
        }

        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Using MessageSource [" + this.messageSource + "]");
        }
    } else {
        DelegatingMessageSource dms = new DelegatingMessageSource();
        dms.setParentMessageSource(this.getInternalParentMessageSource());
        this.messageSource = dms;
        beanFactory.registerSingleton("messageSource", this.messageSource);
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Unable to locate MessageSource with name 'messageSource': using default [" + this.messageSource + "]");
        }
    }

}

该方法用于初始化容器的国际化消息资源,具体是如何做的就不分析了,这不是本篇文章的重点。

第五步,调用了 initApplicationEventMulticaster() 方法:

protected void initApplicationEventMulticaster() {
    ConfigurableListableBeanFactory beanFactory = this.getBeanFactory();
    if (beanFactory.containsLocalBean("applicationEventMulticaster")) {
        this.applicationEventMulticaster = (ApplicationEventMulticaster)beanFactory.getBean("applicationEventMulticaster", ApplicationEventMulticaster.class);
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Using ApplicationEventMulticaster [" + this.applicationEventMulticaster + "]");
        }
    } else {
        this.applicationEventMulticaster = new SimpleApplicationEventMulticaster(beanFactory);
        beanFactory.registerSingleton("applicationEventMulticaster", this.applicationEventMulticaster);
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Unable to locate ApplicationEventMulticaster with name 'applicationEventMulticaster': using default [" + this.applicationEventMulticaster + "]");
        }
    }

}

该方法用于初始化应用上下文事件广播器,同样不做深入介绍。

第六步,调用 onRefresh() 方法:


protected void onRefresh() throws BeansException {
}

这是一个钩子方法,具体是由子类去执行某些特定的操作的,这里简单介绍一下何为钩子方法。

假设有一个接口,接口中有很多个方法,而现在你只想要使用接口中的某几个方法,那么你就可以编写一个抽象类实现该接口,并实现你要用的那些方法,而对于其它方法你只需要空实现即可,这些真正实现的方法称为具体方法,而其它方法便称为钩子方法。

其实钩子方法和其它方法没有本质上的区别,有的只是意识形态上的一种差异,钩子方法会事先进行实现,你可以在钩子方法中提供一些默认的配置,以便你在继承抽象类的时候能够不用实现该方法便得到应有的配置,但一般都会采用在抽象类中空实现的办法,你可以在子类中对其实现进行拓展,也可以不实现使用原有功能。

接下来看第七步,调用 registerListeners() 方法:

protected void registerListeners() {
    Iterator var1 = this.getApplicationListeners().iterator();

    while(var1.hasNext()) {
        ApplicationListener<?> listener = (ApplicationListener)var1.next();
        this.getApplicationEventMulticaster().addApplicationListener(listener);
    }

    String[] listenerBeanNames = this.getBeanNamesForType(ApplicationListener.class, true, false);
    String[] var7 = listenerBeanNames;
    int var3 = listenerBeanNames.length;

    for(int var4 = 0; var4 < var3; ++var4) {
        String listenerBeanName = var7[var4];
        this.getApplicationEventMulticaster().addApplicationListenerBean(listenerBeanName);
    }

    Set<ApplicationEvent> earlyEventsToProcess = this.earlyApplicationEvents;
    this.earlyApplicationEvents = null;
    if (earlyEventsToProcess != null) {
        Iterator var9 = earlyEventsToProcess.iterator();

        while(var9.hasNext()) {
            ApplicationEvent earlyEvent = (ApplicationEvent)var9.next();
            this.getApplicationEventMulticaster().multicastEvent(earlyEvent);
        }
    }

}

该方法用于注册事件监听器。

第八步,调用 finishBeanFactoryInitialization() 方法:

protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
    if (beanFactory.containsBean("conversionService") && beanFactory.isTypeMatch("conversionService", ConversionService.class)) {
        beanFactory.setConversionService((ConversionService)beanFactory.getBean("conversionService", ConversionService.class));
    }

    if (!beanFactory.hasEmbeddedValueResolver()) {
        beanFactory.addEmbeddedValueResolver(new StringValueResolver() {
            public String resolveStringValue(String strVal) {
                return AbstractApplicationContext.this.getEnvironment().resolvePlaceholders(strVal);
            }
        });
    }

    String[] weaverAwareNames = beanFactory.getBeanNamesForType(LoadTimeWeaverAware.class, false, false);
    String[] var3 = weaverAwareNames;
    int var4 = weaverAwareNames.length;

    for(int var5 = 0; var5 < var4; ++var5) {
        String weaverAwareName = var3[var5];
        this.getBean(weaverAwareName);
    }

    beanFactory.setTempClassLoader((ClassLoader)null);
    beanFactory.freezeConfiguration();
    beanFactory.preInstantiateSingletons();
}

该方法会初始化所有单实例的 Bean,并将其放入 Spring 容器的缓存池中。

第九步,调用 finishRefresh() 方法:

protected void finishRefresh() {
    this.initLifecycleProcessor();
    this.getLifecycleProcessor().onRefresh();
    this.publishEvent((ApplicationEvent)(new ContextRefreshedEvent(this)));
    LiveBeansView.registerApplicationContext(this);
}

该方法用于创建上下文刷新事件,事件广播器则负责将这些事件广播到每个注册的事件监听器中。

至此,IoC 容器完成了它的所有任务。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值