欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

一文掌握Spring中循環(huán)依賴與三級緩存

 更新時間:2023年09月25日 14:56:12   作者:_子棲_  
這篇文章主要介紹了Spring中循環(huán)依賴與三級緩存,Spring通過三級緩存解決了循環(huán)依賴,其中一級緩存為單例池,二級緩存為早期曝光對象earlySingletonObjects,三級緩存為早期曝光對象工廠(singletonFactories),本文結合實例代碼介紹的非常詳細,需要的朋友參考下吧

先看幾個問題

  • 什么事循環(huán)依賴?
  • 什么情況下循環(huán)依賴可以被處理?
  • spring是如何解決循環(huán)依賴的?

什么是循環(huán)依賴?

簡單理解就是實例 A 依賴實例 B 的同時 B 也依賴了 A

在這里插入圖片描述

@Component
public class A {
	// A 中依賴 B
	@Autowired
	private B b;
}
@Component
public class B {
	// B 中依賴 A
	@Autowired
	private A a;
}

什么情況下循環(huán)依賴可以被處理?

spring 解決循環(huán)依賴是有前提條件的

  • 出現(xiàn)循環(huán)依賴的 bean 必須是單例的
  • 依賴注入的方式不能全是構造器注入的方式

其中第一點是很好理解的,第二點:不能全是構造器注入是什么意思呢?用代碼說話

@Component
public class A {
	// A 中依賴 B
	public A(B b){
	}
}
@Component
public class B {
	// B 中依賴 A
	public B(A a){
	}
}

為了測試循環(huán)依賴的解決情況跟注入方式的關系,我們做如下四種情況的測試

在這里插入圖片描述

Spring是如何解決循環(huán)依賴的?

分兩種情況進行說明:

  • 簡單的循環(huán)依賴(沒有AOP)
  • 含有AOP的循環(huán)依賴

簡單的循環(huán)依賴(沒有AOP)

還是使用上面的例子:

@Component
public class A {
	// A 中依賴 B
	@Autowired
	private B b;
}
@Component
public class B {
	// B 中依賴 A
	@Autowired
	private A a;
}

通過前面我們知道這種循環(huán)依賴是可以解決的,下面進行分析:

首先我們都知道Spring在創(chuàng)建Bean的時候主要有三步:

  • 實例化,對應方法 AbstractAutowireCapableBeanFactory#createBeanInstance ,實例化之后只是在堆中創(chuàng)建了實例,實例中屬性都為默認值,然后放入到 三級緩存 之中
  • 屬性注入,對應方法 AbstractAutowireCapableBeanFactory#populateBean ,為實例化之后的對象進行屬性填充
  • 初始化,對應方法 AbstractAutowireCapableBeanFactory#initializeBean ,執(zhí)行初始化方法,之后在實現(xiàn)了 BeanPostProcessor postProcessAfterInitialization 完成AOP代理

在這里插入圖片描述

創(chuàng)建A對象

getSingleton()

public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
		// beanName 斷言處理
		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 (logger.isDebugEnabled()) {
					logger.debug("Creating shared instance of singleton bean '" + beanName + "'");
				}
				// 此處是在單例bean創(chuàng)建之前, 判斷bean是否需要檢查,
				// 并且將beanName添加到singletonsCurrentlyInCreation(正在創(chuàng)建bean的集合,是一個setFromMap集合)中
				beforeSingletonCreation(beanName);
				boolean newSingleton = false;
				boolean recordSuppressedExceptions = (this.suppressedExceptions == null);
				if (recordSuppressedExceptions) {
					this.suppressedExceptions = new LinkedHashSet<>();
				}
				try {
					// 此處是一個回調,回去執(zhí)行createBean()方法,也就是開始真正的創(chuàng)建bean
					singletonObject = singletonFactory.getObject();
					newSingleton = true;
				}
				catch (IllegalStateException ex) {
					// Has the singleton object implicitly appeared in the meantime ->
					// if yes, proceed with it since the exception indicates that state.
					singletonObject = this.singletonObjects.get(beanName);
					if (singletonObject == null) {
						throw ex;
					}
				}
				catch (BeanCreationException ex) {
					if (recordSuppressedExceptions) {
						for (Exception suppressedException : this.suppressedExceptions) {
							ex.addRelatedCause(suppressedException);
						}
					}
					throw ex;
				}
				finally {
					if (recordSuppressedExceptions) {
						this.suppressedExceptions = null;
					}
					// 至此,beanName創(chuàng)建完畢,從singletonsCurrentlyInCreation(正在創(chuàng)建的集合)中移除
					afterSingletonCreation(beanName);
				}
				if (newSingleton) {
					// 將成品的bean添加到一級緩存,并從二級緩存、三級緩存中移除,并添加到已完成注冊的單例bean集合中
					addSingleton(beanName, singletonObject);
				}
			}
			return singletonObject;
		}
	}

從上面我們可以看到,spring在創(chuàng)建一個bean時,先是調用 getBean() -> doGetBean() -> getSingleton() 主要的處理邏輯就在getSingleton()之中,從getSingleton()源碼中我們可以看到

1、先從一級緩存獲取A
2、獲取不到,再執(zhí)行回調去創(chuàng)建A

下面接著調用回調方法 createBean() 去創(chuàng)建 A

大致流程如下:本質就是使用反射創(chuàng)建A對象實例

在這里插入圖片描述

注意:需要注意在 createBeanInstance() 方法中先調用 instantiateBean() 方法創(chuàng)建bean實例對象,創(chuàng)建完畢以后,會接著調用 addSingletonFactory() 方法,下面我們分析一下這個方法

addSingletonFactory

在這里插入圖片描述

可以看到 earlySingletonExposure 為 true,就會將 創(chuàng)建出來的A對象實例對象放入到三級緩存之中

protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
		// 對 singletonFactory 進行非空校驗
		Assert.notNull(singletonFactory, "Singleton factory must not be null");
		// 對一級緩存進行加鎖
		synchronized (this.singletonObjects) {
			// 如果一級緩存中不存在 beanName 對應的單例對象
			if (!this.singletonObjects.containsKey(beanName)) {
				// 將 singletonFactory 添加到三級緩存中
				this.singletonFactories.put(beanName, singletonFactory);
				// 將 beanName 從二級緩存中移除
				this.earlySingletonObjects.remove(beanName);
				// 將 beanName 添加到 registeredSingletons 中
				this.registeredSingletons.add(beanName);
			}
		}
	}

可以看出這里放入三級緩存中的是一個 ObjectFactory ,這個工廠的 getObject()方法可以得到一個對象,而這個對象是由 getEarlyBeanReference() 創(chuàng)建的,那么問題來了,這個 getEarlyBeanReference() 方法什么時候被調用呢?在創(chuàng)建B對象的時候

接著往下看:

三級緩存放入完畢,然后對A進行屬性填充,大致流程如下,這里不做詳細分析

在這里插入圖片描述

一句話概括就是:對A進行屬性填充的時候發(fā)現(xiàn),A中依賴了B對象,就調用 this.beanFactory.getBean()方法,獲取B對象實例 ,也就是套娃模式開啟

又開始重復上述流程去創(chuàng)建B對象實例,流程如下:

在這里插入圖片描述

此時 B 對象創(chuàng)建完畢,三級緩存中也有了 B 對象的工廠,然后對 B 對象進行屬性填充,流程與A類似,屬性填充過程中發(fā)現(xiàn)B對象中依賴A對象,又調用 this.beanFactory.getBean(A) ,下面分析 getSingleton() 方法注意: 此處 getSingleton()方法與前面介紹的getSingleton()方法不同,前面介紹的getSingleton()方法是在本次getSingleton()方法執(zhí)行完畢未獲取到結果之后,才會執(zhí)行前面講解的getSingleton()方法

@Nullable
	protected Object getSingleton(String beanName, boolean allowEarlyReference) {
		// Quick check for existing instance without full singleton lock
		// 從一級緩存中獲取該beanName實例
		Object singletonObject = this.singletonObjects.get(beanName);
		// 如果以及緩存中不存在并且該beanName對應的單例bean正在創(chuàng)建中
		if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
			// 從二級緩存中獲取該beanName實例
			singletonObject = this.earlySingletonObjects.get(beanName);
			// 二級緩存中不存在并且允許提前引用
			if (singletonObject == null && allowEarlyReference) {
				// 鎖定全局變量進行操作
				synchronized (this.singletonObjects) {
					// Consistent creation of early reference within full singleton lock
					// 從一級緩存中獲取該beanName實例
					singletonObject = this.singletonObjects.get(beanName);
					// 如果一級緩存中獲取不到
					if (singletonObject == null) {
						// 從二級緩存中獲取
						singletonObject = this.earlySingletonObjects.get(beanName);
						// 二級緩存中也獲取不到
						if (singletonObject == null) {
							// 當某些方法需要提前初始化的時候則會調用addSingletonFactory方法將對應的 ObjectFactory 初始化策略存儲在 singletonFactories
							ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
							if (singletonFactory != null) {
								// 如果存在單例對象工廠,則通過工廠創(chuàng)建一個單例對象
								singletonObject = singletonFactory.getObject();
								// 放入到二級緩存中
								this.earlySingletonObjects.put(beanName, singletonObject);
								// 并從三級緩存中移除
								this.singletonFactories.remove(beanName);
							}
						}
					}
				}
			}
		}
		return singletonObject;
	}

從源碼中可以看到:getSingleton()方法會先從一級緩存中獲取A對象實例,如果獲取不到再從二級緩存中獲取,二級緩存中獲取不到再從三級緩存中,那么此時三級緩存中肯定是可以獲取到的,獲取到之后調用 getObject() 方法,此時調用 getObject()方法,會回調 getEarlyBeanReference() 方法

protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
		// 該方法主要用于提前獲取 bean 的引用,以便于解決循環(huán)依賴的問題
		// 將當前 bean 賦值給 exposedObject
		Object exposedObject = bean;
		if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
			// 這塊代碼是用代理對象替換原始對象,這樣就可以在原始對象的基礎上做一些增強操作
			for (BeanPostProcessor bp : getBeanPostProcessors()) {
				// AOP --> AnnotationAwareAspectJAutoProxyCreator
				if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
					SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
					exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
				}
			}
		}
		return exposedObject;
	}

從源碼可以看出實際上就是調用了后置處理器的 getEarlyBeanReference,而真正實現(xiàn)了這個方法的后置處理器只有一個,就是通過@EnableAspectJAutoProxy 注解導入的 AnnotationAwareAspectJAutoProxyCreator。也就是說如果在不考慮AOP的情況下,上面的代碼等價于:

protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
		Object exposedObject = bean;
		return exposedObject;
	}

這樣的話,也就是說這個工廠啥也沒干,直接將實例化階段創(chuàng)建的對象返回了!

所以說在不考慮AOP的情況下三級緩存有用嘛?講道理,真的沒什么用,我直接將這個對象放到二級緩存中不是一點問題都沒有嗎?如果你說它提高了效率,那你告訴我提高的效率在哪?

那么三級緩存到底有什么作用呢?不要急,我們先把整個流程走完,在下文結合AOP分析循環(huán)依賴的時候你就能體會到三級緩存的作用!

到這里不知道小伙伴們會不會有疑問,B中提前注入了一個沒有經過初始化的A類型對象不會有問題嗎?

答:不會

這個時候我們需要將整個創(chuàng)建A這個Bean的流程走完,如下圖:

在這里插入圖片描述

從上圖中我們可以看到,雖然在創(chuàng)建B時會提前給B注入了一個還未初始化的A對象,但是在創(chuàng)建A的流程中一直使用的是注入到B中的A對象的引用,之后會根據這個引用對A進行初始化,所以這是沒有問題的。

創(chuàng)建B對象

從前面我們已經知道,在創(chuàng)建A的過程中已經把B對象創(chuàng)建好了,而且已經放入到了一級緩存,但是spring是通過循環(huán)遍歷beanName去創(chuàng)建bean實例的,所以B還會在創(chuàng)建一次,與創(chuàng)建A對象的區(qū)別在于,在創(chuàng)建B對象的過程中在調用getSingleton()方法的時候,可以從一級緩存中直接拿到B對象,所以直接返回,不在進行創(chuàng)建

至此沒有AOP的循環(huán)依賴就到此為止,下面繼續(xù)看有AOP的循環(huán)依賴

AOP循環(huán)依賴

之前我們已經說過了,在普通的循環(huán)依賴的情況下,三級緩存沒有任何作用。三級緩存實際上跟Spring中的AOP相關,我們再來看一看getEarlyBeanReference()方法的代碼:

protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
		// 該方法主要用于提前獲取 bean 的引用,以便于解決循環(huán)依賴的問題
		// 將當前 bean 賦值給 exposedObject
		Object exposedObject = bean;
		if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
			// 這塊代碼是用代理對象替換原始對象,這樣就可以在原始對象的基礎上做一些增強操作
			for (BeanPostProcessor bp : getBeanPostProcessors()) {
				// AOP --> AnnotationAwareAspectJAutoProxyCreator
				if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
					SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
					exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
				}
			}
		}
		return exposedObject;
	}

如果在開啟AOP的情況下,就會調用 AnnotationAwareAspectJAutoProxyCreator的getEarlyBeanReference()方法

public Object getEarlyBeanReference(Object bean, String beanName) {
		// 根據bean的類型和名稱獲取緩存的key,如果beanName為空,則使用bean的類型作為key
		// 如果beanName不為空,則使用beanName作為key,如果beanName是一個FactoryBean的名稱,則使用&+beanName作為key
		Object cacheKey = getCacheKey(bean.getClass(), beanName);
		// 添加到earlyProxyReferences中
		this.earlyProxyReferences.put(cacheKey, bean);
		// 創(chuàng)建aop代理
		return wrapIfNecessary(bean, beanName, cacheKey);
	}

從代碼可以看出如果我們對A進行了AOP代理的話,那么此時的getEarlyBeanReference()方法將返回一個A的代理對象,而不是實例化階段創(chuàng)建的A對象,這樣就意味著B中注入的A將是一個代理對象而不是A的實例化階段創(chuàng)建后的對象。

在這里插入圖片描述

看到這個圖你可能會產生下面這些疑問

在給B注入的時候為什么要注入一個代理對象?

答:當我們對A進行了AOP代理時,說明我們希望從容器中獲取到的就是A代理后的對象而不是A本身,因此把A當作依賴進行注入時也要注入它的代理對象

明明初始化的時候是A對象,那么Spring是在哪里將代理對象放入到容器中的呢?

在完成初始化的時候,spring會在調用一次getSingleton()方法,這一次傳入的參數(shù)又不一樣了,false可以理解為禁用三級緩存,前面說過,B進行屬性填充的時候,已經從三級緩存中獲取到A對象,然后生成A的代理對象,并將代理對象放入到二級緩存中,所以在A完成初始化的時候,所以再從二級緩存中獲取到A代理對象賦值給 exposedObject,最終放入到一級緩存中

在這里插入圖片描述

初始化的時候是對A對象本身進行初始化,而容器中以及注入到B中的都是代理對象,這樣不會有問題嗎?

答:不會,這是因為不管是cglib代理還是jdk動態(tài)代理生成的代理類,內部都持有一個目標類的引用,當調用代理對象的方法時,實際會去調用目標對象的方法,A完成初始化相當于代理對象自身也完成了初始化

三級緩存為什么要使用工廠而不是直接使用引用?換而言之,為什么需要這個三級緩存,直接通過二級緩存暴露一個引用不行嗎?

答:這個工廠的目的在于 延遲創(chuàng)建對實例化階段生成的對象的代理,只有真正發(fā)生循環(huán)依賴的時候,才去提前生成代理對象,否則只會創(chuàng)建一個工廠并將其放入到三級緩存中,但是不會去通過這個工廠去真正創(chuàng)建對象

我們思考一種簡單的情況,就以單獨創(chuàng)建A為例,假設AB之間現(xiàn)在沒有依賴關系,但是A被代理了,這個時候當A完成實例化后還是會進入下面這段代碼:

        // Eagerly cache singletons to be able to resolve circular references
		// even when triggered by lifecycle interfaces like BeanFactoryAware.
		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");
			}
			// 將創(chuàng)建的bean 的 lambda 表達式放入到三級緩存中
			addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
		}

假設我們在這里直接使用二級緩存的話,那么意味著所有的Bean在這一步都要完成AOP代理。這樣做有必要嗎?

不僅沒有必要,而且違背了Spring在結合AOP跟Bean的生命周期的設計!Spring結合AOP跟Bean的生命周期本身就是通過AnnotationAwareAspectJAutoProxyCreator這個后置處理器來完成的,在這個后置處理的postProcessAfterInitialization方法中對初始化后的Bean完成AOP代理。如果出現(xiàn)了循環(huán)依賴,那沒有辦法,只有給Bean先創(chuàng)建代理,但是沒有出現(xiàn)循環(huán)依賴的情況下,設計之初就是讓Bean在生命周期的最后一步完成代理而不是在實例化后就立馬完成代理。

三級緩存真的提高了效率了嗎?

通過以上分析,我們已經知道了三級緩存的真正作用,但是這個答案可能還無法說服你,所以我們再最后總結分析一波,三級緩存真的提高了效率了嗎?分為兩點討論:

沒有進行AOP的Bean間的循環(huán)依賴

從上文分析可以看出,這種情況下三級緩存根本沒用!所以不會存在什么提高了效率的說法

進行了AOP的Bean間的循環(huán)依賴

就以我們上的A、B為例,其中A被AOP代理,我們先分析下使用了三級緩存的情況下,A、B的創(chuàng)建流程

在這里插入圖片描述

假設不使用三級緩存直接使用二級緩存

在這里插入圖片描述

上面兩個流程的唯一區(qū)別在于為A對象創(chuàng)建代理的時機不同,在使用了三級緩存的情況下為A創(chuàng)建代理的時機是在B中需要注入A的時候,而不使用三級緩存的話在A實例化后就需要馬上為A創(chuàng)建代理然后放入到二級緩存中去。對于整個A、B的創(chuàng)建過程而言,消耗的時間是一樣的

綜上,不管是哪種情況,三級緩存提高了效率這種說法都是錯誤的!

總結

“Spring是如何解決的循環(huán)依賴?”

答:Spring通過三級緩存解決了循環(huán)依賴,其中一級緩存為單例池(singletonObjects),二級緩存為早期曝光對象earlySingletonObjects,三級緩存為早期曝光對象工廠(singletonFactories)。當A、B兩個類發(fā)生循環(huán)引用時,在A完成實例化后,就使用實例化后的對象去創(chuàng)建一個對象工廠,并添加到三級緩存中,如果A被AOP代理,那么通過這個工廠獲取到的就是A代理后的對象,如果A沒有被AOP代理,那么這個工廠獲取到的就是A實例化的對象。當A進行屬性注入時,會去創(chuàng)建B,同時B又依賴了A,所以創(chuàng)建B的同時又會去調用getBean(a)來獲取需要的依賴,此時的getBean(a)會從緩存中獲取,第一步,先獲取到三級緩存中的工廠;第二步,調用對象工工廠的getObject方法來獲取到對應的對象,得到這個對象后將其注入到B中。緊接著B會走完它的生命周期流程,包括初始化、后置處理器等。當B創(chuàng)建完后,會將B再注入到A中,此時A再完成它的整個生命周期。至此,循環(huán)依賴結束!

“為什么要使用三級緩存呢?二級緩存能解決循環(huán)依賴嗎?”

答:如果要使用二級緩存解決循環(huán)依賴,意味著所有Bean在實例化后就要完成AOP代理,這樣違背了Spring設計的原則,Spring在設計之初就是通過AnnotationAwareAspectJAutoProxyCreator這個后置處理器來在Bean生命周期的最后一步來完成AOP代理,而不是在實例化后就立馬進行AOP代理。

到此這篇關于Spring中循環(huán)依賴與三級緩存的文章就介紹到這了,更多相關Spring循環(huán)依賴與三級緩存內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

相關文章

  • java打包maven啟動報錯jar中沒有主清單屬性

    java打包maven啟動報錯jar中沒有主清單屬性

    本文主要介紹了java打包maven啟動報錯jar中沒有主清單屬性,可能原因是創(chuàng)建springboot項目時,自動導入,下面就來介紹一下解決方法,感興趣的可以了解一下
    2024-03-03
  • java split()使用方法解析

    java split()使用方法解析

    這篇文章主要介紹了java split()使用方法解析,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下
    2020-02-02
  • JDK1.8新特性之方法引用 ::和Optional詳解

    JDK1.8新特性之方法引用 ::和Optional詳解

    這篇文章主要介紹了JDK1.8新特性之方法引用 ::和Optional,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2021-09-09
  • 微信、支付寶二碼合一掃碼支付實現(xiàn)思路(java)

    微信、支付寶二碼合一掃碼支付實現(xiàn)思路(java)

    這篇文章主要為大家詳細介紹了微信、支付寶二碼合一掃碼支付實現(xiàn)思路,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2019-08-08
  • Java設計模式之解釋器模式

    Java設計模式之解釋器模式

    這篇文章介紹了Java設計模式之解釋器模式,文中通過示例代碼介紹的非常詳細。對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2022-10-10
  • mybatis foreach遍歷LIST讀到數(shù)據為null的問題

    mybatis foreach遍歷LIST讀到數(shù)據為null的問題

    這篇文章主要介紹了mybatis foreach遍歷LIST讀到數(shù)據為null的問題,具有很好的參考價值,希望對大家有所幫助。
    2022-02-02
  • Java的MyBatis框架項目搭建與hellow world示例

    Java的MyBatis框架項目搭建與hellow world示例

    MyBatis框架為Java程序的數(shù)據庫操作帶來了很大的便利,這里我們就從最基礎的入手,來看一下Java的MyBatis框架項目搭建與hellow world示例,需要的朋友可以參考下
    2016-06-06
  • 基于UncategorizedSQLException異常處理方案

    基于UncategorizedSQLException異常處理方案

    這篇文章主要介紹了基于UncategorizedSQLException異常處理方案,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2023-12-12
  • 基于Java事件監(jiān)聽編寫一個中秋猜燈謎小游戲

    基于Java事件監(jiān)聽編寫一個中秋猜燈謎小游戲

    眾所周知,JavaSwing是Java中關于窗口開發(fā)的一個工具包,可以開發(fā)一些窗口程序,然后由于工具包的一些限制,導致Java在窗口開發(fā)商并沒有太多優(yōu)勢,不過,在JavaSwing中關于事件的監(jiān)聽機制是我們需要重點掌握的內容,本文將基于Java事件監(jiān)聽編寫一個中秋猜燈謎小游戲
    2023-09-09
  • java 導入Excel思路及代碼示例

    java 導入Excel思路及代碼示例

    這篇文章主要介紹了java 導入Excel思路及代碼示例,簡要分析了設計思路,然后分享了實例代碼,具有一定參考價值,需要的朋友可以了解下。
    2017-11-11

最新評論