一文掌握Spring中循環(huán)依賴與三級緩存
先看幾個(gè)問題
- 什么事循環(huán)依賴?
- 什么情況下循環(huán)依賴可以被處理?
- spring是如何解決循環(huán)依賴的?
什么是循環(huán)依賴?
簡單理解就是實(shí)例 A 依賴實(shí)例 B 的同時(shí) 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 必須是單例的
- 依賴注入的方式不能全是構(gòu)造器注入的方式
其中第一點(diǎn)是很好理解的,第二點(diǎn):不能全是構(gòu)造器注入是什么意思呢?用代碼說話
@Component public class A { // A 中依賴 B public A(B b){ } } @Component public class B { // B 中依賴 A public B(A a){ } }
為了測試循環(huán)依賴的解決情況跟注入方式的關(guān)系,我們做如下四種情況的測試
Spring是如何解決循環(huán)依賴的?
分兩種情況進(jì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)依賴是可以解決的,下面進(jìn)行分析:
首先我們都知道Spring在創(chuàng)建Bean的時(shí)候主要有三步:
- 實(shí)例化,對應(yīng)方法
AbstractAutowireCapableBeanFactory#createBeanInstance
,實(shí)例化之后只是在堆中創(chuàng)建了實(shí)例,實(shí)例中屬性都為默認(rèn)值,然后放入到三級緩存
之中 - 屬性注入,對應(yīng)方法
AbstractAutowireCapableBeanFactory#populateBean
,為實(shí)例化之后的對象進(jìn)行屬性填充 - 初始化,對應(yīng)方法
AbstractAutowireCapableBeanFactory#initializeBean
,執(zhí)行初始化方法,之后在實(shí)現(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的集合,是一個(gè)setFromMap集合)中 beforeSingletonCreation(beanName); boolean newSingleton = false; boolean recordSuppressedExceptions = (this.suppressedExceptions == null); if (recordSuppressedExceptions) { this.suppressedExceptions = new LinkedHashSet<>(); } try { // 此處是一個(gè)回調(diào),回去執(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)建一個(gè)bean時(shí),先是調(diào)用 getBean() -> doGetBean() -> getSingleton() 主要的處理邏輯就在getSingleton()之中,從getSingleton()源碼中我們可以看到
1、先從一級緩存獲取A
2、獲取不到,再執(zhí)行回調(diào)去創(chuàng)建A
下面接著調(diào)用回調(diào)方法 createBean() 去創(chuàng)建 A
大致流程如下:本質(zhì)就是使用反射創(chuàng)建A對象實(shí)例
注意:需要注意在 createBeanInstance()
方法中先調(diào)用 instantiateBean()
方法創(chuàng)建bean實(shí)例對象,創(chuàng)建完畢以后,會(huì)接著調(diào)用 addSingletonFactory()
方法,下面我們分析一下這個(gè)方法
addSingletonFactory
可以看到 earlySingletonExposure 為 true,就會(huì)將 創(chuàng)建出來的A對象實(shí)例對象放入到三級緩存之中
protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) { // 對 singletonFactory 進(jìn)行非空校驗(yàn) Assert.notNull(singletonFactory, "Singleton factory must not be null"); // 對一級緩存進(jìn)行加鎖 synchronized (this.singletonObjects) { // 如果一級緩存中不存在 beanName 對應(yīng)的單例對象 if (!this.singletonObjects.containsKey(beanName)) { // 將 singletonFactory 添加到三級緩存中 this.singletonFactories.put(beanName, singletonFactory); // 將 beanName 從二級緩存中移除 this.earlySingletonObjects.remove(beanName); // 將 beanName 添加到 registeredSingletons 中 this.registeredSingletons.add(beanName); } } }
可以看出這里放入三級緩存中的是一個(gè) ObjectFactory ,這個(gè)工廠的 getObject()方法可以得到一個(gè)對象,而這個(gè)對象是由 getEarlyBeanReference() 創(chuàng)建的,那么問題來了,這個(gè) getEarlyBeanReference() 方法什么時(shí)候被調(diào)用呢?在創(chuàng)建B對象的時(shí)候
接著往下看:
三級緩存放入完畢,然后對A進(jìn)行屬性填充,大致流程如下,這里不做詳細(xì)分析
一句話概括就是:對A進(jìn)行屬性填充的時(shí)候發(fā)現(xiàn),A中依賴了B對象,就調(diào)用 this.beanFactory.getBean()方法,獲取B對象實(shí)例
,也就是套娃模式開啟
又開始重復(fù)上述流程去創(chuàng)建B對象實(shí)例,流程如下:
此時(shí) B 對象創(chuàng)建完畢,三級緩存中也有了 B 對象的工廠,然后對 B 對象進(jìn)行屬性填充,流程與A類似,屬性填充過程中發(fā)現(xiàn)B對象中依賴A對象,又調(diào)用 this.beanFactory.getBean(A)
,下面分析 getSingleton()
方法注意: 此處 getSingleton()方法與前面介紹的getSingleton()方法不同,前面介紹的getSingleton()方法是在本次getSingleton()方法執(zhí)行完畢未獲取到結(jié)果之后,才會(huì)執(zhí)行前面講解的getSingleton()方法
@Nullable protected Object getSingleton(String beanName, boolean allowEarlyReference) { // Quick check for existing instance without full singleton lock // 從一級緩存中獲取該beanName實(shí)例 Object singletonObject = this.singletonObjects.get(beanName); // 如果以及緩存中不存在并且該beanName對應(yīng)的單例bean正在創(chuàng)建中 if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) { // 從二級緩存中獲取該beanName實(shí)例 singletonObject = this.earlySingletonObjects.get(beanName); // 二級緩存中不存在并且允許提前引用 if (singletonObject == null && allowEarlyReference) { // 鎖定全局變量進(jìn)行操作 synchronized (this.singletonObjects) { // Consistent creation of early reference within full singleton lock // 從一級緩存中獲取該beanName實(shí)例 singletonObject = this.singletonObjects.get(beanName); // 如果一級緩存中獲取不到 if (singletonObject == null) { // 從二級緩存中獲取 singletonObject = this.earlySingletonObjects.get(beanName); // 二級緩存中也獲取不到 if (singletonObject == null) { // 當(dāng)某些方法需要提前初始化的時(shí)候則會(huì)調(diào)用addSingletonFactory方法將對應(yīng)的 ObjectFactory 初始化策略存儲(chǔ)在 singletonFactories ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName); if (singletonFactory != null) { // 如果存在單例對象工廠,則通過工廠創(chuàng)建一個(gè)單例對象 singletonObject = singletonFactory.getObject(); // 放入到二級緩存中 this.earlySingletonObjects.put(beanName, singletonObject); // 并從三級緩存中移除 this.singletonFactories.remove(beanName); } } } } } } return singletonObject; }
從源碼中可以看到:getSingleton()方法會(huì)先從一級緩存中獲取A對象實(shí)例,如果獲取不到再從二級緩存中獲取,二級緩存中獲取不到再從三級緩存中,那么此時(shí)三級緩存中肯定是可以獲取到的,獲取到之后調(diào)用 getObject()
方法,此時(shí)調(diào)用 getObject()方法,會(huì)回調(diào) getEarlyBeanReference()
方法
protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) { // 該方法主要用于提前獲取 bean 的引用,以便于解決循環(huán)依賴的問題 // 將當(dāng)前 bean 賦值給 exposedObject Object exposedObject = bean; if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) { // 這塊代碼是用代理對象替換原始對象,這樣就可以在原始對象的基礎(chǔ)上做一些增強(qiáng)操作 for (BeanPostProcessor bp : getBeanPostProcessors()) { // AOP --> AnnotationAwareAspectJAutoProxyCreator if (bp instanceof SmartInstantiationAwareBeanPostProcessor) { SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp; exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName); } } } return exposedObject; }
從源碼可以看出實(shí)際上就是調(diào)用了后置處理器的 getEarlyBeanReference,而真正實(shí)現(xiàn)了這個(gè)方法的后置處理器只有一個(gè),就是通過@EnableAspectJAutoProxy 注解導(dǎo)入的 AnnotationAwareAspectJAutoProxyCreator。也就是說如果在不考慮AOP的情況下,上面的代碼等價(jià)于:
protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) { Object exposedObject = bean; return exposedObject; }
這樣的話,也就是說這個(gè)工廠啥也沒干,直接將實(shí)例化階段創(chuàng)建的對象返回了!
所以說在不考慮AOP的情況下三級緩存有用嘛?講道理,真的沒什么用,我直接將這個(gè)對象放到二級緩存中不是一點(diǎn)問題都沒有嗎?如果你說它提高了效率,那你告訴我提高的效率在哪?
那么三級緩存到底有什么作用呢?不要急,我們先把整個(gè)流程走完,在下文結(jié)合AOP分析循環(huán)依賴的時(shí)候你就能體會(huì)到三級緩存的作用!
到這里不知道小伙伴們會(huì)不會(huì)有疑問,B中提前注入了一個(gè)沒有經(jīng)過初始化的A類型對象不會(huì)有問題嗎?
答:不會(huì)
這個(gè)時(shí)候我們需要將整個(gè)創(chuàng)建A這個(gè)Bean的流程走完,如下圖:
從上圖中我們可以看到,雖然在創(chuàng)建B時(shí)會(huì)提前給B注入了一個(gè)還未初始化的A對象,但是在創(chuàng)建A的流程中一直使用的是注入到B中的A對象的引用,之后會(huì)根據(jù)這個(gè)引用對A進(jìn)行初始化,所以這是沒有問題的。
創(chuàng)建B對象
從前面我們已經(jīng)知道,在創(chuàng)建A的過程中已經(jīng)把B對象創(chuàng)建好了,而且已經(jīng)放入到了一級緩存,但是spring是通過循環(huán)遍歷beanName去創(chuàng)建bean實(shí)例的,所以B還會(huì)在創(chuàng)建一次,與創(chuàng)建A對象的區(qū)別在于,在創(chuàng)建B對象的過程中在調(diào)用getSingleton()方法的時(shí)候,可以從一級緩存中直接拿到B對象,所以直接返回,不在進(jìn)行創(chuàng)建
至此沒有AOP的循環(huán)依賴就到此為止,下面繼續(xù)看有AOP的循環(huán)依賴
AOP循環(huán)依賴
之前我們已經(jīng)說過了,在普通的循環(huán)依賴的情況下,三級緩存沒有任何作用。三級緩存實(shí)際上跟Spring中的AOP相關(guān),我們再來看一看getEarlyBeanReference()方法的代碼:
protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) { // 該方法主要用于提前獲取 bean 的引用,以便于解決循環(huán)依賴的問題 // 將當(dāng)前 bean 賦值給 exposedObject Object exposedObject = bean; if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) { // 這塊代碼是用代理對象替換原始對象,這樣就可以在原始對象的基礎(chǔ)上做一些增強(qiáng)操作 for (BeanPostProcessor bp : getBeanPostProcessors()) { // AOP --> AnnotationAwareAspectJAutoProxyCreator if (bp instanceof SmartInstantiationAwareBeanPostProcessor) { SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp; exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName); } } } return exposedObject; }
如果在開啟AOP的情況下,就會(huì)調(diào)用 AnnotationAwareAspectJAutoProxyCreator的getEarlyBeanReference()方法
public Object getEarlyBeanReference(Object bean, String beanName) { // 根據(jù)bean的類型和名稱獲取緩存的key,如果beanName為空,則使用bean的類型作為key // 如果beanName不為空,則使用beanName作為key,如果beanName是一個(gè)FactoryBean的名稱,則使用&+beanName作為key Object cacheKey = getCacheKey(bean.getClass(), beanName); // 添加到earlyProxyReferences中 this.earlyProxyReferences.put(cacheKey, bean); // 創(chuàng)建aop代理 return wrapIfNecessary(bean, beanName, cacheKey); }
從代碼可以看出如果我們對A進(jìn)行了AOP代理的話,那么此時(shí)的getEarlyBeanReference()方法將返回一個(gè)A的代理對象,而不是實(shí)例化階段創(chuàng)建的A對象,這樣就意味著B中注入的A將是一個(gè)代理對象而不是A的實(shí)例化階段創(chuàng)建后的對象。
看到這個(gè)圖你可能會(huì)產(chǎn)生下面這些疑問
在給B注入的時(shí)候?yàn)槭裁匆⑷胍粋€(gè)代理對象?
答:當(dāng)我們對A進(jìn)行了AOP代理時(shí),說明我們希望從容器中獲取到的就是A代理后的對象而不是A本身,因此把A當(dāng)作依賴進(jìn)行注入時(shí)也要注入它的代理對象
明明初始化的時(shí)候是A對象,那么Spring是在哪里將代理對象放入到容器中的呢?
在完成初始化的時(shí)候,spring會(huì)在調(diào)用一次getSingleton()方法,這一次傳入的參數(shù)又不一樣了,false可以理解為禁用三級緩存,前面說過,B進(jìn)行屬性填充的時(shí)候,已經(jīng)從三級緩存中獲取到A對象,然后生成A的代理對象,并將代理對象放入到二級緩存中,所以在A完成初始化的時(shí)候,所以再從二級緩存中獲取到A代理對象賦值給 exposedObject,最終放入到一級緩存中
初始化的時(shí)候是對A對象本身進(jìn)行初始化,而容器中以及注入到B中的都是代理對象,這樣不會(huì)有問題嗎?
答:不會(huì),這是因?yàn)椴还苁莄glib代理還是jdk動(dòng)態(tài)代理生成的代理類,內(nèi)部都持有一個(gè)目標(biāo)類的引用,當(dāng)調(diào)用代理對象的方法時(shí),實(shí)際會(huì)去調(diào)用目標(biāo)對象的方法,A完成初始化相當(dāng)于代理對象自身也完成了初始化
三級緩存為什么要使用工廠而不是直接使用引用?換而言之,為什么需要這個(gè)三級緩存,直接通過二級緩存暴露一個(gè)引用不行嗎?
答:這個(gè)工廠的目的在于 延遲創(chuàng)建對實(shí)例化階段生成的對象的代理,只有真正發(fā)生循環(huán)依賴的時(shí)候,才去提前生成代理對象,否則只會(huì)創(chuàng)建一個(gè)工廠并將其放入到三級緩存中,但是不會(huì)去通過這個(gè)工廠去真正創(chuàng)建對象
我們思考一種簡單的情況,就以單獨(dú)創(chuàng)建A為例,假設(shè)AB之間現(xiàn)在沒有依賴關(guān)系,但是A被代理了,這個(gè)時(shí)候當(dāng)A完成實(shí)例化后還是會(huì)進(jìn)入下面這段代碼:
// 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 表達(dá)式放入到三級緩存中 addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean)); }
假設(shè)我們在這里直接使用二級緩存的話,那么意味著所有的Bean在這一步都要完成AOP代理。這樣做有必要嗎?
不僅沒有必要,而且違背了Spring在結(jié)合AOP跟Bean的生命周期的設(shè)計(jì)!Spring結(jié)合AOP跟Bean的生命周期本身就是通過AnnotationAwareAspectJAutoProxyCreator這個(gè)后置處理器來完成的,在這個(gè)后置處理的postProcessAfterInitialization方法中對初始化后的Bean完成AOP代理。如果出現(xiàn)了循環(huán)依賴,那沒有辦法,只有給Bean先創(chuàng)建代理,但是沒有出現(xiàn)循環(huán)依賴的情況下,設(shè)計(jì)之初就是讓Bean在生命周期的最后一步完成代理而不是在實(shí)例化后就立馬完成代理。
三級緩存真的提高了效率了嗎?
通過以上分析,我們已經(jīng)知道了三級緩存的真正作用,但是這個(gè)答案可能還無法說服你,所以我們再最后總結(jié)分析一波,三級緩存真的提高了效率了嗎?分為兩點(diǎn)討論:
沒有進(jìn)行AOP的Bean間的循環(huán)依賴
從上文分析可以看出,這種情況下三級緩存根本沒用!所以不會(huì)存在什么提高了效率的說法
進(jìn)行了AOP的Bean間的循環(huán)依賴
就以我們上的A、B為例,其中A被AOP代理,我們先分析下使用了三級緩存的情況下,A、B的創(chuàng)建流程
假設(shè)不使用三級緩存直接使用二級緩存
上面兩個(gè)流程的唯一區(qū)別在于為A對象創(chuàng)建代理的時(shí)機(jī)不同,在使用了三級緩存的情況下為A創(chuàng)建代理的時(shí)機(jī)是在B中需要注入A的時(shí)候,而不使用三級緩存的話在A實(shí)例化后就需要馬上為A創(chuàng)建代理然后放入到二級緩存中去。對于整個(gè)A、B的創(chuàng)建過程而言,消耗的時(shí)間是一樣的
綜上,不管是哪種情況,三級緩存提高了效率這種說法都是錯(cuò)誤的!
總結(jié)
“Spring是如何解決的循環(huán)依賴?”
答:Spring通過三級緩存解決了循環(huán)依賴,其中一級緩存為單例池(singletonObjects),二級緩存為早期曝光對象earlySingletonObjects,三級緩存為早期曝光對象工廠(singletonFactories)。當(dāng)A、B兩個(gè)類發(fā)生循環(huán)引用時(shí),在A完成實(shí)例化后,就使用實(shí)例化后的對象去創(chuàng)建一個(gè)對象工廠,并添加到三級緩存中,如果A被AOP代理,那么通過這個(gè)工廠獲取到的就是A代理后的對象,如果A沒有被AOP代理,那么這個(gè)工廠獲取到的就是A實(shí)例化的對象。當(dāng)A進(jìn)行屬性注入時(shí),會(huì)去創(chuàng)建B,同時(shí)B又依賴了A,所以創(chuàng)建B的同時(shí)又會(huì)去調(diào)用getBean(a)來獲取需要的依賴,此時(shí)的getBean(a)會(huì)從緩存中獲取,第一步,先獲取到三級緩存中的工廠;第二步,調(diào)用對象工工廠的getObject方法來獲取到對應(yīng)的對象,得到這個(gè)對象后將其注入到B中。緊接著B會(huì)走完它的生命周期流程,包括初始化、后置處理器等。當(dāng)B創(chuàng)建完后,會(huì)將B再注入到A中,此時(shí)A再完成它的整個(gè)生命周期。至此,循環(huán)依賴結(jié)束!
“為什么要使用三級緩存呢?二級緩存能解決循環(huán)依賴嗎?”
答:如果要使用二級緩存解決循環(huán)依賴,意味著所有Bean在實(shí)例化后就要完成AOP代理,這樣違背了Spring設(shè)計(jì)的原則,Spring在設(shè)計(jì)之初就是通過AnnotationAwareAspectJAutoProxyCreator這個(gè)后置處理器來在Bean生命周期的最后一步來完成AOP代理,而不是在實(shí)例化后就立馬進(jìn)行AOP代理。
到此這篇關(guān)于Spring中循環(huán)依賴與三級緩存的文章就介紹到這了,更多相關(guān)Spring循環(huán)依賴與三級緩存內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java打包maven啟動(dòng)報(bào)錯(cuò)jar中沒有主清單屬性
本文主要介紹了java打包maven啟動(dòng)報(bào)錯(cuò)jar中沒有主清單屬性,可能原因是創(chuàng)建springboot項(xiàng)目時(shí),自動(dòng)導(dǎo)入,下面就來介紹一下解決方法,感興趣的可以了解一下2024-03-03微信、支付寶二碼合一掃碼支付實(shí)現(xiàn)思路(java)
這篇文章主要為大家詳細(xì)介紹了微信、支付寶二碼合一掃碼支付實(shí)現(xiàn)思路,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-08-08mybatis foreach遍歷LIST讀到數(shù)據(jù)為null的問題
這篇文章主要介紹了mybatis foreach遍歷LIST讀到數(shù)據(jù)為null的問題,具有很好的參考價(jià)值,希望對大家有所幫助。2022-02-02Java的MyBatis框架項(xiàng)目搭建與hellow world示例
MyBatis框架為Java程序的數(shù)據(jù)庫操作帶來了很大的便利,這里我們就從最基礎(chǔ)的入手,來看一下Java的MyBatis框架項(xiàng)目搭建與hellow world示例,需要的朋友可以參考下2016-06-06基于UncategorizedSQLException異常處理方案
這篇文章主要介紹了基于UncategorizedSQLException異常處理方案,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-12-12基于Java事件監(jiān)聽編寫一個(gè)中秋猜燈謎小游戲
眾所周知,JavaSwing是Java中關(guān)于窗口開發(fā)的一個(gè)工具包,可以開發(fā)一些窗口程序,然后由于工具包的一些限制,導(dǎo)致Java在窗口開發(fā)商并沒有太多優(yōu)勢,不過,在JavaSwing中關(guān)于事件的監(jiān)聽機(jī)制是我們需要重點(diǎn)掌握的內(nèi)容,本文將基于Java事件監(jiān)聽編寫一個(gè)中秋猜燈謎小游戲2023-09-09