Spring揭秘-Spring的IoC容器之BeanFactory

【版权申明】未经博主同意,谢绝转载!(请尊重原创,博主保留追究权)
https://blog.csdn.net/qq_36000403/article/details/83189280
出自【zzf__的博客】

1.Spring的IoC容器的介绍

Spring的IoC容器是一个IoC Service Provider,但是,这只是它被冠以IoC之名的部分原因,我们不能忽略的是”容器”。Spring的IoC容器是一个提供IoC支持的轻量级容器,除了提供基本的IoC支持,它作为轻量级容器还提供了IoC之外的支持。如在Spring的IoC容器之上,Spring还提供了相应的AOP框架支持,企业级服务集成等服务。Spring的IoC容器和IoC Service Provider所提供的服务之间存在一定的交集,二者的关系如图
在这里插入图片描述

1.1 Spring提供了两种容器类型:

BeanFactory:基础类型IoC容器,提供完整的IoC服务支持。如果没有特殊指定,默认采用延迟初始化策略(lazy-load)。只有当客户端对象需要访问容器中的某个受管对象的时候,才对该受管对象进行初始化以及依赖注入操作。相对来说,容器启动初期速度较快,所需要的资源有限。 对于资源有限,并且功能要求不是很严格的场景,BeanFactory是比较合适的IoC容器选择

ApplicationContext: ApplicationContext在BeanFactory的基础上构建,是相对比较高级的容器实现,除了拥有BeanFactory的所有支持,ApplicationContext还提供了其他高级特性,比如事件发布,国际化信息支持等,这些会在后面详述。ApplicationContext所管理的对象,在该类型容器启动之后,默认全部初始化并绑定完成。所以,相对于BeanFactory来说,ApplicationContext要求更多的系统资源,同时,因为在启动时就完成所有初始化,容器启动时间较之BeanFactory也会长一些。在那些系统资源充足容器启动时间较之BeanFactory也会长一些。在那些系统资源充足,并且要求更多功能的场景中,ApplicationContext类型的容器是比较合适的选择

BeanFactory,就是bean的工厂,既然spring框架提倡使用POJO,那么把每个业务对象看作一个JavaBean对象,那么把每个业务对象看作一个JavaBean对象,或许理解为什么Spring的IoC基本容器会起这么一个名字。

拥有BeanFactory后,对象之间的依赖关系的解决方式改变了,以前我们是需要自己去”拉”(Pull)所依赖的业务对象,现在需要依赖什么,让BeanFactory为我们推过来(Push)就行了


2.BeanFactory的对象注册与依赖绑定方式

2.1直接编码方式:

其实,把编码方式单独提出来称作一种方式并不合适,不管使用什么方式,最终都需要编码才能”落实”所有信息并付诸使用。不过通过这些代码,起码可以让我们更加清楚BeanFactory在底层是如何运作的

通过编码方式使用BeanFactory实现业务对象的注册及绑定

public static void main(String[] args){
	DefaultListableBeanFactory beanRegistry = new DefaultListableBeanFactory();
	BeanFactory contaioner = (BeanFactory)bindViaCode(beanRegistry);
	FXNewsProvider  newsProvider = 
	(FXNewsProvider) contaioner.getBean(“djNewsProvider”);
	newsProvider.getAndPersistNews();
}


public static BeanFactory bindViaCode(BeanDefinitionRegistry registry){
	//bean定义
	AbstractBeanDefinition newsProvider = 
	new RootBeanDefinition(FXNewsProvider.class,true);


	AbstractBeanDefinition newsListener = 
	new RootBeanDefinition(DowJonesNewsListener.class,true);

	AbstractBeanDefinition newsPersister = 
	new RootBeanDefinition(DowJonesNewsPersister.class,true);

	//将bean定义注册到容器中
	Registry.registerBeanDefinition(“djNewsProvider”,newsProvider);
	Registry.registerBeanDefinition(“djListener”,newsProvider);
	Registry.registerBeanDefinition(“djPersister”, newsPersister);

	//1.可以通过构造方法注入方式
	ConstructorArgumentValues argValues  =  new ConstructorArgumentValues();
	argValues.addIndexedArgumentValue(0,newsListener);
	argValues.addIndexedArgumentValue(1,newsPersister);
	newsProvider.setConstructorArgumentValues(argValues);

	//2.或者通过setter方法注入方式
	MutablePropertyValues propertyValues = new MutablePropertyValues();
	propertyValues.addPropertyValue(new propertyValue(“newsListener”, newsListener));
	propertyValues.addPropertyValue(new propertyValue(“newsPersister”, newsPersister));
	
	//绑定完成
	return (BeanFactory)registry;
}

先看下图:

BeanFactory是个接口不能直接使用,DefaultListableBeanFactory同时是BeanFactory和BeanDefinitionRegistry的实现类

BeanDefinitionRegistry接口在BeanFactory的实现中担当Bean注册管理的角色,定义了抽象Bean的注册逻辑
BeanFactory接口只定义如何访问容器内管理Bean的方法

每一个受管的对象,在容器中都会有一个BeanDefinition的实例(instance)与之相对应,该BeanDefinition的实例负责保存对象的所有必要信息,包括其对应的对象的class类型,是否是抽象类,构造方法参数以及其他属性等。当客户端向BeanFactory请求相应对象的时候,BeanFactory会通过这些信息为客户端返回一个完备可用的对象实例。

2.2外部配置文件方式

采用外部配置文件时、 Spring的loC容器有一个统一的处理方式。 通常情况下,需要根据不同的外部配置文件格式,给出相应的BeandefinitionReader现类,由 BeanDefinitionReader的相应实现类负责将相应的配置文件内容读取并映射到BeanDefinition,然后将映射后的BeanDefinition注册到一个BeanDefinitionRegistry,之后,BeanDefinitionRegistry即完成Bean的注册和加载。实当然,大部分工作,包括解析文件格式、装配 BeanDefinition之类的工作,都是由 BeandefinitionReader的相应实现类来做的, BeanDefinitionRegistry只不过负责保管而已。

2.2.1 Properties配置格式的加载(略)
2.2.2 XML配置格式的加载

加载XML配置文件的BeanFactory的使用演示

public static void main(String[] args){
	DefaultListableBeanFactory beanRegistry = new DefaultListableBeanFactory();
	BeanFactory container = (BeanFactory)bindViaXMLFile(beanRegistry);
	FXNewsProvider newsProvider = 
	(FXNewsProvider)container.getBean(“djNewsProvider”);
	newsProvider.getAndPersistNews();
}

public static BeanFactory bindViaXMLFile(BeanDefinitionRegistry registry){
	XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(registry);
	Reader.loadBeanDefinitions(“classpath:../news-config.xml”);
	Return (BeanFactory)registry;
	//或者直接
	//return new XmlBeanFactory(new ClassPathResource(“../news-config.xml”));
}

与为Properties配置文件格式提供PropertiesBeanDefinitionReader相对应,Spring同样为XML格式的配置文件提供了现成的BeanDefinitionReader实现,即XmlBeanDefinitionReader。XmlBeanDefinitionReader负责读取Spring指定格式的XML配置文件并解析,之后将解析后的文件内容映射到相应的BeanDefinition,并加载相应的BeanDefinitionRegistry(在这里是Default-ListableBeanFactory),这时,整个BeanFactory就可以放给客户端使用了。
除了提供XmlBeanDefinitionReader用于XML格式配置文件的加载,Spring还在DefaultListableBeanFactory的基础上构建了简化XML格式配置加载的XmlBeanFactory实现。从以上代码最后注释掉的一行,你可以看到使用了XmlBeanFactory之后,完成XML的加载和BeanFactory的初始化是多么简单

2.2.3 注解方式
@Component
public class FXNewsProvider{
	@Autowired
	private IFXNewsListener newsListener;
	@Autowired
	private IFXNewsListener newsListener;
	
	public FXNewsProvider(IFXNewsListener newsListner,IFXNewsPersister newsPersister){

	this. newsListner  =  newsListner;
	this. newsPersister  =  newsPersister;
}
	….
}

@Component
Public class DowJonesNewsListener implements IFXNewsListener{
…
}
@Component
Public class DowJonesNewsPersister implements IFXNewsPersister{
…
}

然后在配置文件中使用

<context:component-scan  base-package=”cn.spring21.project.base.package”/>

然后在以上所有这些工作都完成之后,我们就可以像通常那样加载并执行当前应用程序了,如下所示:

public static void main(String[] args){
	ApplicationContext ctx = new ClassPathXmlApplicationContext(“配置文件路径”);
	FXNewsProvider newsProvider = (FXNewsProvider)container.getBean(“FXNewsProvider”);
	newsProvider.getAndPersistNews();
}


3.BeanFactory的XML之旅

3.1bena的scope

  • singleton
    配置中的bean定义可以看作是一种模板,容器会根据这个模板来构造对象。但是要根据这个模板构造多少对象实例,又该让这些构造完的对象实例存活多久,则由容器根据bean定义的scope语义来决定。标记为singleton
    scope(容器默认的scope)的对象定义,①(对象实例数量)在Spring的IoC容器中只存在一个实例,所有对该对象的引用将共享这个实例。②(对象存活时间)该实例从容器启动,并因为第一次被请求而初始化之后,将一直存活到容器退出,也就是说,它与IoC容器”几乎”拥有相同的”寿命”.

  • prototype
    针对声明为拥有 prototype scope的beam定义,容器在接到该类型对象的请求的时候,会每次都重新生成一个新的对象实例给请求方。虽然这种类型的对象的实例化以及属性设置等工作都是由容器负责的,但是只要准备完毕,并且对象实例返回给请求方之后,容器就不再拥有当前返回对象的引用,请求方需要自己负责当前返回对象的后继生命周期的管理工作,包括该对象的销毁。也就是说,容器每次返回给请求方一个新的对象突例之后,就任由这个对象实例“自生自灭”了

  • request session和global session
    这三个scope类型是Spring2.0之后新增加的,它们不像之前的singleton和prototype那么”通用”,因为它们只适用于Web应用程序,通常是与XmlWebApplicationContext共同
    使用


4.容器背后的秘密

先看图

4.1Spring的IoC容器功能实现的两个阶段:

功能:Spring的IoC容器所起的作用,就像上图那样,它会以某种方式加载Configuration Metadata(通常也就是XML格式的配置信息),然后根据这些信息绑定整个系统的对象

Spring的IoC容器实现以上功能的过程,基本上可以按照类似的流程划分为两个阶段,即容器启动阶段和Bean实例化阶段,如下图所示

4.1.1 容器启动阶段

容器启动伊始,首先会通过某种途径加载 Configuration Metadata。除了代码方式比较直接,在大部分情况下,容器需要依赖某些工具类( BeanDefinitionReader)对加载的 Configuration Metadata进行解析和分析。并将分析后的信息编组为相应的BeanDefinition,最后把这些保存了bean定义必要信息BeanDefinition,注册到相应的 BeanDefinitionRegistry,这样容器启动工作就完成了。
下图演示了这个阶段的主要工作。

总地来说,该阶段所做的工作可以认为是准备性的,重点更加侧重于对象管理信息的收集。当然,一些验证性或者辅助性的工作也可以在这个阶段完成

4.1.2 Bean实例化阶段

经过第一阶段,现在所有的bean定义信息都通过 Beandefinition的方式注册到了 BeanDefinitionRegistry中。当某个请求通过容器的getBean方法明确地请求某个对象,或者因依赖关系容器需要隐式地调用getBean方法时,就会触发第二阶段的活动。
该阶段,容器会首先检查所请求的对象之前是否已经初始化。如果没有初始化。则会根据注册的BeanDefinition所提供的信息实例化被请求对象,并为其注入依赖。如果该对象实现了某些回调接口,也会根据回调接口的要求来装配它。当该对象装配完毕之后,容器会立即将其返回请求方使用。如果说第一阶段只是根据图纸装配生产线的话,那么第二阶段就是使用装配好的生产线来生产具体的产品了

4.2 插手”容器的启动”

Spring提供了一种叫做BeanFactoryPostProcessor的容器扩展机制,该机制允许我们在容器实例化相应对象之前,对注册到容器的BeanDefinition所保存的信息做相应的修改,这就相当于在容器实现的第一阶段最后加入一道工序,让我们对最终的BeanDefinition做一些额外的操作,比如修改其中bean定义的某些属性,为bean定义增加其他信息等。

可以自己实现BeanFactoryPostProcessor ,如果有多个BeanFactoryPostProcessor,需要为其添加Order实现。Spring提供了几个现成的BeanFactoryPostProcessor实现类,分别是PropertyPlaceholderConfigurer和OverrideConfigurer,最后还有CustomEditorConfigurer,对于BeanFactory来说,我们需要用手动方式应用所有的BeanFactoryPostProcessor,这里不作介绍,对于ApplicationContext,来说,简单很多,因为ApplicationContext会自动识别配置文件中的BeanFactoryPostProcessor,仅需要在XML配置文件中简单配置一下即可,如配置CustomEditorConfigurer

<bean class =”org.springframework.beans.factory.config.CustomEditorConfigurer”>
	<property name=”customEditors”>
		<map>
			<entry key=”java.util.Date”>
				<ref bean=”datePropertyEditor”/>
			</entry>
		</map>
	</property>
</bean>
<bean id=”datePropertyEditor” class =”…DatePropertyEditor”>
	<property name=”datePattern”>
		<value>yyyy/MM/dd</value>	
	</property>
</bean>

以下对这三个BeanFactoryPostProcessor实现类进行介绍,重点介绍CustomEditorConfigurer

4.2.1 PropertyPlaceholderConfigurer

配置文件中的占位符

4.2.2 OverrideConfigurer

可以对容器中配置的任何你想处理的bean定义的property信息进行覆盖替换

4.2.3 CustomEditorConfigurer

为了处理配置文件中的数据类型与真正的业务对象所定义的数据类型转换其它两个BeanFactoryPostProcessor都是通过BeanDefinition中的数据进行变更以达到某种目的,与它们有所不同,CustomEditorConfigurer是另一种类型的BeanFactoryPostProcessor实现,它只是辅助性地将后期会用到的信息注册到容器,对BeanDefinition没有任何变动,我们知道,不管对象是什么类型,也不管这些对象所声明的依赖对象是什么类型,通常都是通过XML(或者 properties甚至其他媒介)文件格式来配置这些对象类型。但XML所记载的,都是 string类型,即容器从XML格式的文件中读取的都是字符串形式,最终应用程序却是由各种类型的对象所构成。要想完成这种由字符串到具体对象的转换(不管这个转换工作最终由谁来做),都需要这种转换规则相关的信息,而CustomEditorConfigurer就是帮助我们传达类似信息的。

Spring内部通过 Javabean的PropertyEditor来帮助进行和String类型到其他类型的转换工作。只要为每种对象类型提供一个 PropertyEditor,就可以根据该对象类型取得与其相对应PropertyEditor来做具体的类型转换。Spring容器内部在做具体的类型转换的时候,会采用JavaBean框架内默认的PropertyEditor的搜索逻辑,从而继承了对原生类型以及java.lang.String.java.awt.Color和java.awt.Font等类型的转换支持。同时,Spring框架还提供了自身实现的一些PropertyEditor,这些PropertyEditor大部分都位于org.springframework.beans.propertyeditors包下。

其中有StringArrayPropertyEditor ClassEditor FileEditor LocaleEditor PatternEditor以上这些PropertyEditor,容器通常会默认加载使用,所以,即使我们不告诉容器应该如何对这些类型进行转换,容器同样可以正确完成工作。但当我们需要指定的类型没有包含在以上所提到的PropertyEditor之列的时候,就需要给出针对这种类型的PropertyEditor实现,并通过CustomEditorConfigurer告知容器,以便容器在适当的时机使用到适当的PropertyEditor,详细使用方式,这里不作介绍,请自行查询资料


5.了解bean的一生

容器启动之后,并不会马上就实例化相应的bean定义,我们知道,容器现在仅仅拥有所有对象的BeanDefinition来保存实例化阶段将要用的必要信息。只有当请求通过BeanFactory的getBean()方法来请求某个对象实例的时候,才有可能触发Bean实例化阶段的活动。BeanFactory的getBean方法可以被客户端对象显示调用,也可以在容器内部隐式地被调用。隐式调用有如下两种情况
1.对于 Beanfactory来说,对象实例化默认采用延迟初始化。通常情况下,当对象A被请求而需要第一次实例化的时候,如果它所依赖的对象B之前同样没有被实例化,那么容器会先实例化对象A所依赖的对象。这时容器内部就会首先实例化对象B,以及对象A依赖的其他还没有被实例化的对象。这种情况是容器内部调用getBean(),对于本次请求的请求方是隐式的。

2.Applicationcontext启动之后会实例化所有的bean定义。但 ApplicationContext在实现的过程中依然循Sping容器实现流程的两个阶段,只不过它会在启动阶段的活动完成之后,紧接着调用注册到该容器的所有bean定义的实例化方法getBean()。这就是为什么当你得到 ApplicationContext类型的容器引用时,容器内所有对象已经被全部实例化完成。不信你查一下类org.springframework.context.support.AbstractApplicationContext的 refresh()方法

之所以说 getBean()方法是有可能触发Bean实例化阶段的活动,是因为只有当对应某个bean定义的 getBean()方法第一次波调用时,不管是显式的还是隐式的,Bean实例化阶段的活动才会被触发,第二次被调用则会直接返回容器缓存的第一次实例化完的对象实例( prototype类型bean除外)。当getBean()方法内部发现该bean定义之前还没有被实例化之后,会通过 createBean()方法来进行具体的对象实例化,实例化过程如下图所示

在这里插入图片描述

Spring容器将对其所管理的对象全部给予统一的生命周期管理,这些被管理的对象完全摆脱了原来那种”new完后被使用,脱离作用域后即被回收”的命运。

5.1 Bean的实例化与BeanWrapper

容器在内部实现的时候,采用”策略模式”来决定采用何种方式初始化bean实例,通常,可以通过反射或者CGLIB动态字节码生成来初始化相应的bean实例或者动态生成其子类

org.springframework.beans.factory.support.InstantiationStrategy定义是实例化策略的抽象接口,其直接子类SimpleInstantiationStrategy实现了简单的对象实例化功能,可以通过反射来实例化对象实例,但不支持方法注入方式的对象实例化。CglibSubclassingInstantiationStrategy继承了SimpleInstantiationStrategy的以反射方式实例化对象的功能,并且通过CGLIB的动态字节码生成功能,该策略实现类可以动态生成某个类的子类,进而满足了方法注入所需的对象实例化需求。默认情况下,容器内部采用的是CglibSubclassingInstantiationStrategy
容器只要根据相应bean定义的BeanDefinition取得实例化信息结合CglibSubclassingInstantiationStratery以及不同的bean定义类型,就可以返回实例化完成的对象实例。但是,返回方式上有些”点缀”.不是直接返回构造完成的对象实例,而是以BeanWrapper对构造完成的对象实例进行包裹,返回相应的BeanWrapper实例。

至此,第一步结束。

BeanWrapper接口通常在Spring框架内部使用,它有一个实现类org.springframework.beans.BeanWrapperImpl. 其作用是对某个bean进行”包裹”,然后对这个”包裹”的bean进行操作,比如设置或者获取bean的相应属性值。而在第一步结束后返回BeanWrapper实例而不是原先的对象实例,就是为了第二步”设置对象属性”。

BeanWrapper定义继承了org.springframework.beans.PropertyAccessor接口,可以以统一的方式对对象属性进行访问;BeanWrapper定义同时又直接或者间接继承了PropertyEditorRegistry和TypeConverter接口。不知你是否还记得CustomEditorConfigurer?当把各种PropertyEditor注册给容器时,BeanWrapper用到这些PropertyEditor,在第一步构造完成对象之后,Spring会根据对象实例构造一个BeanWrapperImpl实例,然后将之前CustomEditorConfigurer注册的PropertyEditor复制一份给BeanWrapperImpl实例(这就是BeanWrapper同时又是PropertyEditorRegistry的原因),这样,当BeanWrapper转换类型,设置对象属性时,就不会无从下手了。

5.2 各色的Aware接口

当对象实例化完成并且相关属性以及依赖设置完成之后,Spring容器会检查当前对象实例是否实现了一系列的以Aware命名结尾的接口定义,如果是,则将这些Aware接口定义中规定的依赖注入给当前对象实例

5.3 BeanPostProcessor

BeanpostProcessor的概念容易与 BeanfactoryPostProcessor的概念混淆。但只要记住BeanPostProcessor是存在于对象实例化阶段,而BeanFactoryPostProcessor则是存在于容器启动阶段,这两个概念就比较容易区分了。
与BeanFactoryPostProcessor通常会处理容器内所有符合条件的 BeanDefinition类似,BeanPostProcessor会处理容器内所有符合条件的实例化后的对象实例。该接口声明了两个方法,分别在两个不同的时机执行,见如下代码定义:

Public interface BeanPostProcessor{
Object postProcessBeforeInitialization(Object bean,String beanName) throws BeansException;

Object postProcessAfterInitialization(Object bean,String beanName) throws BeansException;
}

BeanPostProcessor的两个方法中都传入了原来的对象实例的引用,这为我们扩展容器的对象实例化过程中的行为提供了极大的便利,我们几乎可以对传入的对象实例执行任何操作。
通常比较常见的使用BeanPostProcessor的场景,是处理标记接口实现类,或者为当前对象提供代理实现,还可以通过BeanPostProcessor对当前对象实例做更多的处理,比如替换当前对象实例或者字节码增强当前对象实例等。Spring的AOP则更多地使用BeanPostProcessor来为对象生成相应的代理对象,如org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator。

BeanPostProcessor是容器提供的对象实例化阶段的强有力扩展点。

我们可以自定义BeanPostProcessor,对于BeanFactory容器来说,需要采用手工编码的方式将BeanPostProcessor实现类注册到容器。对于ApplicationContext容器来说,事情则方便很多,直接将相应的BeanPostProcessor实现类通过通常的XML配置文件配置一下即可。ApplicationContext容器会自动识别并加载注册到容器的BeanPostProcessor

实际上,并非所有注册到Spring容器内的bean定义都是按照bean的实例化过程图实例化的

5.4 InitializingBean和init-method

org. springframework.beans. factory.InitializingBean是容器内部广泛使用的一个对象生命周期标识接口其定义如下:

public interface InitializingBean{
void afterPropertiesSet() throws Exception;
}

该接口定义很简单,其作用在于,在对象实例化过程调用过“ BeanPostProcessor的前置处理”之后,会接着检测当前对象是否实现了InitializingBean接口,如果是,则会调用其afterPropertiesSet()方法进一步调整对象实例的状态。比如,在有些情况下,某个业务对象实例化完成后,还不能处于可以使用状态。这个时候就可以让该业务对象实现该接口,并在方法 afterPropertiesSet()中完成对该业务对象的后续处理。
虽然该接口在Spring容器内部广泛使用,但如果真的让我们的业务对象实现这个接口,则显得Spring容器比较具有侵入性。所以,Spring还提供了另一种方式来指定自定义的对象初始化操作,那就是在XML配置的时候,使用的init-method属性。

通过init-method,系统中业务对象的自定义初始化操作可以以任何方式命名,而不再受制于InitializingBean的 afterPropertiesSet()。如果系统开发过程中规定:所有业务对象的自定义初始化操作都必须以init()命名,为了省去挨个的设置init-method这样的烦琐,我们还可以通过最顶层的的 default-init-method统一指定这-init()方法名。

5.5 DisposableBean与destroy-method

当所有的一切,该设置的设置,该注入的注入,该调用的调用完成之后,容器将检查 singleton类型的bean实例,看其是否实现了org.spring framework.beans.factory. Disposablebean接口。或者其对应的bean定义是否通过的 destroy-method属性指定了自定义的对象销毁方法。如果是,就会为该实例注册一个用于对象销毁的回调( Callback),以便在这些 singleton类型的对象实例销毁之前,执行销毁逻辑
与 InitializingBean和init-method用于对象的自定义初始化相对应, DisposableBean和
destroy-method为对象提供了执行自定义销毁逻辑的机会。

最常见到的该功能的使用场景就是在 Spring容器中注册数据库连接池,在系统退出后,连接池应该关闭,以释放相应资源。

不过,这些自定义的对象销毁逻辑,在对象实例化完成并注册了相关的回调方法之后,并不会马上执行。回调方法注册后,返回的对象实例即处于使用状态,只有该对象实例不再被使用的时候,才会执行相关的自定义销毁逻辑,此时通常也就是Spring容器关闭的时候。但Spring容器在关闭之前,不会聪明到自动调用这些回调方法。所以,需要我们告知容器,在哪个时间点来执行对象的自定义销毁方法。具体使用方法自行查阅资料,并不难。这里不再作过多篇幅解释。


好了,关于BeanFactory就介绍到这了。欢迎留言区评论
  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值