• Spring IOC源码:finishBeanFactoryInitialization详解


    Spring源码系列:

    Spring IOC源码:简单易懂的Spring IOC 思路介绍
    Spring IOC源码:核心流程介绍
    Spring IOC源码:ApplicationContext刷新前准备工作
    Spring IOC源码:obtainFreshBeanFactory 详解(上)
    Spring IOC源码:obtainFreshBeanFactory 详解(中)
    Spring IOC源码:obtainFreshBeanFactory 详解(下)
    Spring IOC源码:<context:component-scan>源码详解
    Spring IOC源码:invokeBeanFactoryPostProcessors 后置处理器详解
    Spring IOC源码:registerBeanPostProcessors 详解
    Spring IOC源码:实例化前的准备工作
    Spring IOC源码:finishBeanFactoryInitialization详解
    Spring IoC源码:getBean 详解
    Spring IoC源码:createBean( 上)
    Spring IoC源码:createBean( 中)
    Spring IoC源码:createBean( 下)
    Spring IoC源码:finishRefresh 完成刷新详解

    前言

    这篇文章开始讲解Bean的实例化过程,也就是refresh中的finishBeanFactoryInitialization方法,该方法是IOC中最核心也是最复杂的,后续会分为几篇文章进行详细讲解。该方法会将beanFactory工厂中的定义信息进行实例化及属性注入;

    正文

    进入fresh中的finishBeanFactoryInitialization方法

    	protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
    		// 判断beanFactory工厂中是否有ConversionService或其定义信息,没有则实例化创建一个ConversionService转换器
    		if (beanFactory.containsBean(CONVERSION_SERVICE_BEAN_NAME) &&
    				beanFactory.isTypeMatch(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class)) {
    			beanFactory.setConversionService(
    					beanFactory.getBean(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class));
    		}
    
    		// Register a default embedded value resolver if no bean post-processor
    		// (such as a PropertyPlaceholderConfigurer bean) registered any before:
    		// at this point, primarily for resolution in annotation attribute values.
    		//如果工厂中没有嵌入式解析器,则创建一个默认的
    		if (!beanFactory.hasEmbeddedValueResolver()) {
    			beanFactory.addEmbeddedValueResolver(strVal -> getEnvironment().resolvePlaceholders(strVal));
    		}
    
    		// Initialize LoadTimeWeaverAware beans early to allow for registering their transformers early.
    		//实例化LoadTimeWeaverAware对象
    		String[] weaverAwareNames = beanFactory.getBeanNamesForType(LoadTimeWeaverAware.class, false, false);
    		for (String weaverAwareName : weaverAwareNames) {
    			getBean(weaverAwareName);
    		}
    
    		// Stop using the temporary ClassLoader for type matching.
    		beanFactory.setTempClassLoader(null);
    
    		// Allow for caching all bean definition metadata, not expecting further changes.
    		//冻结bean定义信息,不允许bean定义信息被修改,因为要开始实例化了
    		beanFactory.freezeConfiguration();
    
    		// 实例化剩余的懒加载的bean
    		beanFactory.preInstantiateSingletons();
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    beanFactory.preInstantiateSingletons(),见方法1详解

    方法1:preInstantiateSingletons

    public void preInstantiateSingletons() throws BeansException {
    		if (logger.isTraceEnabled()) {
    			logger.trace("Pre-instantiating singletons in " + this);
    		}
    
    		// Iterate over a copy to allow for init methods which in turn register new bean definitions.
    		// While this may not be part of the regular factory bootstrap, it does otherwise work fine.
    		//复杂一份新的beanName用于遍历,因为bean中的init方法可能会创建新的bean定义信息
    		List<String> beanNames = new ArrayList<>(this.beanDefinitionNames);
    
    		// Trigger initialization of all non-lazy singleton beans...
    		//遍历所有非懒加载beanName
    		for (String beanName : beanNames) {
    			//该bean可能存在父类定义,这里合并后封装成RootBeanDefinition 对象,后续操作都是使用RootBeanDefinition 
    			RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);
    			//不是抽象、且非懒加载的单例
    			if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
    				//判断是否的factoryBean
    				if (isFactoryBean(beanName)) {
    					//获取factoryBean本对象,这里如果beanName带&前缀的话获取的是FactoryBean对象,如果是不带前缀的beanName,则获取的是FactoryBean中的getObject返回的实例。
    					Object bean = getBean(FACTORY_BEAN_PREFIX + beanName);
    					if (bean instanceof FactoryBean) {
    						final FactoryBean<?> factory = (FactoryBean<?>) bean;
    						//用于标识是否是急切初始化
    						boolean isEagerInit;
    						if (System.getSecurityManager() != null && factory instanceof SmartFactoryBean) {
    							isEagerInit = AccessController.doPrivileged((PrivilegedAction<Boolean>)
    											((SmartFactoryBean<?>) factory)::isEagerInit,
    									getAccessControlContext());
    						}
    						else {
    						//如果实现了SmartFactoryBean 接口,则该Bean会提前进行创建
    							isEagerInit = (factory instanceof SmartFactoryBean &&
    									((SmartFactoryBean<?>) factory).isEagerInit());
    						}
    						//如果是急切初始化,提前进行生成
    						if (isEagerInit) {
    							getBean(beanName);
    						}
    					}
    				}
    				else {
    				//非FactoryBean,直接实例化Bean
    					getBean(beanName);
    				}
    			}
    		}
    
    		// Trigger post-initialization callback for all applicable beans...
    		//遍历所有的Bean,如果为SmartInitializingSingleton类型,则调用其afterSingletonsInstantiated方法。
    		for (String beanName : beanNames) {
    		//从一级缓存中获取对象
    			Object singletonInstance = getSingleton(beanName);
    			if (singletonInstance instanceof SmartInitializingSingleton) {
    				final SmartInitializingSingleton smartSingleton = (SmartInitializingSingleton) singletonInstance;
    				if (System.getSecurityManager() != null) {
    					AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
    						smartSingleton.afterSingletonsInstantiated();
    						return null;
    					}, getAccessControlContext());
    				}
    				else {
    					smartSingleton.afterSingletonsInstantiated();
    				}
    			}
    		}
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67

    1、FactoryBean翻译过来就是工厂bean的意思,它跟我们平时在配置文件中配置的bean有一点区别,平常的bean或者getBean方法中以反射的形式进行创建,而实现了FactoryBean接口的Bean会通过其getObject方法返回一个实例。如:

    public class MyFactoryBean implements FactoryBean {
    	@Override
    	public Object getObject() throws Exception {
    		Student student=new Student();
    		return student;
    	}
    
    	@Override
    	public Class<?> getObjectType() {
    		return Student.class;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    假如该工厂bean的beanName为myFactoryBean,当我们传入myFactoryBean去获取Bean的时候返回的其实是Student对象,如果传入的是带前缀的&myFactoryBean,则返回的是MyFactoryBean 本身这个实例。

    2、在整个IOC中我们经常会看到getBean(beanName)方法,其参数是一个beanName。该方法是将beanFactory工厂中的定义信息进行实例化的节点,传入beanName,会先从缓存中查询是否存在,存在则返回,不存在则会进行创建。

    getMergedLocalBeanDefinition(beanName),见方法2详解
    isFactoryBean(beanName),见方法5详解

    方法2:getMergedLocalBeanDefinition

    	protected RootBeanDefinition getMergedLocalBeanDefinition(String beanName) throws BeansException {
    		// 从缓存中查询是否存在该定义信息
    		RootBeanDefinition mbd = this.mergedBeanDefinitions.get(beanName);
    		if (mbd != null && !mbd.stale) {
    			return mbd;
    		}
    		return getMergedBeanDefinition(beanName, getBeanDefinition(beanName));
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    	protected RootBeanDefinition getMergedBeanDefinition(String beanName, BeanDefinition bd)
    			throws BeanDefinitionStoreException {
    
    		return getMergedBeanDefinition(beanName, bd, null);
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5

    getMergedBeanDefinition(beanName, bd, null),见方法3详解

    方法3:getMergedBeanDefinition

    protected RootBeanDefinition getMergedBeanDefinition(
    			String beanName, BeanDefinition bd, @Nullable BeanDefinition containingBd)
    			throws BeanDefinitionStoreException {
    
    		synchronized (this.mergedBeanDefinitions) {
    			RootBeanDefinition mbd = null;
    			RootBeanDefinition previous = null;
    
    			// Check with full lock now in order to enforce the same merged instance.
    			if (containingBd == null) {
    			//尝试从缓存中获取
    				mbd = this.mergedBeanDefinitions.get(beanName);
    			}
    
    			if (mbd == null || mbd.stale) {
    				previous = mbd;
    				mbd = null;
    				//判断该bean是否存在父类,如果没有则判断本身是否是RootBeanDefinition,否则创建一个新的RootBeanDefinition
    				if (bd.getParentName() == null) {
    					// Use copy of given root bean definition.
    					//一般BeanDefinition在被加载后是GenericBeanDefinition或ScannedGenericBeanDefinition
    					if (bd instanceof RootBeanDefinition) {
    						mbd = ((RootBeanDefinition) bd).cloneBeanDefinition();
    					}
    					else {
    						mbd = new RootBeanDefinition(bd);
    					}
    				}
    				else {
    					// Child bean definition: needs to be merged with parent.
    					BeanDefinition pbd;
    					try {
    						//获取父类名称
    						String parentBeanName = transformedBeanName(bd.getParentName());
    						//如果父类与当前beanName不一致,则递归调用当前方法进行查找合并,因为父类也有可能存在父类。。
    						if (!beanName.equals(parentBeanName)) {
    							pbd = getMergedBeanDefinition(parentBeanName);
    						}
    						else {
    							//获取父类的BeanFactory 
    							BeanFactory parent = getParentBeanFactory();
    							//只有在存在父BeanFactory的情况下,才允许父定义beanName与自己相同,否则就是将自己设置为父定义
    							if (parent instanceof ConfigurableBeanFactory) {
    								pbd = ((ConfigurableBeanFactory) parent).getMergedBeanDefinition(parentBeanName);
    							}
    							else {
    								throw new NoSuchBeanDefinitionException(parentBeanName,
    										"Parent name '" + parentBeanName + "' is equal to bean name '" + beanName +
    										"': cannot be resolved without an AbstractBeanFactory parent");
    							}
    						}
    					}
    					catch (NoSuchBeanDefinitionException ex) {
    						throw new BeanDefinitionStoreException(bd.getResourceDescription(), beanName,
    								"Could not resolve parent bean definition '" + bd.getParentName() + "'", ex);
    					}
    					// Deep copy with overridden values.
    					//创建新的,并覆盖原来的RootBeanDefinition
    					mbd = new RootBeanDefinition(pbd);
    					//设置被覆盖的bean定义
    					mbd.overrideFrom(bd);
    				}
    
    				// Set default singleton scope, if not configured before.
    				//如果没有配置Scope属性,则默认按单例处理
    				if (!StringUtils.hasLength(mbd.getScope())) {
    					mbd.setScope(RootBeanDefinition.SCOPE_SINGLETON);
    				}
    
    				// A bean contained in a non-singleton bean cannot be a singleton itself.
    				// Let's correct this on the fly here, since this might be the result of
    				// parent-child merging for the outer bean, in which case the original inner bean
    				// definition will not have inherited the merged outer bean's singleton status.
    				if (containingBd != null && !containingBd.isSingleton() && mbd.isSingleton()) {
    					//同步Scope属性值
    					mbd.setScope(containingBd.getScope());
    				}
    
    				// Cache the merged bean definition for the time being
    				// (it might still get re-merged later on in order to pick up metadata changes)
    				if (containingBd == null && isCacheBeanMetadata()) {
    					//存储到缓存中
    					this.mergedBeanDefinitions.put(beanName, mbd);
    				}
    			}
    			//如果原先的定义信息不为空,则拷贝部分先前的属性值到当前的合并的定义中
    			if (previous != null) {
    				copyRelevantMergedBeanDefinitionCaches(previous, mbd);
    			}
    			return mbd;
    		}
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92

    这里稍微整理一下:
    1、如果当前的bean定义不存在父类,则判断当前定义是否为RootBeanDefinition,如果是则直接返回,不是则创建一个新的合并的Bean定义信息(RootBeanDefinition)。

    2、如果存在父定义,并且父定义beanName与子定义信息的beanName不同时,则递归创建RootBeanDefinition,因为父类也存在父类,即子类有父类,爷爷类,太爷爷类。。。

    3、如果父定义的beanName与自定义的beanName相同时,如果其父工厂为ConfigurableBeanFactory类型,则调用父工厂getMergedBeanDefinition方法,从父工厂中进行查找合并。

    transformedBeanName(bd.getParentName()),见方法4详解

    方法4:transformedBeanName

    	protected String transformedBeanName(String name) {
    		return canonicalName(BeanFactoryUtils.transformedBeanName(name));
    	}
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    	public static String transformedBeanName(String name) {
    		Assert.notNull(name, "'name' must not be null");
    		//如果名称带不以&符号开头则直接返回
    		if (!name.startsWith(BeanFactory.FACTORY_BEAN_PREFIX)) {
    			return name;
    		}
    		//切割&符号后面的字符为beanName
    		return transformedBeanNameCache.computeIfAbsent(name, beanName -> {
    			do {
    				beanName = beanName.substring(BeanFactory.FACTORY_BEAN_PREFIX.length());
    			}
    			while (beanName.startsWith(BeanFactory.FACTORY_BEAN_PREFIX));
    			return beanName;
    		});
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    	public String canonicalName(String name) {
    		String canonicalName = name;
    		// Handle aliasing...
    		String resolvedName;
    		do {
    			//从别名缓存中获取真正的beanName
    			resolvedName = this.aliasMap.get(canonicalName);
    			if (resolvedName != null) {
    				canonicalName = resolvedName;
    			}
    		}
    		while (resolvedName != null);
    		return canonicalName;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    方法5:isFactoryBean

    	public boolean isFactoryBean(String name) throws NoSuchBeanDefinitionException {
    		//去除&前缀,通过别名获取beanName
    		String beanName = transformedBeanName(name);
    		//尝试从缓存中获取
    		Object beanInstance = getSingleton(beanName, false);
    		if (beanInstance != null) {
    			//判断是否实现了FactoryBean接口
    			return (beanInstance instanceof FactoryBean);
    		}
    		//如果bean定义缓存中没有存在该beanName 并且 其父类工厂是一个ConfigurableBeanFactory
    		if (!containsBeanDefinition(beanName) && getParentBeanFactory() instanceof ConfigurableBeanFactory) {
    			// 从父工厂中寻找
    			return ((ConfigurableBeanFactory) getParentBeanFactory()).isFactoryBean(name);
    		}
    		return isFactoryBean(beanName, getMergedLocalBeanDefinition(beanName));
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    整理下思路:
    1、先将beanName去掉前缀,并尝试从别名缓存获取对应的beanName,通过beanName在三级缓存中查询是否已经实例化的对象,有则进行类型判断。

    2、beanName在当前beanFactory中没有存在定义,并且该工厂存在父工厂且是ConfigurableBeanFactory类型,则在父工厂进行查找,也就是使用父工厂重新执行一遍isFactoryBean流程。

    3、beanName在当前beanFactory工厂中存在定义,则进行解析判断。

    getSingleton(beanName, false),见方法6详解

    isFactoryBean(beanName, getMergedLocalBeanDefinition(beanName)),见方法7详解

    方法6:getSingleton

    	protected Object getSingleton(String beanName, boolean allowEarlyReference) {
    		//从一级缓存中获取,即成品缓存(完成了初始化过程即属性注入等)
    		Object singletonObject = this.singletonObjects.get(beanName);
    		//如果一级缓存获取不到,且在创建的缓存中存在时
    		if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
    			synchronized (this.singletonObjects) {
    				//从二级缓存中获取,即半成品缓存(完成了初始化,未实例化)
    				singletonObject = this.earlySingletonObjects.get(beanName);
    				//二级缓存中不存在,并且允许提前引用
    				if (singletonObject == null && allowEarlyReference) {
    					//从三级缓存中获取ObjectFactory对象
    					ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
    					if (singletonFactory != null) {
    					//调用getObject获取实例,该实例也是半成品
    						singletonObject = singletonFactory.getObject();
    						//放入到二级缓存中
    						this.earlySingletonObjects.put(beanName, singletonObject);
    						//从三级缓存中移除
    						this.singletonFactories.remove(beanName);
    					}
    				}
    			}
    		}
    		return singletonObject;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    这个方法很重要,是解决循环依赖的组成部分。singletonObjects一级缓存、earlySingletonObjects二级缓存、singletonFactories三级缓存。这里简单介绍下循环依赖,现有A、B两个类,A中有B属性,B中也有A属性,当创建A后,对属性B进行注入时,会去创建B,对B进行属性注入时发现B中需要注入A属性,就会再创建A,这样就死循环了。

    我们是否可以使用一级缓存解决循环依赖呢?
    A实例化完成时,放入一级缓存中,当属性注入时去一级缓存中寻找B,找不到则创建B;实例化B后,放入一级缓存中,在对B进行属性注入时,从一级缓存中寻找A,找到后进行注入,这时候A再拿到B进行属性注入。这种方式也可以解决循环依赖,但是如果存在多线程,并且在创建过程中去获取A,而A此时的状态还是半成品,那就有问题了,而且如果A的AOP代理对象,那完全操作不了。

    是否可以使用二级缓存解决循环依赖呢?
    创建A后,放入二级缓存中,并从二级缓存查找B,找不到则创建B,B创建完成后放入二级缓存中,从二级缓存中查找A进行属性注入,这时候的B就是一个完整品,将其放入一级缓存中,再从二级缓存中移除。A从一级缓存中获取B,进行属性注入后,加入一级缓存,并且移除二级缓存中的半成品。有了二级缓存后,就可以解决多线程获取的问题。但是如果A被AOP进行代理,A刚开始创建时肯定是一个原始对象(未被代理的),所以B在进行属性注入时从二级缓存中拿出来的注入的对象就是未被代理的A了,那这样也有问题了。

    三级缓存解决循环依赖:
    创建A后,我们往三级缓存中存入ObjectFactory类型的包装类A,此getObject方法实现如下代码所示(会提前调用AOP代理生成代理对象);属性注入时,会创建B,B会往三级缓存中存入ObjectFactory类型的包装类B,进行属性注入时,从一级缓存中获取A,取不到从二级中缓存,取不到从三级缓存获取,调用getObject方法获取代理后的A对象,并存入二级缓存中,移除三级缓存数据,此时B对象就是一个完整的对象,并且其属性A也是一个代理后的对象,加入一级缓存,移除二级缓存(如果B还有引用其它属性如C,C中也引用了B时,会存在数据)、三级缓存。此时A获取到B对象后进行注入,添加到一级缓存中,并移除二级缓存、三级缓存数据。

    	protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
    		Object exposedObject = bean;
    		if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
    			for (BeanPostProcessor bp : getBeanPostProcessors()) {
    				if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
    					SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
    					exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
    				}
    			}
    		}
    		return exposedObject;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    方法7:isFactoryBean

    	protected boolean isFactoryBean(String beanName, RootBeanDefinition mbd) {
    		//判断是否是FactoryBean
    		Boolean result = mbd.isFactoryBean;
    		if (result == null) {
    			//获取class对象
    			Class<?> beanType = predictBeanType(beanName, mbd, FactoryBean.class);
    			//判断是否是FactoryBean或其子类实现
    			result = (beanType != null && FactoryBean.class.isAssignableFrom(beanType));
    			mbd.isFactoryBean = result;
    		}
    		return result;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    总结

    本篇文章讲解了bean创建前的准备工作,及其FactoryBean的概念,并且讲解了三级缓存的作用。

    一级缓存 singletonObjects:存放完整的Bean对象

    二级缓存 earlySingletonObjects:存放半成品Bean对象

    三级缓存singletonFactories:存放封装后的ObjectFactory,通过getObject返回实例对象,该对象可能是代理后的对象也可能是普通对象。

  • 相关阅读:
    NoSuchMethodError
    三、视频设备的枚举以及插拔检测
    Conda常用命令
    【Java】泛型 之 擦拭法
    创建一个窗口使用鼠标点击进行画矩形,函数:namedWindow,setMouseCallback
    完整的电商平台后端API开发总结
    腾讯云COS+Picgo+Typora图床搭建
    CodeFun 推出 PaaS 服务,携手腾讯 CoDesign 赋能前端
    工业场景全流程!机器学习开发并部署服务到云端
    C语⾔内存函数
  • 原文地址:https://blog.csdn.net/weixin_45031612/article/details/127989693