被问麻了,Spring 如何处理循环依赖?
点击关注公众号,利用碎片时间学习
前言
Spring如何处理循环依赖?这是最近较为频繁被问到的一个面试题,在前面Bean实例化流程中,对属性注入一文多多少少对循环依赖有过介绍,这篇文章详细讲一下Spring中的循环依赖的处理方案。
什么是循环依赖
依赖指的是Bean与Bean之间的依赖关系,循环依赖指的是两个或者多个Bean相互依赖,如:
构造器循环依赖
代码示例:
public class BeanA {private BeanB beanB;public BeanA(BeanB beanB){this.beanB = beanB;}
}public class BeanB {private BeanA beanA;public BeanB(BeanA beanA){this.beanA = beanA;}
}
配置文件
<bean id="beanA" class="cn.itsource._01_di.BeanA" ><constructor-arg type="cn.itsource._01_di.BeanB" ref="beanB" /></bean><bean id="beanB" class="cn.itsource._01_di.BeanB" ><constructor-arg type="cn.itsource._01_di.BeanA" ref="beanA" /></bean>
Setter循环依赖
代码示例
public class BeanA {private BeanB beanB;public void setBeanB(BeanB beanB){this.beanB = beanB;}
}@Data
public class BeanB {private BeanA beanA;public void setBeanA(BeanA beanA){this.beanA = beanA;}
}
配置文件
<bean id="beanA" class="cn.itsource._01_di.BeanA" ><property name="beanB" ref="beanB" />
</bean><bean id="beanB" class="cn.itsource._01_di.BeanB"><property name="beanA" ref="beanA" />
</bean>
循环依赖包括: 构造器注入循环依赖 set , 注入循环依赖 和 prototype模式Bean的循环依赖。Spring只解决了单例Bean的 setter 注入循环依赖,对于构造器循环依赖,和 prototype模式的循环依赖是无法解决的,在创建Bean的时候就会抛出异常 :“BeanCurrentlyInCreationException
” ,
循环依赖控制开关在 AbstractRefreshableApplicationContext
容器工厂类中有定义:
public abstract class AbstractRefreshableApplicationContext extends AbstractApplicationContext {@Nullableprivate Boolean allowBeanDefinitionOverriding;//是否允许循环依赖@Nullableprivate Boolean allowCircularReferences;//设置循环依赖public void setAllowCircularReferences(boolean allowCircularReferences) {this.allowCircularReferences = allowCircularReferences;}
默认情况下是允许Bean之间的循环依赖的,在依赖注入时Spring会尝试处理循环依赖。如果将该属性配置为“false”则关闭循环依赖,当在Bean依赖注入的时遇到循环依赖时抛出异常。可以通过如下方式关闭,但是一般都不这么做
ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("bean.xml");
//禁用循环依赖
applicationContext.setAllowCircularReferences(false);
//刷新容器
applicationContext.refresh();
...
构造器循环依赖处理
构造器是不允许循环依赖的,动动你的小脑瓜想一想,比如:A 依赖 B ,B依赖C,C依赖A,在实例化A的时候,构造器需要注入B,然后Spirng会实例化B,此时的A属于“正在创建”的状态。当实例化B的时候,发现构造器需要注入C,然后去实例化C,然而实例化C的时候又需要注入A的实例,这样就造成了一个死循环,永远无法先实例化出某一个Bean,所以Spring遇到这里构造器循环依赖会直接抛出异常。
那么Spring到底是如何做的呢?
首先Spring会走Bean的实例化流程尝试创建 A 的实例 ,在创建实例之间先从 “正在创建Bean池” (一个缓存Map而已)中去查找A 是否正在创建,如果没找到,则将 A 放入 “正在创建Bean池”中,然后准备实例化构造器参数 B。
Spring会走Bean的实例化流程尝试创建 B 的实例 ,在创建实例之间先从 “正在创建Bean池” (一个缓存Map而已)中去查找B 是否正在创建,如果没找到,则将 B 放入 “正在创建Bean池”中,然后准备实例化构造器参数 A。
Spring会走Bean的实例化流程尝试创建 A 的实例 ,在创建实例之间先从 “正在创建Bean池” (一个缓存Map而已)中去查找A 是否正在创建。
此时:Spring发现 A 正处于“正在创建Bean池”,表示出现构造器循环依赖,抛出异常:“
BeanCurrentlyInCreationException
”
DefaultSingletonBeanRegistry#getSingleton
下面我们以 BeanA 构造参数依赖BeanB, BeanB 构造参数依赖BeanA 为例来分析。
当Spring的IOC容器启动,尝试对单利的BeanA进行初始化,根据之前的分析我们知道,单利Bean的创建入口是 AbstractBeanFactory#doGetBean
在该方法中会先从单利Bean缓存中获取,如果没有代码会走到:DefaultSingletonBeanRegistry#getSingleton(jString beanName, ObjectFactory<?> singletonFactory)
方法中 ,在该方法中会先对把创建的Bean加入 一个名字为 singletonsCurrentlyInCreation
的 ConcurrentHashMap中
,意思是该Bean正在创建中,然后调用 ObjectFactory.getObject()
实例化Bean , 假设 BeanA 进入了该方法进行实例化:
//正在创建中的Bean
private final Set<String> singletonsCurrentlyInCreation =Collections.newSetFromMap(new ConcurrentHashMap<>(16));public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {...省略...//把该Bean的名字加入 singletonsCurrentlyInCreation 正在创建池 中beforeSingletonCreation(beanName);boolean newSingleton = false;boolean recordSuppressedExceptions = (this.suppressedExceptions == null);if (recordSuppressedExceptions) {this.suppressedExceptions = new LinkedHashSet<>();}try {//调用ObjectFactory创建Bean的实例singletonObject = singletonFactory.getObject();newSingleton = true;}
...省略...//如果singletonsCurrentlyInCreation中没该Bean,就把该Bean存储到singletonsCurrentlyInCreation中,
//如果 singletonsCurrentlyInCreation 中有 该Bean,就报错循环依赖异常BeanCurrentlyInCreationException
//也就意味着同一个beanName进入该方法2次就会抛异常
protected void beforeSingletonCreation(String beanName) {if (!this.inCreationCheckExclusions.contains(beanName) && !this.singletonsCurrentlyInCreation.add(beanName)) {throw new BeanCurrentlyInCreationException(beanName);}}
beforeSingletonCreation
方法非常关键 ,它会把beanName加入 singletonsCurrentlyInCreation
,一个代表“正在创建中的Bean”的ConcurrentHashMap
中。
如果singletonsCurrentlyInCreation
中没该beanName,就把该Bean存储到singletonsCurrentlyInCreation
中, 如果 singletonsCurrentlyInCreation
中有 该Bean,就报错循环依赖异常BeanCurrentlyInCreationException
【注意】也就意味着同一个beanName进入该方法2次就会抛异常 , 现在BeanA已经加入了singletonsCurrentlyInCreation
AbstractAutowireCapableBeanFactory#autowireConstructor
我们前面分析过 ObjectFactory.getObject
实例化Bean的详细流程,这里我只是大概在复盘一下就行了。因为我们的BeanA的构造器注入了一个BeanB,所以 代码最终会走到AbstractAutowireCapableBeanFactory#autowireConstructor
,通过构造器来实例化BeanA(在属性注入那一章有讲到 ) 。
在autowireConstructor
方法中会通过 ConstructorResolver#resolveConstructorArguments
来解析构造参数,调用 BeanDefinitionValueResolver
去把 ref="beanB"
这种字符串的引用变成一个实实在在的Bean,即BeanB,所以在 BeanDefinitionValueResolver
属性值解析器中又会去实例化BeanB,同样会走到 DefaultSingletonBeanRegistry#getSingleton
中把BeanB加入 singletonsCurrentlyInCreation
“正在创建Bean池”中,然后调用ObjectFactory.getObject
实例化BeanB。
低于BeanB而已同样需要通过构造器创建,BeanB构造器参数依赖了BeanA,也就意味着又会调用 BeanDefinitionValueResolver
去把 ref=“beanA”
这种字符串引用变成容器中的BeanA的Bean实例,然后代码又会走到 DefaultSingletonBeanRegistry#getSingleton
。然后再一次的尝试把BeanA加入singletonsCurrentlyInCreation
“正在创建Bean池”。
此时问题就来了,在最开始创建BeanA的时候它已经加入过一次“正在创建Bean” 池,这会儿实例化BeanB的时候,由于构造器参数依赖了BeanA,导致BeanA又想进入“正在创建Bean” 池 ,此时 Spring抛出循环依赖异常:
Error creating bean with name ‘beanA’: Requested bean is currently in creation: Is there an unresolvable circular reference?
到这,Spring处理构造器循环依赖的源码分析完毕。
setter循环依赖处理
setter循环依赖是可以允许的。Spring是通过提前暴露未实例化完成的Bean的 ObjectFactory
来实现循环依赖的,这样做的目的是其他的Bean可以通过 ObjectFactory 引用到该Bean。
实现流程如下:
Spring创建BeanA,通过无参构造实例化,把BeanA添加到“正在创建Bean池”中,并暴露当前实例的
ObjectFactory
,即把ObjectFactory
添加到singletonFactories
(三级缓存)中,该ObjectFactory
用来获取创建中的BeanA,然后,然后通过setter注入BeanBSpring创建BeanB,通过无参构造实例化,把BeanB添加到“正在创建Bean池”中,并暴露一个
ObjectFactory
,然后,然后通过setter注入BeanA在BeanB通过setter注入BeanA时,由于BeanA 提前暴露了
ObjectFactory
,通过它返回一个提前暴露一个创建中的BeanA。然后完成BeanB的依赖注入
这里补张图:
获取Bean的时候走三级缓存
protected Object getSingleton(String beanName, boolean allowEarlyReference) {//一级缓存,存储实例化好的BeanObject singletonObject = this.singletonObjects.get(beanName);//如果单利缓存池中没有,但是beanName正在创建if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {synchronized (this.singletonObjects) {//获取二级缓存,这个里面存储的是正在创建的Bean,半成品singletonObject = this.earlySingletonObjects.get(beanName);//如果也为空,但是允许循环依赖if (singletonObject == null && allowEarlyReference) {//从三级缓存获取Bean的创建工厂,ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);if (singletonFactory != null) {//创建Bean的实例singletonObject = singletonFactory.getObject();//把Bean存储到二级缓存this.earlySingletonObjects.put(beanName, singletonObject);//移除三级缓存中的创建工厂this.singletonFactories.remove(beanName);}}}}return (singletonObject != NULL_OBJECT ? singletonObject : null);}
AbstractAutowireCapableBeanFactory#doCreateBean
我们以BeanA 通过settter依赖BeanB,BeanB通过setter 依赖BeanA为例来分析一下源码,在之前的Bean实例化流程分析过程中我们了解到,Bean的实例化会走AbstractBeanFactory#doGetBean
,然后查找单利缓存中是否有该Bean ,如果没有就调用 DefaultSingletonBeanRegistry#getSingleton
,方法会把BeanA加入 singletonsCurrentlyInCreation
“创建中的Bean池”,然后调用ObjectFactory.getObject
创建Bean.
org.springframework.beans.factory.support.AbstractBeanFactory#doGetBean
源码:
protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType,@Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {final String beanName = transformedBeanName(name);Object bean;// Eagerly check singleton cache for manually registered singletons.//缓存中获取Bean,解决了循环依赖问题Object sharedInstance = getSingleton(beanName);...缓存中没有走下面...if (mbd.isSingleton()) {//走 DefaultSingletonBeanRegistry#getSingleton ,方法会把bean加入“正在创建bean池”//然后调用ObjectFactory实例化BeansharedInstance = getSingleton(beanName, () -> {try {return createBean(beanName, mbd, args);}catch (BeansException ex) {// Explicitly remove instance from singleton cache: It might have been put there// eagerly by the creation process, to allow for circular reference resolution.// Also remove any beans that received a temporary reference to the bean.destroySingleton(beanName);throw ex;}});bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);}
第一次进来,缓存中是没有BeanA的,所有会走 getSingleton 方法,然后代码最终会走到AbstractAutowireCapableBeanFactory#doCreateBean
方法中 。
AbstractAutowireCapableBeanFactory#doCreateBean
源码:
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)throws BeanCreationException {// Instantiate the bean.BeanWrapper instanceWrapper = null;if (mbd.isSingleton()) {instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);}if (instanceWrapper == null) {//实例化BeaninstanceWrapper = createBeanInstance(beanName, mbd, args);}...省略...//如果是单利 ,如果是允许循环依赖,如果 beanName 出于创建中,已经被添加到“创建中的bean池”
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&isSingletonCurrentlyInCreation(beanName));if (earlySingletonExposure) {if (logger.isDebugEnabled()) {logger.debug("Eagerly caching bean '" + beanName +"' to allow for resolving potential circular references");}//把ObjectFactory 添加到 singletonFactories 中。addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));}try {//走依赖注入流程populateBean(beanName, mbd, instanceWrapper);exposedObject = initializeBean(beanName, exposedObject, mbd);}//缓存单利Bean的创建工厂,用于解决循环依赖
protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {Assert.notNull(singletonFactory, "Singleton factory must not be null");synchronized (this.singletonObjects) {//singletonObjects单利缓存中是否包含Beanif (!this.singletonObjects.containsKey(beanName)) {//提前暴露ObjectFactory,把ObjectFactory放到singletonFactories中,//后面解决循环依赖,获取Bean实例的时候会用到this.singletonFactories.put(beanName, singletonFactory);//早期单利bean缓存中移除Beanthis.earlySingletonObjects.remove(beanName);//把注册的Bean加入registeredSingletons中this.registeredSingletons.add(beanName);}}}
该方法中把BeanA实例化好之后,会把ObjectFactory存储到一个 singletonFactories
(HashMap)中来提前暴露Bean的创建工厂,用于解决循环依赖【重要】,然后调用 populateBean 走属性注入流程。
属性注入会通过BeanDefinition得到bean的依赖属性,然后调用 AbstractAutowireCapableBeanFactory#applyPropertyValues
,把属性应用到对象上。在applyPropertyValues
方法中最终调用 BeanDefinitionValueResolver#resolveValueIfNecessary
解析属性值,比如:ref=“beanB”
这种字符串引用变成 对象实例的引用。
在BeanDefinitionValueResolver
解析依赖的属性值即:BeanB的时候,同样会触发BeanB的实例化,代码会走到AbstractBeanFactory#doGetBean
,然后走方法 DefaultSingletonBeanRegistry#getSingleton
中把BeanB加入 singletonsCurrentlyInCreation
“创建中的Bean池”,然后代码会走到AbstractAutowireCapableBeanFactory#doCreateBean
方法中创建BeanB,
该方法中会先实例化BeanB,接着会把BeanB的ObjectFactory
存储到 singletonFactories (HashMap)
中来提前暴露Bean的创建工厂,用于解决循环依赖,然后调用 populateBean
走属性注入流程。
同样因为BeanB通过Setter 注入了 A,所以在 populateBean
属性注入流程中会解析 ref=“beanA”
为容器中的 BeanA 的实例。
然后会走到 AbstractBeanFactory#doGetBean
中获取BeanA的实例。这个时候流程就不一样了,我们先看一下 AbstractBeanFactory#doGetBean
中的代码
protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType,@Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {final String beanName = transformedBeanName(name);Object bean;// Eagerly check singleton cache for manually registered singletons.//从缓存中获取BeanObject sharedInstance = getSingleton(beanName);...省略...//如果缓存中没有Bean,就创建Beanif (mbd.isSingleton()) {sharedInstance = getSingleton(beanName, () -> {try {return createBean(beanName, mbd, args);}catch (BeansException ex) {// Explicitly remove instance from singleton cache: It might have been put there// eagerly by the creation process, to allow for circular reference resolution.// Also remove any beans that received a temporary reference to the bean.destroySingleton(beanName);throw ex;}});bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);}
在获取单利Bean的实例的时候是会先去单利Bean的缓存中去查看Bean是否已经存在,如果不存在,才会走DefaultSingletonBeanRegistry#getSingleton
方法创建Bean。
问题是:此刻单利Bean缓存中已经有BeanA了,因为在最开始BeanA已经出于“正在创建Bean池”中了。我们先来看一下是如何从缓存获取Bean的。
DefaultSingletonBeanRegistry#getSingleton(java.lang.String, boolean)
源码如下:
//allowEarlyReference :是否创建早期应用,主要用来解决循环依赖@Nullableprotected Object getSingleton(String beanName, boolean allowEarlyReference) {// Quick check for existing instance without full singleton lock//从Map中 singletonObjects = new ConcurrentHashMap<>(256); 获取单利Bean//【一级缓存】singletonObject缓存中是否有Bean , 它存储的是已经实例化好的BeanObject singletonObject = this.singletonObjects.get(beanName);//如果singletonObjects中没有Bean,但是Bean出于正在创建池中,即:Set<String> singletonsCurrentlyInCreation中有Bean,if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {//【二级缓存】从早期单例对象的缓存 earlySingletonObjects 中获取singletonObject = this.earlySingletonObjects.get(beanName);//早期单利对象缓存中也没有,但是允许循环依赖if (singletonObject == null && allowEarlyReference) {synchronized (this.singletonObjects) {// Consistent creation of early reference within full singleton locksingletonObject = this.singletonObjects.get(beanName);if (singletonObject == null) {singletonObject = this.earlySingletonObjects.get(beanName);if (singletonObject == null) {//【三级缓存】获取ObjectFactory , 对象创建工厂,得到Bean创建过程中提前暴露的工厂。ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);if (singletonFactory != null) {//通过工厂ObjectFactory 获取对象实例singletonObject = singletonFactory.getObject();//把对象存储到早期缓存中this.earlySingletonObjects.put(beanName, singletonObject);//把ObjectFactory移除this.singletonFactories.remove(beanName);}}}}}}return singletonObject;}
这里就是经典的三级缓存解决Spring循环依赖。你看到了,这里会先从 singletonObjects
单利Bean缓存集合中获取Bean(该缓存是实例化完成了的Bean),如果没有,就从earlySingletonObjects
早期对象缓存中获取Bean(该缓存中存放的是还未实例化完成的早期Bean),如果还是没有,就从singletonFactories
中得到暴露的ObjectFactory
来获取依赖的Bean。然后放入早期缓存中。并把ObjectFactory
从singletonFactories
中移除。最后返回Bean的实例。
由于在实例化BeanA的时候已经把BeanA的ObjectFactory
添加到了 singletonFactories
缓存中,那么这里就会走到 singletonFactory.getObject();
方法得到BeanA的实例,并且会把BeanA存储到 earlySingletonObjects
早期单利Bean缓存中。
BeanA的实例成功返回,那么BeanB的 setter注入成功,代表BeanB实例化完成,那么BeanA的setter方法注入成功,BeanA实例化完成。
prototype模式的循环依赖
对于prototype模式下的Bean不允许循环依赖,因为 这种模式下Bean是不做缓存的,所以就没法暴露ObjectFactory
,也就没办法实现循环依赖。
总结
不知道你有没有看晕,反正我但是在源码时的过程是比较辛苦的,这里需要你对前面Bean的实例化流程和属性注入流程比较熟悉,否则就会晕菜。
这里总结一下:
构造器循环依赖是不允许的,主要通过 singletonsCurrentlyInCreation
“正在创建Bean池” 把创建中的Bean缓存起来,如果循环依赖,同一个Bean势必会尝试进入该缓存2次,抛出循环依赖异常。
setter循环依赖是可以允许的。Spring是通过提前暴露未实例化完成的Bean的 ObjectFactory
来实现循环依赖的,这样做的目的是其他的Bean可以通过 ObjectFactory
引用到该Bean 。在获取依赖的Bean的时候使用到了三级缓存。
下面的面试题你会答了吗?
Spirng支持那种模式下的循环依赖(构造器?,setter?, prototype?)
Spring是如何处理构造器注入循环依赖的?
Spring是如何处理Setter注入循环依赖的?
来源:blog.csdn.net/u014494148/article/details/117879937
推荐:
最全的java面试题库
PS:因为公众号平台更改了推送规则,如果不想错过内容,记得读完点一下“在看”,加个“星标”,这样每次新文章推送才会第一时间出现在你的订阅列表里。点“在看”支持我们吧!
被问麻了,Spring 如何处理循环依赖?相关推荐
- Spring源码剖析-Spring如何处理循环依赖
前言 你是不是被这个骚气的标题吸引进来的,_ 喜欢我的文章的话就给个好评吧,你的肯定是我坚持写作最大的动力,来吧兄弟们,给我一点动力 Spring如何处理循环依赖?这是最近较为频繁被问到的一个面试题, ...
- Spring 如何处理循环依赖?
Spring 如何处理循环依赖? 文章目录 Spring 如何处理循环依赖? 项目环境 1.什么是循环依赖? 2.Spring 如何来处理循环依赖? 2.1 allowCircularReferenc ...
- 图解Spring解决循环依赖
点击上方蓝色"方志朋",选择"设为星标"回复"666"获取独家整理的学习资料! 来源:juejin.cn/post/684490412216 ...
- Spring 的循环依赖:真的必须非要三级缓存吗?
作者:青石路 www.cnblogs.com/youzhibing/p/14337244.html 写作背景 做 Java 开发的,一般都绕不开 Spring,那么面试中肯定会被问到 Spring 的 ...
- 【源码分析】Spring的循环依赖(setter注入、构造器注入、多例、AOP)
写在前面 首先最简单的循环依赖demo就是:A->B 且 B->A.本文围绕这个例子去讲解setter注入的循环依赖.构造器注入循环依赖.多例的循环依赖.带AOP的循环依赖.以下是一些结论 ...
- spring 循环依赖_简单说说 Spring 的循环依赖
作者 | 田伟然 回首向来萧瑟处,归去,也无风雨也无晴. 杏仁工程师,关注编码和诗词. 前言 本文最耗时间的点就在于想一个好的标题, 既要灿烂夺目,又要光华内敛,事实证明这比砍需求还要难! 由于对象之 ...
- Spring当中循环依赖很少有人讲,今天一起来学习!
网上关于Spring循环依赖的博客太多了,有很多都分析的很深入,写的很用心,甚至还画了时序图.流程图帮助读者理解,我看了后,感觉自己是懂了,但是闭上眼睛,总觉得还没有完全理解,总觉得还有一两个坎过不去 ...
- Spring如何处理循环引用
Spring如何处理循环引用 一,划重点 Spring处理循环依赖记录,要是写错了,喷轻一点. 解题核心: 1.AbstractBeanFactory的2个getSingleton方法 2.early ...
- Spring当中循环依赖很少有人讲,今天让我们来看看吧
网上关于Spring循环依赖的博客太多了,有很多都分析的很深入,写的很用心,甚至还画了时序图.流程图帮助读者理解,我看了后,感觉自己是懂了,但是闭上眼睛,总觉得还没有完全理解,总觉得还有一两个坎过不去 ...
最新文章
- .net core 生成html,ASP.NET Core 中如何将 .cshtml 视图文件生成为 .cs 文件
- VTK:相互作用之ShiftAndControl
- [LeetCode] Number of 1 Bits Reverse Integer - 整数问题系列
- Java 13:切换表达式的增强功能
- Scrapy_LinkExtractor
- Shell 脚本来监控 Linux 系统的内存
- Git Tag 使用
- mysql批量插入跟更新_Mysql批量插入和更新的性能-问答-阿里云开发者社区-阿里云...
- 第二次扩大会议(3.19)
- oracle备份文件命令,oracle备份命令使用实例
- 速达数据库服务器密码修改,速达,管家婆SQL Server帐套密码PJ方法 -电脑资料
- 高通 lcd dtsi
- 项目管理第十二章项目采购管理
- How to Read a Visualization Research Paper: Extracting the Essentials
- 深入理解微信二维码扫码登录的原理
- CQ CSP-S2021游记
- iOS UILable换行相关设置
- 用ajax表单全部提交
- Adobe Premiere视频添加水印图片教程,小白一看就会!
- SpringMVC控制器内请求转发关键字forward无效原因
热门文章
- Koffee设计模式学习之路(一) —— 模式学习总结思路
- linux虚拟桌面设置不同的背景,虚拟桌面增强器为Windows 10中的每个虚拟桌面设置不同的壁纸 | MOS86...
- ConcurrentHashMap源码解析
- 大连医科大学中山学院计算机科学与技术,大连医科大学中山学院计算机科学与技术专业2016年在山西理科高考录取最低分数线...
- 快速寻找9位数内的自幂数
- 用python打开ccd相机_用python测量CCD图像
- 仙童传奇-半导体的奇迹
- 数据降维处理(PCALAD)
- C#快递单号查询接口物流路由信息快递鸟api代码接入.Net
- Python配置新环境时,复制已经安装好虚拟环境的三种方法