【Spring】Spring中的三级缓存及循环依赖的问题

上篇文章我们说了SpringBean的生命周期,知道了SpringBean的实例化创建和销毁的过程,但是我们知道Spring是支持一个Bean可以引入另外的Bean对象的,那么就不可避免出现相互依赖的问题,Spring是如何解决这个问题呢?Spring注册Bean的方式有很多种,Spring又是可以解决哪些循环依赖又不能解决哪几种循环依赖呢,本文会一一介绍。

一、什么是循环依赖

通俗的讲,循环依赖是指Spring中N个Bean相互依赖从而形成一个闭环的现象。

 相互依赖只是闭环的因,闭环是相互依赖的果,特殊情况下自己依赖自己也是一种闭环。

二、循环依赖的代码演示

 2.1、引入依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.sxx</groupId>
    <artifactId>CyclicDependence</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <spring.version>5.1.6.RELEASE</spring.version>
        <junit.version>4.12</junit.version>
        <slf4j.version>1.7.35</slf4j.version>
        <aspectjweaver.version>1.8.9</aspectjweaver.version>
        <json.version>1.2.27</json.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>${junit.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>${slf4j.version}</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>${aspectjweaver.version}</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>${json.version}</version>
        </dependency>
    </dependencies>
</project>

2.2、创建对象

package com.sxx.cyclic.entity;

import com.alibaba.fastjson.JSON;

public class A {

    private String name;

    private B b;

    @Override
    public String toString() {
//        System.out.println("A中注入了Bean对象" + b);
        return JSON.toJSONString(this);
    }

    public String getName() {
        return name;
    }
    
    public void setName(String name) {
        this.name = name;
    }
    
    public B getB() {
        return b;
    }
    
    public void setB(B b) {
        this.b = b;
    }
}
package com.sxx.cyclic.entity;

import com.alibaba.fastjson.JSON;

public class B {

    private Integer age;

    private A a;

    @Override
    public String toString() {
//        System.out.println("B中注入了Bean对象" + a);
        return JSON.toJSONString(this);
    }

    public Integer getAge() {
        return age;
    }

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

    public A getA() {
        return a;
    }

    public void setA(A a) {
        this.a = a;
    }
}

2.3、编写xml文件

<?xml version="1.0" encoding="utf-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">
    <bean id="a" class="com.sxx.cyclic.entity.A">
        <property name="name" value="张三"/>
        <!--引入B-->
        <property name="b" ref="b"/>
    </bean>
    <bean id="b" class="com.sxx.cyclic.entity.B">
        <property name="age" value="20"/>
        <!--引入A-->
        <property name="a" ref="a"/>
    </bean>
</beans>

2.4、测试类

package com.sxx.cyclic.entity;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class CyclicTest {

    public static void main(String[] args) {
        ApplicationContext applicationContext = new
                ClassPathXmlApplicationContext("classpath*:bean.xml");
        A a = applicationContext.getBean("a", A.class);
        System.out.println(a.toString());
    }
}

2.5、结果

{"b":{"a":{"$ref":".."},"age":20},"name":"张三"}

三、Spring的循环依赖

从上面的代码我们可以很明显发现两个问题:

        1.我在重写A、B对象的toString方法时把引入的其他bean对象打印出来了,但是在实际执行的时候我却把他注释掉了;

        2.在调用A的toString方法时,打印出来的a对象里面出现了本不该出现的属性,如下图所示:

试想下为什么会出现上述两种情况?

首先我们可以尝试放开toString中的打印语句看看会出现什么情况。

可以看到,当我们放开A、B中toString的打印语句后出现StackOverflowError,这说明我们的代码中出现了循环调用,根据之前的代码很容易猜测是出现了A、B两个bean相互持有的缘故,那么这又是在什么时候触发的呢?我们可以先注释掉toString打印信息,然后在getBean方法之后查看下A、B两个bean的对象信息。

 从图中可以看出,在Spring启动之后我们通过getBean从容器中获取A对象的时候,此时的A、B两个bean还在互相依赖,出现了闭环,于是在后续调用toString的时候要依赖打印b对象,b对象中又依赖打印a对象,于是出现了StackOverflowError,这也能解释为什么会出现下面这种现象了。

{"b":{"a":{"$ref":".."},"age":20},"name":"张三"}

3.1、Spring如何解决循环依赖

先放出流程图,后面会从源码层面一步步解析

 3.2、Spring创建singleton bean过程

要了解Spring解决循环依赖的过程,我们首先要知道Spring创建singleton bean的过程。

3.2.1、三级缓存

要了解singleton bean创建过程首先要知道三级缓存的概念,关于Spring的三级缓存在org.springframework.beans.factory.support.DefaultSingletonBeanRegistry下,其源码如下:

/**
 * 一级缓存:单例(对象)池,这里面的对象都是确保初始化完成,可以被正常使用的
 * 它可能来自3级,或者2级
 */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap(256);
        
/**
 * 三级缓存:单例工厂池,这里面不是bean本身,是它的一个工厂,未来调getObject来获取真正的bean
 * 一旦获取,就从这里删掉,进入2级(发生闭环的话)或1级(没有闭环)
 */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap(16);
        
/**
 * 二级缓存:早期(对象)单例池,这里面都是半成品,只是有人用它提前从3级get出来,把引用暴露出去
 * 它里面的属性可能是null,所以叫早期对象,early:半成品
 * 未来在getBean付完属性后,会调addSingleton清掉2级,正式进入1级
 */
private final Map<String, Object> earlySingletonObjects = new HashMap(16);

 注意:三级缓存其实质就是三个不同功能的map,严格意义上来说只有singletonObjects算是一级缓存,其他两个只是Spring在生成bean过程中起辅助作用,即解决生成bean过程中存在的一些问题的。

3.2.2、Spring创建singleton bean过程

我们知道在Spring初始化之后,我们需要通过getBean()去拿到对应的Bean对象,那么getBean里面都做了哪些操作呢?

从上图可知,getBean方法是调用了doGetBean()方法,这个才是getBean的实际执行内容,我们看到下面对相关源码的分析:

protected <T> T doGetBean(String name, @Nullable Class<T> requiredType, @Nullable Object[] args, boolean typeCheckOnly) throws BeansException {
        //转化beanname,是否&开头,skip
		String beanName = this.transformedBeanName(name);
		//先尝试获取如果拿不到再创建,循环依赖闭环可以拿到;拿1级-2级-3级
        Object sharedInstance = this.getSingleton(beanName);
        Object bean;
		//判断是否是闭环,是的时候进入
        if (sharedInstance != null && args == null) {
            if (this.logger.isTraceEnabled()) {
                if (this.isSingletonCurrentlyInCreation(beanName)) {
                    this.logger.trace("Returning eagerly cached instance of singleton bean '" + beanName + "' that is not fully initialized yet - a consequence of a circular reference");
                } else {
                    this.logger.trace("Returning cached instance of singleton bean '" + beanName + "'");
                }
            }
			//下面这个方法:如果是普通 Bean 的话,直接返回 sharedInstance
			//如果是 FactoryBean 的话,返回它创建的那个实例对象
            bean = this.getObjectForBeanInstance(sharedInstance, name, beanName, (RootBeanDefinition)null);
        } else {
			//没有创建过原型类型的bean
            if (this.isPrototypeCurrentlyInCreation(beanName)) {
				//当前线程已经创建过了此 beanName 的 prototype 类型的 bean,那么抛异常
                throw new BeanCurrentlyInCreationException(beanName);
            }

			//检查一下这个 BeanDefinition 在容器中是否存在(初始化不存在)
            BeanFactory parentBeanFactory = this.getParentBeanFactory();
            if (parentBeanFactory != null && !this.containsBeanDefinition(beanName)) {
                //如果当前容器不存在这个 BeanDefinition,试试父容器中有没有
				String nameToLookup = this.originalBeanName(name);
                if (parentBeanFactory instanceof AbstractBeanFactory) {
                    return ((AbstractBeanFactory)parentBeanFactory).doGetBean(nameToLookup, requiredType, args, typeCheckOnly);
                }

                if (args != null) {
					//返回父容器的查询结果
                    return parentBeanFactory.getBean(nameToLookup, args);
                }

                if (requiredType != null) {
                    return parentBeanFactory.getBean(nameToLookup, requiredType);
                }

                return parentBeanFactory.getBean(nameToLookup);
            }
			//typeCheckOnly 为 false,将当前 beanName 放入一个 alreadyCreated 的 Set 集合中
            if (!typeCheckOnly) {
				//将当前的bean存入到set集合alreadyCreated中
                this.markBeanAsCreated(beanName);
            }

            try {
				//beanDefinitionMap.get()
                RootBeanDefinition mbd = this.getMergedLocalBeanDefinition(beanName);
                //仅仅检查这个RootBeanDefinition是否为抽象的
				this.checkMergedBeanDefinition(mbd, beanName, args);
				//先初始化有depends-on依赖的所有Bean
                String[] dependsOn = mbd.getDependsOn();
                String[] var11;
                if (dependsOn != null) {
                    var11 = dependsOn;
                    int var12 = dependsOn.length;

                    for(int var13 = 0; var13 < var12; ++var13) {
                        String dep = var11[var13];
						//检查是不是有循环依赖
                        if (this.isDependent(beanName, dep)) {
                            throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Circular depends-on relationship between '" + beanName + "' and '" + dep + "'");
                        }
						//注册一下依赖关系
                        this.registerDependentBean(dep, beanName);

                        try {
							//初始化被依赖项
                            this.getBean(dep);
                        } catch (NoSuchBeanDefinitionException var24) {
                            throw new BeanCreationException(mbd.getResourceDescription(), beanName, "'" + beanName + "' depends on missing bean '" + dep + "'", var24);
                        }
                    }
                }
				//创建 singleton 的实例
                if (mbd.isSingleton()) {
					//bean实例化完后放到一级缓存
                    sharedInstance = this.getSingleton(beanName, () -> {
                        try {
							//执行创建bean
                            return this.createBean(beanName, mbd, args);
                        } catch (BeansException var5) {
							//显示从单例缓存中删除bean,并删除该bean临时引用的所有bean
                            this.destroySingleton(beanName);
                            throw var5;
                        }
                    });
                    bean = this.getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
				//创建prototype 的实例
                } else if (mbd.isPrototype()) {
                    var11 = null;

                    Object prototypeInstance;
                    try {
                        this.beforePrototypeCreation(beanName);
						//执行创建bean
                        prototypeInstance = this.createBean(beanName, mbd, args);
                    } finally {
                        this.afterPrototypeCreation(beanName);
                    }

                    bean = this.getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
				// 如果不是singleton或prototype的话,需要委托给相应的实现类来处理
                } else {
                    String scopeName = mbd.getScope();
                    Scope scope = (Scope)this.scopes.get(scopeName);
                    if (scope == null) {
                        throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'");
                    }

                    try {
                        Object scopedInstance = scope.get(beanName, () -> {
                            this.beforePrototypeCreation(beanName);

                            Object var4;
                            try {
                                var4 = this.createBean(beanName, mbd, args);
                            } finally {
                                this.afterPrototypeCreation(beanName);
                            }

                            return var4;
                        });
                        bean = this.getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
                    } catch (IllegalStateException var23) {
                        throw new BeanCreationException(beanName, "Scope '" + scopeName + "' is not active for the current thread; consider defining a scoped proxy for this bean if you intend to refer to it from a singleton", var23);
                    }
                }
            } catch (BeansException var26) {
                this.cleanupAfterBeanCreationFailure(beanName);
                throw var26;
            }
        }

        if (requiredType != null && !requiredType.isInstance(bean)) {
            try {
                T convertedBean = this.getTypeConverter().convertIfNecessary(bean, requiredType);
                if (convertedBean == null) {
                    throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());
                } else {
                    return convertedBean;
                }
            } catch (TypeMismatchException var25) {
                if (this.logger.isTraceEnabled()) {
                    this.logger.trace("Failed to convert bean '" + name + "' to required type '" + ClassUtils.getQualifiedName(requiredType) + "'", var25);
                }

                throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());
            }
        } else {
            return bean;
        }
    }

具体的主流程如下图所示:

这里以Spring5.1.6版本为例,各个方法的入口如下:

1、getBean的入口:

org.springframework.beans.factory.support.AbstractBeanFactory#doGetBean 

2、根据条件尝试从三级缓存中拿到Bean的入口org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#getSingleton(java.lang.String, boolean)

3、这个和2名字相同,但是作用完全不一样,他是通过factory创建org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#getSingleton(java.lang.String, org.springframework.beans.factory.ObjectFactory<?>)

4、执行创建Bean的入口org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#createBean(java.lang.String, org.springframework.beans.factory.support.RootBeanDefinition, java.lang.Object[])

5、实际创建Bean的方法org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean

6、实例化Bean的入口org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#createBeanInstance

7.真正填充Bean属性的入口

org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#populateBean

8、调用Bean的后置处理器的方法入口

org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#initializeBean(java.lang.String, java.lang.Object, org.springframework.beans.factory.support.RootBeanDefinition)

 3.2.3、三个getSingleton

 从上面可以看出在创建单例bean的时候出现了两个同名不同作用的getSingleton方法,实际上在DefaultSingletonBeanRegistry中重载了三个getSingleton方法,他们的作用也是不相同的。

	@Nullable
    public Object getSingleton(String beanName) {
        return this.getSingleton(beanName, true);
    }

    @Nullable
    protected Object getSingleton(String beanName, boolean allowEarlyReference) {
        //先从一级缓存中拿
		Object singletonObject = this.singletonObjects.get(beanName);
        if (singletonObject == null && this.isSingletonCurrentlyInCreation(beanName)) {
            synchronized(this.singletonObjects) {
				//在从二级缓存拿
                singletonObject = this.earlySingletonObjects.get(beanName);
                //allowEarlyReference是否允许循环依赖
				if (singletonObject == null && allowEarlyReference) {
                    //在从三级缓存拿,如果找到则在这结束这个死循环
					ObjectFactory<?> singletonFactory = (ObjectFactory)this.singletonFactories.get(beanName);
                    if (singletonFactory != null) {
					    //调用早期对象方法AbstractAutowireCapableBeanFactory.getEarlyBeanReference
                        singletonObject = singletonFactory.getObject();
                        //三级缓存返回的早期对象放到了二级缓存
						this.earlySingletonObjects.put(beanName, singletonObject);
                        //移除三级缓存
						this.singletonFactories.remove(beanName);
                    }
                }
            }
        }
		//返回这个单例对象
        return singletonObject;
    }

    public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
        Assert.notNull(beanName, "Bean name must not be null");
        synchronized(this.singletonObjects) {
			//尝试从一级缓存中拿
            Object singletonObject = this.singletonObjects.get(beanName);
            if (singletonObject == null) {
				//判断是否是正在销毁的单例
                if (this.singletonsCurrentlyInDestruction) {
                    throw new BeanCreationNotAllowedException(beanName, "Singleton bean creation not allowed while singletons of this factory are in destruction (Do not request a bean from a BeanFactory in a destroy method implementation!)");
                }

                if (this.logger.isDebugEnabled()) {
                    this.logger.debug("Creating shared instance of singleton bean '" + beanName + "'");
                }
				//创建前的检查
                this.beforeSingletonCreation(beanName);
                boolean newSingleton = false;
                boolean recordSuppressedExceptions = this.suppressedExceptions == null;
                if (recordSuppressedExceptions) {
                    this.suppressedExceptions = new LinkedHashSet();
                }

                try {
					//调用createBean方法
                    singletonObject = singletonFactory.getObject();
                    newSingleton = true;
                } catch (IllegalStateException var16) {
                    singletonObject = this.singletonObjects.get(beanName);
                    if (singletonObject == null) {
                        throw var16;
                    }
                } catch (BeanCreationException var17) {
                    BeanCreationException ex = var17;
                    if (recordSuppressedExceptions) {
                        Iterator var8 = this.suppressedExceptions.iterator();

                        while(var8.hasNext()) {
                            Exception suppressedException = (Exception)var8.next();
                            ex.addRelatedCause(suppressedException);
                        }
                    }

                    throw ex;
                } finally {
                    if (recordSuppressedExceptions) {
                        this.suppressedExceptions = null;
                    }

                    this.afterSingletonCreation(beanName);
                }

                if (newSingleton) {
					//bean实例化完后 ,放到一级缓存
                    this.addSingleton(beanName, singletonObject);
                }
            }

            return singletonObject;
        }
    }

根据getSingleton的源码可知,三个方法的的作用如下:

第一个getSingleton:调用第二个getSingleton,同时传入beanName和true;

第二个getSingleton:先从一级缓存中拿bean,如果没有再校验条件,同时根据allowEarlyReference判断是否进入下面方法,如果进入就从三级拿bean,同时三级缓存移入二级缓存,否则直接返回null;

第三个getSingleton:先从一级缓存中拿bean,如果没有,通过传入的singletonFactory创建并放入一级缓存中,同时清除二三级缓存。

 3.3、Spring解决循环依赖的过程

其实从之前的Spring解决循环依赖的流程图与Spring创建singleton bean的源码分析我们很容易知道,Spring处理循环依赖的流程如下:

  1. 入口为getBean("a")方法;
  2. 进入doGetBean()方法开始创建A对象;
  3. 第一遍先去一级缓存中查询,因为Spring刚启动,第一遍肯定查询不到;
  4. 开始在doGetBean中进行A的初始化,并放入三级缓存中;
  5. A初始化完成后开始填充属性,在填充属性的时候发现需要注入B对象,因此触发getBean("b");
  6. 重复1-4的流程,然后开始对B填充属性,再次进入到getBean("a")方法中;
  7. 此时不在和第一次进入getBean("a")一样了,根据条件检测有闭环,开始尝试从三级缓存中拿到一个半成品即没有填充属性的A对象,这事便可以完成对B对象的填充;
  8. B对象初始化完成后,将B移入到一级缓存中;
  9. 此时回转到第一次getBean("a")时便可以获取到一个成品的B对象,此时便完成了A对象的填充
  10. 至此,A、B循环依赖解决完成。

至此能解释在调用toString()方法时打印出来的对象A中B属性存在$ref的问题了

四、Spring循环依赖的若干问题

4.1、Spring不能解决的循环依赖

从上面的流程我们知道,Spring利用三级缓存的变化来解决循环依赖的,如果bean时多例的话每次都需要去实例化一个新的对象,压根就不用三级缓存,因此无法解决多例的缓存依赖。除此之前,我们知道Spring是在A实例化完成之后,在填充属性时才开始getBean("b")触发b的实例化过程,而如果是通过构造方法进行的循环依赖,实例化时需要调用构造方法,也没有办法触发b的实例化过程,因此也无法解决此种情况的循环依赖。

前提依赖注入的方式是否可以解决循环依赖原因
A、B互相依赖(循环依赖)均采用setter注入三级缓存可以解决循环依赖
A、B互相依赖(循环依赖)均采用构造器注入A实例化的时候要调用构造器,需要引入B,此时未到填充A属性过程,不能触发B的实例化过程
A、B互相依赖(循环依赖)A注入B的方式为setter,B注入A的方式为构造器A在填充属性的时候可以触发B实例化过程,B在实例化时可以在三级缓存中拿到A的代理bean,因此B可以正常初始化
A、B互相依赖(循环依赖)A注入B的方式为构造器,B注入A的方式为setterA实例化的时候要调用构造器,需要引入B,此时未到填充A属性过程,不能触发B的实例化过程

4.2、为什么要用三级缓存解决循环依赖

 我们已经知道了Spring利用三级缓存来解决循环依赖了,为什么三级缓存?一级或者二级缓存能不能解决循环依赖呢?

从单纯的解决循环依赖的角度上来看,二级甚至是一级缓存都是可以处理缓存依赖的,那么Spring为什么要用三级缓存缓存呢?

首先说下一级缓存的问题。

我们知道Spring的bean有的是需要aop进行增强的,对于这样的bean最终放到缓存中的应该是一个代理 bean。而代理 bean 的产生是在 initializeBean(第三阶段) 的时候。所以,我们推导出:如果只使用一级缓存的话,缓存的插入应该放在 initializeBean 之后。

如果在 initializeBean 的时候记录缓存,那么碰到循环依赖的情况,需要在 populateBean(第二阶段) 的时候再去注入循环依赖的 bean,此时,缓存中是没有循环依赖的 bean 的,就会导致 bean 重新创建实例,这样显然是不行的。

那么可以用二级缓存解决循环依赖吗?

答案是可以的,那么Spring为什么不用二级缓存呢?前面我们已经详细介绍了Spring三级缓存的不同作用,我们知道,Spring的二级缓存放的是半成品的bean,没有过多的扩展,如果仅仅用于解决循环依赖是完全可以的,但是三级缓存是一个Factory,里面可以在创建的前后嵌入我们的代码,和前后置处理器,Aop(getObject中)之类的操作就发生在这里,他的扩展性比二级缓存强,看起来也更加清晰明白,这就是三级缓存的绝妙之处。

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值