目录

1.什么是循环依赖?

2.为什么会出现循环依赖?

3.面对循环依赖问题,我们该如何思考解决?

4.Spring是怎么解决循环依赖的?

5.总结


1.什么是循环依赖?

有两个类Order、Customer,Order对象依赖了Customer对象,同时Customer对象也依赖了Order对象,这就构成了循环依赖;

// Order依赖了Customer
public class Order{private Customer customer;
}// Customer依赖了Order
public class Customer{private Order order;
}

如果我们自己去创建对象,不考虑Spring的话,对象之间相互依赖也并没有什么问题;但是,在Spring中循环依赖就是一个问题了, 在Spring中,一个对象并不是简单的new出来了,而是会经过一系列的Bean的生命周期,就是因为有Bean的生命周期,所以才会出现循环依赖问题。当然,在Spring中,出现循环依赖的场景很多,有的场景Spring自动帮我们解决了,而有的场景则需要程序员自己来解决。

2.为什么会出现循环依赖?

在Spring中,从实例化Bean对象到初始化完成,大致要经历三步(事实上Spring创建Bean有很多步骤),依赖注入就是在属性赋值这一步中完成的,实例化bean之后,Spring需要给对象中的属性进行赋值(依赖注入),那么Spring中循环依赖的问题是怎么出现的呢?

@Component
public class ServiceA{@Autowiredprivate ServiceB b; // ServiceA依赖了ServiceB
}@Component
public class ServiceB{@Autowiredprivate ServiceA a; // ServiceB依赖了ServiceA
}

比如上面的ServiceA类,ServiceA类中存在一个ServiceB类的属性,属性名为b,当ServiceA类创建出来一个bean实例之后,就要给b属性去赋值,此时就会根据b属性的类型和属性名去BeanFactory中去获取ServiceB类所对应的bean,如果此时BeanFactory中存在ServiceB类对应的bean,就直接获取并赋值给b属性,如果此时BeanFactory中不存在ServiceB对应的bean,则需创建一个ServiceB对应的bean实例,然后赋值给b属性;这个过程会存在一个问题:如果此时ServiceB类在BeanFactory中还没有生成对应的bean,则会触发ServiceB类bean的创建,就会经过ServiceB创建bean的生命周期, 在创建ServiceB的bean的过程中,如果此时ServiceB中也存在一个ServiceA类的a属性,那么在创建ServiceB的bean的过程中就需要ServiceA类对应的bean,但是,触发ServiceB类bean创建的条件是ServiceA类bean在创建过程中的依赖注入,所以,这里就出现了循环依赖,这就是循环依赖的场景;在Spring中,通过某些机制可以帮助开发者解决部分循环依赖的问题,这个机制就是三级缓存。

3.面对循环依赖问题,我们该如何思考解决?

3.1. 在ServiceA、ServiceB之间增加缓存来打破循环

想要打破ServiceA和ServiceB之间的这种循环状态,那就不能让双方在创建bean实例时都触发对方bean的创建,否则循环依赖问题依然得不到解决;那么怎么做呢?可以通过增加缓存来实现,具体做法如下:

ServiceA创建bean的过程中,在进行依赖注入之前,先把实例化出来的ServiceA的原始对象(这里的原始对象是指没有经过属性赋值,或只经过了部分属性赋值的bean)放入缓存(提前暴露,只要放到缓存中,其它bean需要时就可以从缓存中拿了),放入缓存后,再进行依赖注入,而ServiceA依赖了ServiceB,所以,接下来要给ServiceA找一个ServiceB类型的bean,赋值给ServiceA的b属性;如果ServiceB的bean不存在,则会触发ServiceB的bean的创建,经过ServiceB创建bean的生命周期,而创建ServiceB的bean的过程和ServiceA一样,也是先创建一个ServiceB的原始对象,然后把ServiceB的原始对象提前暴露出来放入缓存中,接着再对ServicrB的原始对象进行依赖注入,而ServicrB也依赖了ServiceA,此时能从缓存中拿到ServiceA的原始对象(这里只是ServiceA的原始对象,还不是经过完整生命周期的bean)赋值给ServiceB的a属性,ServiceB就可以正常的完成依赖注入,这样ServiceA和ServiceB都能完成正常bean的创建,就不会陷入循环中。

问题:ServiceB在进行依赖注入时,赋值给ServiceB属性的是ServiceA的原始对象,并不是经过完整生命周期的对象,这样赋值正确吗?

答:在整个bean创建过程中,都只有一个ServiceA对象,所以对于ServiceB而言,即使在属性注入时,注入的是ServiceA原始对象,也没有影响,因为这里赋的是ServiceA在内存中的地址值,ServiceA原始对象在后续的生命周期流程中在堆中的没有发生变化,当ServiceA经过了完整生命周期后,前面赋值给ServiceB属性的ServiceA对象,也就变成了具备完整生命周期的对象。

3.2.要解决循环依赖,还需要考虑AOP
1) 基于上面的场景,不知道有没有发现一个问题?

从缓存中取出ServiceA的原始对象注入给ServiceB的a属性之后,待ServiceB完成创建后,回到ServiceA创建bean的流程中,在ServiceA后面的生命周期步骤中,当执行到初始化后这一步,ServiceA的原始对象进行了AOP,生成了一个代理对象,对于ServiceA来说,最终生成的bean对象应该是进行了AOP之后的代理对象,而赋值给ServiceB的a属性的bean对象,不是ServiceA进行AOP之后的代理对象,而是原始对象;所以,如果仅仅是基于上面<3.1. 在ServiceA、ServiceB之间增加缓存来打破循环>来解决的话,就会出现这样的问题;

2) 怎么解决ServiceB依赖的ServiceA对象和最终的ServiceA对象不是同一个对象?

要解决这个问题,就得提前进行AOP,如果要解决循环依赖,但依然是在初始化后进行AOP,那么赋值给ServiceB的a属性的那个对象就不是代理对象;按照正常的bean的生命周期流程,AOP是在初始化后进行的,但我们要考虑循环依赖的场景,就需要提前进行AOP,当然,这与正常的创建bean的生命周期流程不符,因为,正常bean的创建是在初始化后这一步进行AOP的,所以,也只有在出现循环依赖的情况下(例如ServiceA出现了循环依赖),才需要提前进行AOP,最后将进行过AOP步骤的对象缓存起来;

3.3.增加三级缓存

基于<3.2.要解决循环依赖,还需要考虑AOP>,还存在这样的问题:

1)出现循环依赖是怎么判断的?

增加集合来记录(Spring中是singletonsCurrentlyInCreation);当ServiceA正在创建时,将该bean对应的beanName存到集合中,当ServiceB创建需要用到ServiceA时,但发现ServiceA仍在创建中,那么就可以断定ServiceA出现了循环依赖;

2)怎么判断出现了循环依赖的bean是否要进行AOP?

基于BeanPostProcessor机制,BeanPostProcessor会根据传入的bean去判断是否要进行AOP,如果需要进行AOP就生成代理对象并返回,否则就把传入的原始bean对象返回;在这里就要增加三级缓存了,增加一个Map,key是beanName,value是判断是否要进行AOP的Lambda表达式;

4.Spring是怎么解决循环依赖的?

4.1.Spring 通过三级缓存解决了循环依赖

/** Cache of singleton objects: bean name to bean instance. */
// 单例池
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);/** Cache of singleton factories: bean name to ObjectFactory. */
//三级缓存
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);/** Cache of early singleton objects: bean name to bean instance. */
//二级缓存
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);

一级缓存 : Map<String,Object> singletonObjects,单例池,用于保存实例化、属性赋值(依赖注入)、初始化完成的 bean实例(经过完整的生命周期的bean对象);

二级缓存 : Map<String,Object> earlySingletonObjects,早期"曝光"对象,用于保存实例化完成的bean实例(暂时还没有经过完整的生命周期的bean对象);

三级缓存 : Map<String,ObjectFactory<?>> singletonFactories,早期"曝光"对象工厂,用于保存 bean 创建工厂,以便于后面扩展有可能创建代理对象,二级缓存中存储的就是从这个工厂中获取到的对象。

4.2.Spring解决循环依赖源码分析

1) 向三级缓存添加

在创建bean的过程中,当执行createBeanInstance()方法创建完原始的bean对象,之后调用populateBean方法进行属性赋值(依赖注入),但是在属性赋值之前,Spring会判断当前正在创建的bean是否支持循环依赖,如果支持,就会添加到三级缓存(singletonFactories),singletonFactories中存的是某个beanName对应的ObjectFactory,这个ObjectFactory 是一个函数式接口,所以支持Lambda表达式:() -> getEarlyBeanReference(beanName, mbd, bean),就是一个ObjectFactory,执行该Lambda表达式就会去执行getEarlyBeanReference方法,注意这里只会往三级缓存添加,并不会执行该Lambda表达式,具体调用过程是在后面调用ObjectFactory的getObject方法时调用,具体源码如下:

// Eagerly cache singletons to be able to resolve circular references
// even when triggered by lifecycle interfaces like BeanFactoryAware.
//如果当前bean支持循环依赖就会提前往三级缓存里存
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {if (logger.isTraceEnabled()) {logger.trace("Eagerly caching bean '" + beanName +"' to allow for resolving potential circular references");}// 为了解决循环依赖提前缓存单例创建工厂// 循环依赖-添加到三级缓存// 执行到这里的时候,Spring并不知道当前正在创建的bean会不会出现循环依赖,先缓存起来,目的是后面 // 判断出现循环依赖的时候使用addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {Assert.notNull(singletonFactory, "Singleton factory must not be null");synchronized (this.singletonObjects) {if (!this.singletonObjects.containsKey(beanName)) {this.singletonFactories.put(beanName, singletonFactory);this.earlySingletonObjects.remove(beanName);this.registeredSingletons.add(beanName);}}
}

2)Lambda表达式中的getEarlyBeanReference方法分析

getEarlyBeanReference方法会去执行SmartInstantiationAwareBeanPostProcessor中的getEarlyBeanReference方法, 在整个Spring中,默认只有AbstractAutoProxyCreator类实现了SmartInstantiationAwareBeanPostProcessor接口中getEarlyBeanReference方法,而该类就是用来进行AOP的,AOP是通过一个BeanPostProcessor来实现的,开启AOP之后,Spring容器中就会增加一个BeanPostProcessor,这个BeanPostProcessor就是 AnnotationAwareAspectJAutoProxyCreator,它的父类是AbstractAutoProxyCreator,而在 Spring中AOP利用的是JDK的动态代理,或者是CGLib的动态代理,如果给一个类中的某个方法设置了切面,那么这个类最终就需要生成一个代理对象;

protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {Object exposedObject = bean;if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {for (SmartInstantiationAwareBeanPostProcessor bp : getBeanPostProcessorCache().smartInstantiationAware) {exposedObject = bp.getEarlyBeanReference(exposedObject, beanName);}}return exposedObject;
}

3) 什么时候执行Lambda表达式,并执行getEarlyBeanReference方法?

当判断出现循环依赖时,执行ObjectFactory的getObject()方法,就会执行getEarlyBeanReference方法,循环依赖是通过getSingleton方法中的isSingletonCurrentlyInCreation()来判断的;

/*** Return the (raw) singleton object registered under the given name.* <p>Checks already instantiated singletons and also allows for an early* reference to a currently created singleton (resolving a circular reference).* @param beanName the name of the bean to look for* @param allowEarlyReference whether early references should be created or not* @return the registered singleton object, or {@code null} if none found*/
@Nullable
protected Object getSingleton(String beanName, boolean allowEarlyReference) {// Quick check for existing instance without full singleton lockObject singletonObject = this.singletonObjects.get(beanName);if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {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<?> singletonFactory = this.singletonFactories.get(beanName);if (singletonFactory != null) {singletonObject = singletonFactory.getObject();this.earlySingletonObjects.put(beanName, singletonObject);this.singletonFactories.remove(beanName);}}}}}}return singletonObject;
}

4) AbstractAutoProxyCreator的getEarlyBeanReference方法到底做了些什么?

首先得到一个cachekey,cachekey就是beanName,然后把beanName和bean(这是原始对象)存入earlyProxyReferences中,调用 wrapIfNecessary方法, wrapIfNecessary方法是用来进行AOP的,wrapIfNecessary方法中会判断当前bean是否要进行AOP,如果要则要生成一个代理对象,否则还是返回原始对象,而Spring用于解决循环依赖的getEarlyBeanReference方法和Bean的生命周期初始化后的postProcessAfterInitialization方法中都会调用wrapIfNecessary方法;

// AbstractAutoProxyCreator
// 提前进行AOP
@Override
public Object getEarlyBeanReference(Object bean, String beanName) {Object cacheKey = getCacheKey(bean.getClass(), beanName);//记录当前的bean进行了AOPthis.earlyProxyReferences.put(cacheKey, bean);return wrapIfNecessary(bean, beanName, cacheKey);
}
//正常进行AOP的地方
@Override
public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {if (bean != null) {Object cacheKey = getCacheKey(bean.getClass(), beanName);//判断当前bean有没有提前进行AOPif (this.earlyProxyReferences.remove(cacheKey) != bean) {return wrapIfNecessary(bean, beanName, cacheKey);}}return bean;
}

说明:只有出现了循环依赖,Spring才需要将AOP提前,才会执行getEarlyBeanReference方法,调用wrapIfNecessary方法,但是,不管有没有出现循环依赖都会执行初始化后的postProcessAfterInitialization方法,进而调用其中的wrapIfNecessary方法;earlyProxyReferences的作用是在Spring出现了循环依赖的情况下,记录提前进行了AOP的bean,避免在进行初始化后这一步时,重复进行AOP,生成新的代理对象(wrapIfNecessary方法会判断当前bean是否要进行AOP,如果需要则生成代理对象,如果不需要还是返回原始对象,所以上述所说的提前进行AOP,最终返回的对象并不一定是代理对象)。

5) 如果提前进行AOP,在初始化后这一步就不会再进行AOP,返回的对象就不是代理对象而是原始对象,由于最终要把代理对象放到单例池,这个对象是从哪里获取的?

从二级缓存中获取

 //说明:当某个正在创建的bean出现了循环依赖,同时该bean也要进行AOP,那么就会提前进行AOP,一但提前进行了AOP,就会生成代理对象,//那么在初始化后这一步就不会再进行AOP,返回的对象就不是代理对象而是原始对象,由于最终要把代理对象放到单例池,所以,这里还要去二级缓存去获取if (earlySingletonExposure) {Object earlySingletonReference = getSingleton(beanName, false);if (earlySingletonReference != null) {if (exposedObject == bean) {exposedObject = earlySingletonReference;}else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {// beanName被哪些bean依赖了,现在发现beanName所对应的bean对象发生了改变,那么则会报错String[] dependentBeans = getDependentBeans(beanName);Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);for (String dependentBean : dependentBeans) {if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {actualDependentBeans.add(dependentBean);}}if (!actualDependentBeans.isEmpty()) {throw new BeanCurrentlyInCreationException(beanName,"Bean with name '" + beanName + "' has been injected into other beans [" +StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +"] in its raw version as part of a circular reference, but has eventually been " +"wrapped. This means that said other beans do not use the final version of the " +"bean. This is often the result of over-eager type matching - consider using " +"'getBeanNamesForType' with the 'allowEagerInit' flag turned off, for example.");}}}}
对于上面 exposedObject == bean 的判断,大多数情况下是相同的,但是也有某些场景会不同,比如Spring的异步调用,此时List<BeanPostProcessor> beanPostProcessors中会增加一个AsyncAnnotationBeanPostProcessor,经过AsyncAnnotationBeanPostProcessor处理的bean就会生成新的代理对象;

4.3.为什么要有二级缓存和三级缓存?

只有二级缓存确实可以解决循环依赖的问题,但有一个前提:这个bean没被AOP进行切面代理,如果这个bean被AOP进行了切面代理,那么只使用二级缓存(这里的二级缓存是指singletonFactories,缓存的是Lambda表达式)是无法解决问题,这样会导致每次执行singleFactory.getObject()方法都产生一个新的代理对象,无法保证对象的单例性,所以还要借助另外一个缓存来保存产生的代理对象;如果出现循环依赖+AOP时,多个地方注入这个动态代理对象需要保证都是同一个对象,而三级缓存中的取出来的动态代理对象每次都是新对象,地址值不一样。

三级缓存的作用是为了解决Spring中Bean在进行依赖注入时发生的循环依赖。如果不需要AOP,那么只需要二级缓存即可实现,如果有AOP,其实二级缓存也能够实现,但是会打破Bean的生命周期,这不符合Spring的设计原则,由于需要把AOP对象放入二级缓存中,那么就必须在所有需要AOP处理的Bean对象初始化之前就对Bean对象进行后置处理(生成AOP对象),即使没有出现循环依赖,这并不是Spring想看到的,所以Spring引入了三级缓存,而且存入的是结构,ObjectFactory是一个Lambda表达式,相当于一个回调函数,当出现循环依赖的时候,会执行Lambda表达式,获取到Bean对象或者 AOP代理对象,再将Bean对象或者 AOP代理对象存入二级缓存中,如果之后还有循环依赖指向该对象(类似 A 依赖 B , B 依赖 A , C 依赖 A这种情况),就直接从二级缓存里面获取,从而解决了循环依赖。(为什么不直接在二级缓存里存放Lambda表达式?因为同一个Lambda表达式每执行一次,就会生成一个新的代理对象,不能保证单例)。

4.4.Spring有没有解决singleton下的构造注入产生的循环依赖?

@Component
public class ServiceA {private ServiceB serviceB;public ServiceA(ServiceB serviceB) {this.serviceB = serviceB;}public void test() {System.out.println(serviceB);}}@Component
public class ServiceB {private ServiceA serviceA;public ServiceB(ServiceA serviceA) {this.serviceA = serviceA;}public void test() {System.out.println(serviceA);}}

基于构造注入的方式下产生的循环依赖也是无法解决的,原因是ServiceA通过构造方法创建对象时,无法创建ServiceA所依赖的ServiceB对象,获取所依赖bean对象的引用。
创建bean对象,执行顺序为:1.调用构造方法  2. 放到三级缓存  3. 属性赋值

调用构造方法时会触发所依赖的bean对象的创建,createBeanInstance是调用构造方法创建bean对象,在里面会注入构造方法中所依赖的bean,而此时并没有执行到addSingletonFactory方法来添加主bean对象的创建工厂到三级缓存singletonFactories中。

4.5.Spring有没有解决多例下的set注入产生的循环依赖?
多实例并不会调用getSingleton方法,自然也不会缓存。也就是说,无论多少次创建多实例bean都不会调用beforeSingletonCreation;

注意:当两个bean的scope都是prototype的时候,才会出现异常,如果其中一个是singleton,就不会出现。

4.6.Spring解决循环依赖的机理

Spring为什么可以解决set + singleton模式下循环依赖?根本原因在于:该方式可以做到将"实例化Bean"和"给Bean属性赋值"这两个动作分开去完成,实例化Bean的时候:调用无参数构造方法来完成,此时可以先不给属性赋值,可以提前将该Bean对象“曝光”给外界,给Bean属性赋值的时候,调用setter方法来完成。两个步骤是完全可以分离开去完成的,并且这两步不要求在同一个时间点上完成。也就是说,Bean都是单例的,我们可以先把所有的单例Bean实例化出来,放到一个集合当中(我们可以称之为缓存),所有的单例Bean全部实例化完成之后,以后我们再慢慢的调用setter方法给属性赋值,这样就解决了循环依赖的问题。

5.总结

总结一下三级缓存:

1. singletonObjects:缓存经过了完整生命周期的bean

2. earlySingletonObjects:缓存未经过完整生命周期的bean,如果某个bean出现了循环依赖, 就会提前把这个暂时未经过完整生命周期的bean放入earlySingletonObjects中,这个bean如果要进行AOP,那么就会把代理对象放入earlySingletonObjects中,否则就是把原始对象放入 earlySingletonObjects,但是不管怎么样,即使是代理对象,代理对象所代理的原始对象也是没有经过完整生命周期的,所以放入earlySingletonObjects中的可以统一认为是未经过完整生命周期的bean。

3. singletonFactories:缓存的是一个ObjectFactory,也就是一个Lambda表达式。在每个Bean 的创建过程中,经过实例化得到一个原始对象后,都会提前基于原始对象构造一个Lambda表达式,并保存到三级缓存中,如果当前Bean没有出现循环依赖,那么这个Lambda表达式不会被执行,当前bean按照自己的生命周期正常执行,执行完后直接把当前bean放入singletonObjects中,如果当前bean在依赖注入时发现出现了循环依赖 (当前正在创建的bean被其他bean依赖了),则从三级缓存中拿到Lambda表达式,并执行 Lambda表达式得到一个对象,并把得到的对象放入二级缓存(如果当前Bean需要AOP,那么执行Lambda表达式,得到就是对应的代理对象,如果无需AOP,则直接得到一个原始对象)。

Spring之循环依赖源码解析相关推荐

  1. Spring循环依赖源码剖析

    Spring循环依赖源码剖析 一.场景介绍 二.整理执行流程总结 三.源码分析 编写测试类 /*** 测试循环依赖*/@Testpublic void testCyclicDependence(){A ...

  2. spring 注解试事物源码解析

    spring 注解试事物源码解析 基于xml注解式事务入口 public class TxNamespaceHandler extends NamespaceHandlerSupport {stati ...

  3. Spring事件机制Event源码解析(未完待续)

    Spring事件机制Event源码解析(未完待续) 监听器: ApplicationEvent事件 ApplicationListener监听器(观察者) ApplicationEventMultic ...

  4. spring循环依赖源码分析

    以下讲的循环依赖时基于单例模式下的@Autowired或者set方法的spring的循环依赖 spring循环依赖 搞懂之前需要了解bean的创建过程 大概步骤如下: 1.org.springfram ...

  5. Spring AOP 超详细源码解析

    知识章节 基础知识 什么是 AOP AOP 的全称是 "Aspect Oriented Programming",即面向切面编程 在 AOP 的思想里面,周边功能(比如性能统计,日 ...

  6. 帮你深度探寻Spring循环依赖源码实现!面经解析

    前言 我想,很多人和我一样在煎熬中度过着2021年,也经历了不少困难,随着国家对疫情的控制,互联网行业又重新迎来了生机. 我在2021年拿到了阿里Java研发岗的offer,也算是正式提桶进大厂的打工 ...

  7. 【Spring Boot实战】源码解析Spring Boot自动配置原理

    一.简介 Spring致力于让Java开发更简单,SpringBoot致力于让使用Spring进行Java开发更简单,SpringCloud致力于基于SpringBoot构建微服务生态圈,让微服务开发 ...

  8. Spring aware使用与源码解析

    目录 1. aware接口的作用 2. 常用aware接口及作用 3. 使用样例:ApplicationContextAware 在Bean中获取上下文 4. 自定义aware的方式 4.1 定义继承 ...

  9. 帮你深度探寻Spring循环依赖源码实现,Java开发经验的有效总结

    前言 该文档在Github上收获40K+star的Java面试神技(这赞数,质量多高就不用我多说了吧)非常全面,包涵Java基础.Java集合.JavaWeb.Java异常.OOP.IO与NIO.反射 ...

最新文章

  1. 杭电1276--士兵队列训练问题
  2. 为什么一些人喜欢在java代码中能加final的变量都加上final
  3. python零基础能学吗 知乎-Python零基础学习能学好吗?老男孩Python面授班
  4. webpack 读取文件夹下的文件_TypeScript完全解读(26课时)_1.TypeScript完全解读-开发环境搭建...
  5. php默认访问的文件,PHP 网站修改默认访问文件的nginx配置
  6. tensorflow实现宝可梦数据集迁移学习
  7. jqgrid mysql 分页_jQgrid 分页显示
  8. 【Python】Python库之游戏开发
  9. 汇编:将指定的内存中连续N个字节填写成指定的内容
  10. java中输入的程序_Java中输入的用法
  11. 小米研发团队从400人增至3700人;iOS 13.3“杀后台”问题缓解;FreeBSD 12.1发布|极客头条...
  12. 开发时解决数据回显的小方法
  13. 第五讲 交错级数、绝对收敛和条件收敛
  14. 有10亿个整数,要求选取重复次数最多的100个整数
  15. 如何设置 ASP.NET Core 程序监听的 IP 和端口
  16. 电脑没有使用计算机进入睡眠状态,电脑打不开,屏上显示:无视频输入,进入睡眠模式。怎么处理...
  17. Tracy JS 小笔记 - 数据结构 栈,队列,链表,字典,集合,哈希表(散列表)
  18. 文件夹选择对话框 JS实现的两种方案
  19. 无模型预测控制(model-free predictive control)+ESO
  20. 应急照明市电检测_图文分析应急照明如何供电? 如何接线?

热门文章

  1. Ubuntu下编译NASA开源深空影像处理软件:Ames Stereo Pipeline
  2. 在Ubuntu下安装Samba文件服务器(译)
  3. Android设备设置Apn相关
  4. Web后端开发入门(3)
  5. 给在校同学的几个建议
  6. 微信H5支付(MWEB)、扫码支付(NATIVE)、APP支付(APP)
  7. 数组元素的遍历及数组常用方法-B站晓舟学习报告笔记
  8. 【正点原子Linux连载】 第十九章 CAN Bus 摘自【正点原子】I.MX6U嵌入式Qt开发指南V1.0.2
  9. 用链脉智能名片,一天帮你交换上百张名片
  10. 面试被问你的职业规划是什么?