版權說明:本文檔由用戶提供并上傳,收益歸屬內容提供方,若內容存在侵權,請進行舉報或認領
文檔簡介
摩根面試準備要點JVM架構(Vincent)?
重要涉及兩個子系統(tǒng)和兩個組件:Classloader(類裝載器)子系統(tǒng),Executionengine(執(zhí)行引擎)子系統(tǒng);Runtimedat(yī)aarea(運營時數(shù)據(jù)區(qū)域)組件,Nativeinterface(本地接口)組件。?
Classloader子系統(tǒng)的作用:根據(jù)給定的全限定名類名(如java.lang.Object)來裝載class文獻的內容到Runtimedataarea中的methodarea(方法區(qū)域)。Javsa程序員可以extendsjava.lang.ClassLoader類來寫自己的Classloader。?
Executionengine子系統(tǒng)的作用:執(zhí)行classes中的指令。任何JVMspecification實現(xiàn)(JDK)的核心是Executionengine,換句話說:Sun的JDK和IBM的JDK好壞重要取決于他們各自實現(xiàn)的Execution
engine的好壞。每個運營中的線程都有一個Executionengine的實例。
Nativeinterface組件:與nat(yī)ivelibraries交互,是其它編程語言交互的接口。
?
Runtimedat(yī)aarea組件:這個組件就是JVM中的內存。下面對這個部分進行具體介紹。Runtimedataarea的整體架構圖Runtimedataarea重要涉及五個部分:Heap(堆),MethodArea(方法區(qū)域),JavaStack(java的棧),ProgramCounter(程序計數(shù)器),Nat(yī)ivemethodstack(本地方法棧)。Heap和MethodArea是被所有線程的共享使用的;而Javastack,Programcounter和Nativemethodstack是以線程為粒度的,每個線程獨自擁有。?
Heap
Java程序在運營時創(chuàng)建的所有類實或數(shù)組都放在同一個堆中。而一個Java虛擬實例中只存在一個堆空間,因此所有線程都將共享這個堆。每一個java程序獨占一個JVM實例,因而每個java程序都有它自己的堆空間,它們不會彼此干擾。但是同一java程序的多個線程都共享著同一個堆空間,就得考慮多線程訪問對象(堆數(shù)據(jù))的同步問題。(這里也許出現(xiàn)的異常java.lang.OutOfMemoryError:Javaheapspace)??Methodarea?在Java虛擬機中,被裝載的class的信息存儲在Methodarea的內存中。當虛擬機裝載某個類型時,它使用類裝載器定位相應的class文獻,然后讀入這個class文獻內容并把它傳輸?shù)教摂M機中。緊接著虛擬機提取其中的類型信息,并將這些信息存儲到方法區(qū)。該類型中的類(靜態(tài))變量同樣也存儲在方法區(qū)中。與Heap同樣,methodarea是多線程共享的,因此要考慮多線程訪問的同步問題。比如,假設同時兩個線程都企圖訪問一個名為Lava的類,而這個類還沒有內裝載入虛擬機,那么,這時應當只有一個線程去裝載它,而另一個線程則只能等待。(這里也許出現(xiàn)的異常java.lang.OutOfMemoryError:PermGenfull)
Javastack?
Javastack以幀為單位保存線程的運營狀態(tài)。虛擬機只會直接對Javastack執(zhí)行兩種操作:以幀為單位的壓?;虺鰲?。每當線程調用一個方法的時候,就對當前狀態(tài)作為一個幀保存到javastack中(壓棧);當一個方法調用返回時,從javastack彈出一個幀(出棧)。棧的大小是有一定的限制,這個也許出現(xiàn)StackOverFlow問題。下面的程序可以說明這個問題。publicclassTestStackOverFlow{?publicstaticvoidmain(String[]args){? Recursiver=newRecursive();??r.doit(10000);? //Exceptioninthread"main"java.lang.StackOverflowError }}classRecursive{ publicintdoit(intt){? if(t<=1){ ??return1; }??returnt+doit(t-1); }}
?Programcounter?每個運營中的Java程序,每一個線程都有它自己的PC寄存器,也是該線程啟動時創(chuàng)建的。PC寄存器的內容總是指向下一條將被執(zhí)行指令的地址;,這里的地址可以是一個本地指針,也可以是在方法區(qū)中相相應于該方法起始指令的偏移量。
Nativemethodstack?對于一個運營中的Java程序而言,它還能會用到一些跟本地方法相關的數(shù)據(jù)區(qū)。當某個線程調用一個本地方法時,它就進入了一個全新的并且不再受虛擬機限制的世界。本地方法可以通過本地方法接口來訪問虛擬機的運營時數(shù)據(jù)區(qū),不止與此,它還可以做任何它想做的事情。比如,可以調用寄存器,或在操作系統(tǒng)中分派內存等??傊?,本地方法具有和JVM相同的能力和權限。(這里出現(xiàn)JVM無法控制的內存溢出問題nativeheapOutOfMemory)
CLassLoader(Vincent)Java的可執(zhí)行文獻不同于C/C++,Java編譯器只產生中間字節(jié)碼文獻(.class文獻),由Java虛擬機(java.exe)解釋執(zhí)行。Java發(fā)布的程序(JAR包)也多半是一堆class文獻,運營時由ClassLoader加載到Java虛擬機中執(zhí)行。ClassLoader是Java虛擬機的重要組成部分,由Java語言編寫,用戶可以實現(xiàn)自定義的ClassLoader來完畢特定的功能。下面我們用例子說明ClassLoader。JVM規(guī)范定義了兩種類型的ClassLoader:BootstrapClassLoader和User-definedClassLoader。JVM在運營時會產生三個ClassLoader:BootstrapClassLoader、ExtensionClassLoader和AppClassLoader。Bootstrap是用C++編寫的,我們在Java中看不到它,是null,是JVM自帶的類裝載器,用來裝載核心類庫,如java.lang.*等。AppClassLoader的Parent是ExtClassLoader,而ExtClassLoader的Parent為BootstrapClassLoader。java中,什么叫不可更改的類(immutableclass)(KevinTam)從字面意思來理解就是不會發(fā)生變化的類,那么是什么不會發(fā)生變化呢,其實就是類的狀態(tài),也就是不變類的實例一旦被創(chuàng)建,其狀態(tài)就不會發(fā)生變化,舉個例子:假如人是一個class,那么我們中的每一個都是人這個類的具體的instance,假如人這個類只有一個狀態(tài)就是生身父母,那么它就是一個不變類,由于每一個人在出生的那一剎那,生身父母就已經被設立了值,并且終生都不會發(fā)生變化。不變類有什么好處呢?1)不變類是線程安全的,由于不變類的狀態(tài)在創(chuàng)建以后不再發(fā)生變化,所以它可以在線程之間共享,而不需要同步。2)不變類的instance可以被reuse創(chuàng)建類的實例需要花費CPU的時間,當這個實例不再被引用時,將會被垃圾回收掉,這時候,又需要花費CPU的時間。對于不變類而言,一個好處就是可以將常用的實例進行緩存,從而減少了對象的創(chuàng)建。舉個例子,對于布爾型,最常用的便是trueandfalse。JDK中的Boolean類就是一個不變類,并且對這兩個實例進行了緩沖。publicfinalclassBooleanimplementsjava.io.Serializable{/***The<code>Boolean</code>objectcorrespondingtotheprimitive*value<code>true</code>.*/publicstaticfinalBooleanTRUE=newBoolean(true);/***The<code>Boolean</code>objectcorrespondingtotheprimitive*value<code>false</code>.*/publicstaticfinalBooleanFALSE=newBoolean(false);//這個方法不會創(chuàng)建新的對象,而是重用已經創(chuàng)建好的instancepublicstaticBooleanvalueOf(booleanb){return(b?TRUE:FALSE);}}3)不變類的某些方法可以緩存計算的結果hashCode這個方法來自于Object這個類,這個方法用來返回對象的hashCode,重要用于將對象放置到hashtable中時,來擬定這個對象的存儲位置。對于一個不變類的實例,它的hashCode也是不變的,所以就可以緩存這個計算的結果,來提高性能,避免不必要的運算,JDK中的String類就是一個例子。publicfinalclassString{/**Cachethehashcodeforthestring*/privat(yī)einthash;//Defaultto0publicinthashCode(){inth=hash;if(h==0){//computethevaluehash=h;//cachethevalue}returnh;}}在JDK中,String,theprimitivewrapperclasses,andBigIntegerandBigDecimal都是不變類。假如一個類是不變類,這個類是不是就不能有改變狀態(tài)的方法呢?答案當然是否認的,String是一個不變類,仍然有replace,replaceAll這樣的方法,而String仍然是一個不變類,那是由于在這些改變狀態(tài)的方法中,每次都是新創(chuàng)建一個String對象。假如大家理解了不變類,那也就不難理解為什么在做String的concat(yī)enate時,應當用StringBuffer而不是用+的操作符。如何對的使用String呢?1)不要用new去創(chuàng)建String對象。假如使用new去創(chuàng)建String,那么每次都會創(chuàng)建一個新對象。publicstaticvoidmain(String[]args){StringA1="A";StringA2="A";//Itwon'tcreateanewobjectcheckInstance(A1,A2);//Result:TheyaresameinstancesStringB1=newString("A");//createanewobjectStringB2=newString("A");//creatanewobjectcheckInstance(B1,B2);//Result:Theyaredifferentinstances}privatestaticvoidcheckInstance(Stringa1,Stringa2){if(a1==a2){System.out.println("Theyaresameinstances");}else{System.out.println("Theyaredifferentinstances");}}2)應當用StringBuffer來做連接操作由于String是一個不變類,那么在做連接操作時,就會創(chuàng)建臨時對象來保存中間的運算結果,而StringBuffer是一個mutableclass,這樣就不需要創(chuàng)建臨時的對象來保存結果,從而提高了性能。JAVAGarbageCollection(Vincent)垃圾分代回收算法(GenerationalCollecting)
基于對對象生命周期分析后得出的垃圾回收算法。把對象分為年青代、年老代、持久代,對不同生命周期的對象使用不同的算法(上述方式中的一個)進行回收?,F(xiàn)在的垃圾回收器(從J2SE1.2開始)都是使用此算法的。
??如上圖所示,為Java堆中的各代分布。?1.Young(年輕代)JVMspecification中的Heap的一部份
年輕代分三個區(qū)。一個Eden區(qū),兩個Survivor區(qū)。大部分對象在Eden區(qū)中生成。當Eden區(qū)滿時,還存活的對象將被復制到Survivor區(qū)(兩個中的一個),當這個Survivor區(qū)滿時,此區(qū)的存活對象將被復制到此外一個Survivor區(qū),當這個Survivor去也滿了的時候,從第一個Survivor區(qū)復制過來的并且此時還存活的對象,將被復制年老區(qū)(Tenured);。需要注意,Survivor的兩個區(qū)是對稱的,沒先后關系,所以同一個區(qū)中也許同時存在從Eden復制過來對象,和從前一個Survivor復制過來的對象,而復制到年老區(qū)的只有從第一個Survivor去過來的對象。并且,Survivor區(qū)總有一個是空的。
2.Tenured(年老代)JVMspecificat(yī)ion中的Heap的一部份
年老代存放從年輕代存活的對象。一般來說年老代存放的都是生命期較長的對象。?3.Perm(持久代)JVMspecification中的Methodarea?用于存放靜態(tài)文獻,如今Java類、方法等。持久代對垃圾回收沒有顯著影響,但是有些應用也許動態(tài)生成或者調用一些class,例如Hibernate等,在這種時候需要設立一個比較大的持久代空間來存放這些運營過程中新增的類。持久代大小通過-XX:MaxPermSize=進行設立。?采用分區(qū)管理機制的JVM將JVM所管理的所有內存資源分為2個大的部分。永久存儲區(qū)(PermanentSpace)和堆空間(TheHeapSpace)。其中堆空間又分為新生區(qū)(Young(New)generationspace)和養(yǎng)老區(qū)(Tenure(Old)generationspace),新生區(qū)又分為伊甸園(Edenspace),幸存者0區(qū)(Survivor0space)和幸存者1區(qū)(Survivor1space)。具體分區(qū)如下圖:
?那JVM他的這些分區(qū)各有什么用途,請看下面的解說。
永久存儲區(qū)(PermanentSpace):永久存儲區(qū)是JVM的駐留內存,用于存放JDK自身所攜帶的Class,Interface的元數(shù)據(jù),應用服務器允許必須的Class,Interface的元數(shù)據(jù)和Java程序運營時需要的Class和Interface的元數(shù)據(jù)。被裝載進此區(qū)域的數(shù)據(jù)是不會被垃圾回收器回收掉的,關閉JVM時,釋放此區(qū)域所控制的內存。?
堆空間(TheHeapSpace):是JAVA對象生死存亡的地區(qū),JAVA對象的出生,成長,死亡都在這個區(qū)域完畢。堆空間又分別按JAVA對象的創(chuàng)建和年齡特性分為養(yǎng)老區(qū)和新生區(qū)。??新生區(qū)(Young(New)generationspace):新生區(qū)的作用涉及JAVA對象的創(chuàng)建和從JAVA對象中篩選出能進入養(yǎng)老區(qū)的JAVA對象。??伊甸園(Edenspace):JAVA對空間中的所有對象在此出生,該區(qū)的名字因此而得名。也即是說當你的JAVA程序運營時,需要創(chuàng)建新的對象,JVM將在該區(qū)為你創(chuàng)建一個指定的對象供程序使用。創(chuàng)建對象的依據(jù)即是永久存儲區(qū)中的元數(shù)據(jù)。?
幸存者0區(qū)(Survivor0space)和幸存者1區(qū)(Survivor1space):當伊甸園的空間用完時,程序又需要創(chuàng)建對象;此時JVM的垃圾回收器將對伊甸園區(qū)進行垃圾回收,將伊甸園區(qū)中的不再被其他對象所引用的對象進行銷毀工作。同時將伊甸園中的尚有其他對象引用的對象移動到幸存者0區(qū)。幸存者0區(qū)就是用于存放伊甸園垃圾回收時所幸存下來的JAVA對象。當將伊甸園中的尚有其他對象引用的對象移動到幸存者0區(qū)時,假如幸存者0區(qū)也沒有空間來存放這些對象時,JVM的垃圾回收器將對幸存者0區(qū)進行垃圾回收解決,將幸存者0區(qū)中不在有其他對象引用的JAVA對象進行銷毀,將幸存者0區(qū)中尚有其他對象引用的對象移動到幸存者1區(qū)。幸存者1區(qū)的作用就是用于存放幸存者0區(qū)垃圾回收解決所幸存下來的JAVA對象。
養(yǎng)老區(qū)(Tenure(Old)generationspace):用于保存從新生區(qū)篩選出來的JAVA對象。
上面我們看了JVM的內存分區(qū)管理,現(xiàn)在我們來看JVM的垃圾回收工作是如何運作的。一方面當啟動J2EE應用服務器時,JVM隨之啟動,并將JDK的類和接口,應用服務器運營時需要的類和接口以及J2EE應用的類和接口定義文獻也及編譯后的Class文獻或JAR包中的Class文獻裝載到JVM的永久存儲區(qū)。在伊甸園中創(chuàng)建JVM,應用服務器運營時必須的JAVA對象,創(chuàng)建J2EE應用啟動時必須創(chuàng)建的JAVA對象;J2EE應用啟動完畢,可對外提供服務。?JVM在伊甸園區(qū)根據(jù)用戶的每次請求創(chuàng)建相應的JAVA對象,當伊甸園的空間局限性以用來創(chuàng)建新JAVA對象的時候,JVM的垃圾回收器執(zhí)行對伊甸園區(qū)的垃圾回收工作,銷毀那些不再被其他對象引用的JAVA對象(假如該對象僅僅被一個沒有其他對象引用的對象引用的話,此對象也被歸為沒有存在的必要,依此類推),并將那些被其他對象所引用的JAVA對象移動到幸存者0區(qū)。
假如幸存者0區(qū)有足夠控件存放則直接放到幸存者0區(qū);假如幸存者0區(qū)沒有足夠空間存放,則JVM的垃圾回收器執(zhí)行對幸存者0區(qū)的垃圾回收工作,銷毀那些不再被其他對象引用的JAVA對象(假如該對象僅僅被一個沒有其他對象引用的對象引用的話,此對象也被歸為沒有存在的必要,依此類推),并將那些被其他對象所引用的JAVA對象移動到幸存者1區(qū)。?假如幸存者1區(qū)有足夠控件存放則直接放到幸存者1區(qū);假如幸存者0區(qū)沒有足夠空間存放,則JVM的垃圾回收器執(zhí)行對幸存者0區(qū)的垃圾回收工作,銷毀那些不再被其他對象引用的JAVA對象(假如該對象僅僅被一個沒有其他對象引用的對象引用的話,此對象也被歸為沒有存在的必要,依此類推),并將那些被其他對象所引用的JAVA對象移動到養(yǎng)老區(qū)。
假如養(yǎng)老區(qū)有足夠控件存放則直接放到養(yǎng)老區(qū);假如養(yǎng)老區(qū)沒有足夠空間存放,則JVM的垃圾回收器執(zhí)行對養(yǎng)老區(qū)區(qū)的垃圾回收工作,銷毀那些不再被其他對象引用的JAVA對象(假如該對象僅僅被一個沒有其他對象引用的對象引用的話,此對象也被歸為沒有存在的必要,依此類推),并保存那些被其他對象所引用的JAVA對象。假如到最后養(yǎng)老區(qū),幸存者1區(qū),幸存者0區(qū)和伊甸園區(qū)都沒有空間的話,則JVM會報告“JVM堆空間溢出(java.lang.OutOfMemoryError:Javaheapspace)”,也即是在堆空間沒有空間來創(chuàng)建對象。?這就是JVM的內存分區(qū)管理,相比不分區(qū)來說;一般情況下,垃圾回收的速度要快很多;由于在沒有必要的時候不用掃描整片內存而節(jié)省了大量時間。?通常大家還會碰到此外一種內存溢犯錯誤“永久存儲區(qū)溢出(java.lang.OutOfMemoryError:JavaPermanentSpace)”。所有的垃圾收集算法都面臨同一個問題,那就是找出應用程序不可到達的內存塊,將其釋放,這里面得不可到達重要是指應用程序已經沒有內存塊的引用了,而在JAVA中,某個對象相應用程序是可到達的是指:這個對象被根(根重要是指類的靜態(tài)變量,或者活躍在所有線程棧的對象的引用)引用或者對象被另一個可到達的對象引用。Reference
Counting(引用計數(shù))
引用計數(shù)是最簡樸直接的一種方式,這種方式在每一個對象中增長一個引用的計數(shù),這個計數(shù)代表當前程序有多少個引用引用了此對象,假如此對象的引用計數(shù)變?yōu)?,那么此對象就可以作為垃圾收集器的目的對象來收集。優(yōu)點:簡樸,直接,不需要暫停整個應用缺陷:1.需要編譯器的配合,編譯器要生成特殊的指令來進行引用計數(shù)的操作,比如每次將對象賦值給新的引用,或者者對象的引用超過了作用域等。2.不能解決循環(huán)引用的問題跟蹤收集器跟蹤收集器一方面要暫停整個應用程序,然后開始從根對象掃描整個堆,判斷掃描的對象是否有對象引用,這里面有三個問題需要搞清楚:1.假如每次掃描整個堆,那么勢必讓GC的時間變長,從而影響了應用自身的執(zhí)行。因此在JVM里面采用了分代收集,在新生代收集的時候minor
gc只需要掃描新生代,而不需要掃描老生代。2.JVM采用了分代收集以后,minor
gc只掃描新生代,但是minor
gc怎么判斷是否有老生代的對象引用了新生代的對象,JVM采用了卡片標記的策略,卡片標記將老生代提成了一塊一塊的,劃分以后的每一個塊就叫做一個卡片,JVM采用卡表維護了每一個塊的狀態(tài),當JAVA程序運營的時候,假如發(fā)現(xiàn)老生代對象引用或者釋放了新生代對象的引用,那么就JVM就將卡表的狀態(tài)設立為臟狀態(tài),這樣每次minor
gc的時候就會只掃描被標記為臟狀態(tài)的卡片,而不需要掃描整個堆。具體如下圖:3.GC在收集一個對象的時候會判斷是否有引用指向對象,在JAVA中的引用重要有四種:Strong
reference,Soft
reference,Weak
reference,Phantom
reference.Strong
Reference
強引用是JAVA中默認采用的一種方式,我們平時創(chuàng)建的引用都屬于強引用。假如一個對象沒有強引用,那么對象就會被回收。public
void
testStrongReference(){Object
referent
=
new
Object();Object
strongReference
=
referent;referent
=
null;System.gc();assertNotNull(strongReference);}
Soft
Reference軟引用的對象在GC的時候不會被回收,只有當內存不夠用的時候才會真正的回收,因此軟引用適合緩存的場合,這樣使得緩存中的對象可以盡量的再內存中待長期一點。Public
void
testSoftReference(){String
str
=
"test";SoftReference<String>
softreference
=
new
SoftReference<String>(str);str=null;System.gc();assertNotNull(softreference.get());}
Weak
reference弱引用有助于對象更快的被回收,假如一個對象沒有強引用只有弱引用,那么在GC后,這個對象肯定會被回收。Public
void
testWeakReference(){String
str
=
"test";WeakReference<String>
weakReference
=
new
WeakReference<String>(str);str=null;System.gc();assertNull(weakReference.get());}
Phantom
reference
Mark-Sweep
Collector(標記-清除收集器)標記清除收集器最早由Lisp的發(fā)明人于1960年提出,標記清除收集器停止所有的工作,從根掃描每個活躍的對象,然后標記掃描過的對象,標記完畢以后,清除那些沒有被標記的對象。優(yōu)點:1
解決循環(huán)引用的問題2
不需要編譯器的配合,從而就不執(zhí)行額外的指令缺陷:1.每個活躍的對象都要進行掃描,收集暫停的時間比較長。Copying
Collector(復制收集器)復制收集器將內存分為兩塊同樣大小空間,某一個時刻,只有一個空間處在活躍的狀態(tài),當活躍的空間滿的時候,GC就會將活躍的對象復制到未使用的空間中去,本來不活躍的空間就變?yōu)榱嘶钴S的空間。復制收集器具體過程可以參考下圖:優(yōu)點:1
只掃描可以到達的對象,不需要掃描所有的對象,從而減少了應用暫停的時間缺陷:1.需要額外的空間消耗,某一個時刻,總是有一塊內存處在未使用狀態(tài)2.復制對象需要一定的開銷Mark-Compact
Collector(標記-整理收集器)標記整理收集器汲取了標記清除和復制收集器的優(yōu)點,它分兩個階段執(zhí)行,在第一個階段,一方面掃描所有活躍的對象,并標記所有活躍的對象,第二個階段一方面清除未標記的對象,然后將活躍的的對象復制到堆得底部。標記整理收集器的過程示意圖請參考下圖:
Mark-compact策略極大的減少了內存碎片,并且不需要像Copy
Collector同樣需要兩倍的空間。JVM的垃圾收集策略
GC的執(zhí)行時要花費一定的CPU資源和時間的,因此在JDK1.2以后,JVM引入了分代收集的策略,其中對新生代采用"Mark-Compact"策略,而對老生代采用了“Mark-Sweep"的策略。其中新生代的垃圾收集器命名為“minor
gc”,老生代的GC命名為"Full
Gc
或者Major
GC".其中用System.gc()強制執(zhí)行的是Full
Gc.SpringIOCandAOP(Minjin)IoC和AOP都是Spring的核心思想??
當然,最為一個框架級的輕量組件,大量的配置文獻是不可缺少的,但是核心是要把這些配置文獻,配置節(jié)組裝起來,并將核心代碼編寫為完全業(yè)務無關的。我們看看Spring是怎么做的。
一方面,IoC,控制反轉。Spring開發(fā)的基本思想:面向接口的編程模式??蚣茏龅脑蕉?應當越能發(fā)現(xiàn)接口在其中起到的作用,而Spring將這種想法,開始貫徹到業(yè)務的開發(fā)中了。Bean的Set方法使用接口作為參數(shù),保證其擴展性,實現(xiàn)依賴關系的松偶爾。所謂的控制反轉,作為中文更好理解的一個翻譯應當是依賴注入,把依賴的類采用接口的方式,運用Set函數(shù),傳入Bean的內部,實現(xiàn)與外界的解耦合。這種注入也可作用于構造函數(shù)。
另一方面,AOP,面向切面的編程方式,我覺得更通俗的說法應當是對容器內的Bean進行方法干涉。被容器中創(chuàng)建的類,看起來執(zhí)行一個普通的函數(shù)調用,由于被容器預解決,而會在方法執(zhí)行前/后進行一些其他的、可配置的操作。當然,這種方法也同樣是面向接口的,或者直接使用反射的。運用java.lang.reflect.Invocat(yī)ionHandler接口可以達成這種干涉的效果。下面是轉載的一個簡樸示例。Java代碼import
java.lang.reflect.Invocat(yī)ionHandler;
import
java.lang.reflect.Method;
import
java.lang.reflect.Proxy;
public
class
DynaProxyHello
implements
InvocationHandler
{
private
Object
proxy;
private
Object
delegate;
public
Object
bind(Object
delegate,Object
proxy)
{
this.proxy
=
proxy;
this.delegate
=
delegate;
return
Proxy.newProxyInstance(
this.delegate.getClass().getClassLoader(),
this.delegate
.getClass().getInterfaces(),
this);
public
Object
invoke(Object
proxy,
Method
method,
Object[]
args)
throws
Throwable
{
Object
result
=
null;
try
{
//反射得到操作者的實例
Class
clazz
=
this.proxy.getClass();
//反射得到操作者的Start方法
Method
start
=
clazz.getDeclaredMethod("start",
new
Class[]
{
Method.class
});
//反射執(zhí)行start方法
start.invoke(this.proxy,
new
Object[]
{
method
});
//執(zhí)行要解決對象的原本方法
result
=
method.invoke(this.delegate,
args);
//
反射得到操作者的end方法
Method
end
=
clazz.getDeclaredMethod("end",
new
Class[]
{
Method.class
});
//
反射執(zhí)行end方法
end.invoke(thixy,
new
Object[]
{
method
});
}
cat(yī)ch
(Exception
e)
{
e.printStackTrace();
}
return
result;
}
}
public
class
Test
{
public
static
void
main(String[]
args)
{
IHello
hello
=
(IHello)new
DynaProxyHello().bind(new
Hello(),new
LoggerOperation());
hello.sayGoogBye("Double
J");
hello.sayHello("Double
J");
}
}
??
當然,不是只有這一個實現(xiàn)方式,java的代理功能只能代理接口,假如要代理類的化,可以使用cglib。?
Spring框架當然不會是上述的那么簡樸(事實上它非常復雜),但是我關注的是核心的實現(xiàn)方式和設計思想。在有些時候,我們不需要使用Spring,甚至不能使用Spring(比如不用Java開發(fā)),但是這種思想和方式是可以復用的。使用這種設計思想,按照當前的語言和環(huán)境規(guī)定,實現(xiàn)自己的IoC和AOP框架。Spring框架(Minjin)Spring框架是一個分層架構,由7個定義良好的模塊組成。Spring模塊構建在核心容器之上,核心容器定義了創(chuàng)建、配置和管理bean的方式,如圖1所示。
圖1.Spring框架的7個模塊
組成Spring框架的每個模塊(或組件)都可以單獨存在,或者與其他一個或多個模塊聯(lián)合實現(xiàn)。每個模塊的功能如下:核心容器:核心容器提供Spring框架的基本功能。核心容器的重要組件是BeanFactory,它是工廠模式的實現(xiàn)。BeanFactory使用控制反轉(IOC)模式將應用程序的配置和依賴性規(guī)范與實際的應用程序代碼分開。Spring上下文:Spring上下文是一個配置文獻,向Spring框架提供上下文信息。Spring上下文涉及公司服務,例如JNDI、EJB、電子郵件、國際化、校驗和調度功能。SpringAOP:通過配置管理特性,SpringAOP模塊直接將面向方面的編程功能集成到了Spring框架中。所以,可以很容易地使Spring框架管理的任何對象支持AOP。SpringAOP模塊為基于Spring的應用程序中的對象提供了事務管理服務。通過使用SpringAOP,不用依賴EJB組件,就可以將聲明性事務管理集成到應用程序中。SpringDAO:JDBCDAO抽象層提供了故意義的異常層次結構,可用該結構來管理異常解決和不同數(shù)據(jù)庫供應商拋出的錯誤消息。異常層次結構簡化了錯誤解決,并且極大地減少了需要編寫的異常代碼數(shù)量(例如打開和關閉連接)。SpringDAO的面向JDBC的異常遵從通用的DAO異常層次結構。SpringORM:Spring框架插入了若干個ORM框架,從而提供了ORM的對象關系工具,其中涉及JDO、Hibernat(yī)e和iBatisSQLMap。所有這些都遵從Spring的通用事務和DAO異常層次結構。SpringWeb模塊:Web上下文模塊建立在應用程序上下文模塊之上,為基于Web的應用程序提供了上下文。所以,Spring框架支持與JakartaStruts的集成。Web模塊還簡化了解決多部分請求以及將請求參數(shù)綁定到域對象的工作。SpringMVC框架:MVC框架是一個全功能的構建Web應用程序的MVC實現(xiàn)。通過策略接口,MVC框架變成為高度可配置的,MVC容納了大量視圖技術,其中涉及JSP、Velocity、Tiles、iText和POI。Spring框架的功能可以用在任何J2EE服務器中,大多數(shù)功能也合用于不受管理的環(huán)境。Spring的核心要點是:支持不綁定到特定J2EE服務的可重用業(yè)務和數(shù)據(jù)訪問對象。毫無疑問,這樣的對象可以在不同J2EE環(huán)境(Web或EJB)、獨立應用程序、測試環(huán)境之間重用。SpringAOP兩種實現(xiàn)機制(BaomingChai)SPRING是通過動態(tài)代理來實現(xiàn)AOP的,SPRING內部提供了2種實現(xiàn)機制?1.假如是有接口聲明的類進行AOP,spring調用的是java.lang.reflection.Proxy類來做解決org.springframework.aop.framework.JdkDynamicAopProxy publicObjectgetProxy(ClassLoaderclassLoader){ ?if(logger.isDebugEnabled()){?? ClasstargetClass=this.advised.getTargetSource().getTargetClass(); logger.debug("CreatingJDKdynamicproxy"+ ??(targetClass!=null?"for["+targetClass.getName()+"]":"")); ?}? Class[]proxiedInterfaces=AopProxyUtpleteProxiedInterfaces(this.advised);??returnProxy.newProxyInstance(classLoader,proxiedInterfaces,this);?}org.springframework.aop.framework.ReflectiveMethodInvocationpublicObjectproceed()throwsThrowable{? // Westartwithanindexof-1andincrementearly.? if(this.currentInterceptorIndex==this.interceptorsAndDynamicMethodMat(yī)chers.size()-1){? returninvokeJoinpoint();? } ObjectinterceptorOrInterceptionAdvice= erceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);??if(interceptorOrInterceptionAdviceinstanceofInterceptorAndDynamicMethodMatcher){ ??//Evaluatedynamicmethodmatcherhere:stat(yī)icpartwillalreadyhave //beenevaluat(yī)edandfoundtomatch.???InterceptorAndDynamicMethodMat(yī)cherdm= ?(InterceptorAndDynamicMethodMatcher)interceptorOrInterceptionAdvice;???if(dm.methodMat(yī)cher.matches(this.method,this.targetClass,this.arguments)){ ? returndm.interceptor.invoke(this); ?} ??else{? ?//Dynamicmatchingfailed.? ??//Skipthisinterceptorandinvokethenextinthechain.????returnproceed(); ??} }??else{ ? //It'saninterceptor,sowejustinvokeit:Thepointcutwillhave?? //beenevaluatedstat(yī)icallybeforethisobjectwasconstructed. ?return((MethodInterceptor)interceptorOrInterceptionAdvice).invoke(this); ?} }2.假如是沒有接口聲明的類呢?SPRING通過CGLIB包和內部類來實現(xiàn)privat(yī)estaticclassStaticUnadvisedInterceptorimplementsMethodInterceptor,Serializable{??privatefinalObjecttarget; ?publicStaticUnadvisedInterceptor(Objecttarget){???this.target=target; ?} ?publicObjectintercept(Objectproxy,Methodmethod,Object[]args, MethodProxymethodProxy)throwsThrowable{? ObjectretVal=methodProxy.invoke(target,args);???returnmassageReturnTypeIfNecessary(proxy,target,retVal); } }?/** *Methodinterceptorusedforstatictargetswithnoadvicechain,whenthe *proxyistobeexposed. */ privatestaticclassStaticUnadvisedExposedInterceptorimplementsMethodInterceptor,Serializable{ ?privatefinalObjecttarget;??publicStaticUnadvisedExposedInterceptor(Objecttarget){ ? this.target=target;??} publicObjectintercept(Objectproxy,Methodmethod,Object[]args,MethodProxymethodProxy)throwsThrowable{ ? ObjectoldProxy=null; try{ ?oldProxy=AopContext.setCurrentProxy(proxy);? ?ObjectretVal=methodProxy.invoke(target,args); returnmassageReturnTypeIfNecessary(proxy,target,retVal);? ?} ? finally{?? ?AopContext.setCurrentProxy(oldProxy); ?} }?}?/** *Interceptorusedtoinvokeadynamictargetwithoutcreatingamethod?*invocationorevaluat(yī)inganadvicechain.(Weknowtherewasnoadvice?*forthismethod.) */ privateclassDynamicUnadvisedInterceptorimplementsMethodInterceptor,Serializable{??publicObjectintercept(Objectproxy,Methodmethod,Object[]args,MethodProxymethodProxy)throwsThrowable{ Objecttarget=advised.getTargetSource().getTarget();? ?try{ ? ObjectretVal=methodProxy.invoke(target,args); ?? returnmassageReturnTypeIfNecessary(proxy,target,retVal); ?} finally{ ??advised.getTargetSource().releaseTarget(target); ? } }?} /**?*Interceptorforunadviseddynamictargetswhentheproxyneedsexposing.?*/?privateclassDynamicUnadvisedExposedInterceptorimplementsMethodInterceptor,Serializable{ ?publicObjectintercept(Objectproxy,Methodmethod,Object[]args,MethodProxymethodProxy)throwsThrowable{?? ObjectoldProxy=null; Objecttarget=advised.getTargetSource().getTarget(); ?try{? ? oldProxy=AopContext.setCurrentProxy(proxy); ? ?ObjectretVal=methodProxy.invoke(target,args);? ??returnmassageReturnTypeIfNecessary(proxy,target,retVal); ?} ??finally{ AopContext.setCurrentProxy(oldProxy); ? advised.getTargetSource().releaseTarget(target); } } }我們自己也可以來試試?1.jdkproxy方式?
先來一個接口
IHelloWorld.javapackagekris.aop.test;publicinterfaceIHelloWorld{?publicvoidprint(Stringname); publicvoidwrite(Stringsth);}再來一個實現(xiàn)
?HelloWorld.javapackagekris.aop.test;publicclassHelloWorldimplementsIHelloWorld{?publicvoidprint(Stringname){ ?System.out.println("HelloWorld"+name);?} publicvoidwrite(Stringsth){??System.out.println("write"+sth); ? }}代理類
DefaultInvocat(yī)ionHandler.javapackagekris.aop.test;importjava.lang.reflect.Invocat(yī)ionHandler;importjava.lang.reflect.Method;publicclassDefaultInvocationHandlerimplementsInvocationHandler{ /** *替換外部class調用的方法 *obj 外部已經已經包裝好InvocationHandler的實例?*method 外部方法 *args??方法參數(shù)?*/ publicObjectinvoke(Objectobj,Methodmethod,Object[]args)? throwsThrowable{ ?Strings1[]={"kris"};? Strings2[]={"anyone"}; IHelloWorldihw=newHelloWorld();? System.out.println("start!"); method.invoke(ihw,args);??method.invoke(ihw,s1);??Objecto=method.invoke(ihw,s2);??System.out.println("stop!");??returno; }}測試類?Test.javapackagekris.aop.test;importjava.lang.reflect.InvocationHandler;importjava.lang.reflect.Proxy;publicclassTest{?publicstaticvoidmain(Stringargs[]){??Classclazz=newHelloWorld().getClass();? ClassLoadercl=clazz.getClassLoader();? Classclasses[]=clazz.getInterfaces(); InvocationHandlerih=newDefaultInvocationHandler(); //用InvocationHandler給HelloWorld進行AOP包裝 ?IHelloWorldihw=(IHelloWorld)Proxy.newProxyInstance(cl,classes,ih);? ihw.print("test");??ihw.write("test"); }}2.用CGLIB包實現(xiàn),一方面不要忘了引入那個包packagekris.aop.cglib.test;publicclassHelloWorld{?publicvoidprint(Stringname){? System.out.println("HelloWorld"+name); }?publicvoidwrite(Stringsth){??System.out.println("write"+sth); }?publicvoidprint(){??System.out.println("HelloWorld"); }}代理類(沒用內部類,看起來清楚點)packagekris.aop.cglib.test;importjava.lang.reflect.Method;importnet.sf.cglixy.MethodInterceptor;importnet.sf.cglib.proxy.MethodProxy;publicclassMethodInterceptorImplimplementsMethodInterceptor{?publicObjectintercept(Objectobj,Methodmethod,Object[]args, ? MethodProxyproxy)throwsThrowable{? System.out.println(method);??proxy.invokeSuper(obj,args); returnnull; }}測試類packagekris.aop.cglib.test;importnet.sf.cglib.proxy.Enhancer;publicclassTest{ publicstaticvoidmain(String[]args){??Enhancerenhancer=newEnhancer(); enhancer.setSuperclass(HelloWorld.class); //設立回調方法實現(xiàn)類 enhancer.setCallback(newMethodInterceptorImpl());??//實例化已經添加回調實現(xiàn)的HELLOWORLD實例 HelloWorldmy=(HelloWorld)enhancer.create(); my.print();?}}SpringAOP的底層實現(xiàn)技術(BaomingChai,StephenYu)AOP概述軟件的編程語言最終的目的就是用更自然更靈活的方式模擬世界,從原始機器語言到過程語言再到面向對象的語言,我們看到編程語言在一步步用更自然、更強大的方式描述軟件。AOP是軟件開發(fā)思想的一個奔騰,AOP的引入將有效填補OOP的局限性,OOP和AOP分別從縱向和橫向對軟件進行抽象,有效地消除反復性的代碼,使代碼以更優(yōu)雅的更有效的方式進行邏輯表達。AOP有三種植入切面的方法:其一是編譯期織入,這規(guī)定使用特殊的Java編譯器,AspectJ是其中的代表者;其二是類裝載期織入,而這規(guī)定使用特殊的類裝載器,AspectJ和AspectWerkz是其中的代表者;其三為動態(tài)代理織入,在運營期為目的類添加增強生成子類的方式,SpringAOP采用動態(tài)代理織入切面。SpringAOP使用了兩種代理機制,一種是基于JDK的動態(tài)代理,另一種是基于CGLib的動態(tài)代理,之所以需要兩種代理機制,很大限度上是由于JDK自身只提供基于接口的代理,不支持類的代理?;贘DK的代理和基于CGLib的代理是SpringAOP的核心實現(xiàn)技術,結識這兩代理技術,有助于探究SpringAOP的實現(xiàn)機理。只要你樂意,你甚至可以拋開Spring,提供自己的AOP實現(xiàn)。帶有橫切邏輯的實例
?一方面,我們來看一個無法通過OOP進行抽象的反復代碼邏輯,它們就是AOP改造的重要對象。下面,我們通過一個業(yè)務方法性能監(jiān)視的實例了解橫切邏輯。業(yè)務方法性能監(jiān)視,在每一個業(yè)務方法調用之前開始監(jiān)視,業(yè)務方法結束后結束監(jiān)視并給出性能報告:代碼清單2ForumService:包含性能監(jiān)視橫切代碼packagecom.baobaotao.proxy;?publicclassForumServiceImplimplementsForumService...{
publicvoidremoveTopic(inttopicId)...{
//開始性能監(jiān)視?
PerformanceMonitor.begin("com.baobaotao.proxy.ForumServiceImpl.removeTopic");?
System.out.println("模擬刪除Topic記錄:"+topicId);?
try...{
Thread.currentThread().sleep(20);
}catch(Exceptione)...{?
thrownewRuntimeException(e);?
}?
//結束監(jiān)視、并給出性能報告信息?
PerformanceMonitor.end();
}
publicvoidremoveForum(intforumId)...{
//開始性能監(jiān)視?PerformanceMonitor.begin("com.baobaotao.proxy.ForumServiceImpl.removeForum");?
System.out.println("模擬刪除Forum記錄:"+forumId);?
try...{?
Thread.currentThread().sleep(40);?
}catch(Exceptione)...{?
thrownewRuntimeException(e);?
}?
//結束監(jiān)視、并給出性能報告信息
PerformanceMonitor.end();?
}?}代碼清單2中粗體表達的代碼就是具有橫切特性的代碼,需要進行性能監(jiān)視的每個業(yè)務方法的前后都需要添加類似的性能監(jiān)視語句。?
我們保證實例的完整性,我們提供了一個非常簡樸的性能監(jiān)視實現(xiàn)類,如所示代碼清單3所示:代碼清單3PerformanceMonitorpackagecom.baobaotao.proxy;publicclassPerformanceMonitor{
//通過一個ThreadLocal保存線程相關的性能監(jiān)視信息
privat(yī)estaticThreadLocal<MethodPerformace>performaceRecord=?newThreadLocal<MethodPerformace>();
publicstaticvoidbegin(Stringmethod){
System.out.println("beginmonitor...");
MethodPerformacemp=newMethodPerformace(method);
performaceRecord.set(mp);?
}
publicstat(yī)icvoidend(){
System.out.println("endmonitor...");
MethodPerformacemp=performaceRecord.get();?
mp.printPerformace();//打印出業(yè)務方法性能監(jiān)視的信息
}
}PerformanceMonitor提供了兩個方法,begin(Stringmethod)方法開始對某個業(yè)務類方法的監(jiān)視,method為業(yè)務方法的署名,而end()方法結束對業(yè)務方法的監(jiān)視,并給出性能監(jiān)視的信息。由于每一個業(yè)務方法都必須單獨記錄性能監(jiān)視數(shù)據(jù),所以我們使用了ThreadLocal,ThreadLocal是削除非線程安全狀態(tài)的不二法寶。ThreadLocal中的元素為方法性能記錄對象MethodPerformace,它的代碼如下所示:代碼清單4MethodPerformacepackagecom.baobaotao.proxy;?publicclassMethodPerformace{?
privatelongbegin;
privat(yī)elongend;?
privat(yī)eStringserviceMethod;?
publicMethodPerformace(StringserviceMethod){
this.serviceMethod=serviceMethod;
this.begin=System.currentTimeMillis();//記錄方法調用開始時的系統(tǒng)時間?
}?
publicvoidprintPerformace(){?
//以下兩行程序得到方法調用后的系統(tǒng)時間,并計算出方法執(zhí)行花費時間
end=System.currentTimeMillis();?
longelapse=end-begin;
//報告業(yè)務方法執(zhí)行時間?
System.out.println(serviceMethod+"花費"+elapse+"毫秒。");
}?}通過下面代碼測試這個擁有方法性能監(jiān)視能力的業(yè)務方法:packagecom.baobaotao.proxy;
publicclassTestForumService{?
publicstaticvoidmain(String[]args){?
ForumServiceforumService=newForumServiceImpl();?
forumService.removeForum(10);
forumService.removeTopic(1012);
}
}我們得到以下的輸出信息:beginmonitor...?模擬刪除Forum記錄:10
endmonitor...
com.baobaxy.ForumServiceImpl.removeForum花費47毫秒。beginmonitor...?模擬刪除Topic記錄:1012
endmonitor...?com.baobaotao.proxy.ForumServiceImpl.removeTopic花費16毫秒。如實例所示,要對業(yè)務類進行性能監(jiān)視,就必須在每個業(yè)務類方法的前后兩處添加上反復性的啟動性能監(jiān)視和結束性能監(jiān)視的代碼。這些非業(yè)務邏輯的性能監(jiān)視代碼破壞了作為業(yè)務類ForumServiceImpl的純粹性。下面,我們分別JDK動態(tài)代理和CGLib動態(tài)代理技術,將業(yè)務方法中啟動和結束性能監(jiān)視的這些橫切代碼從業(yè)務類中完畢移除。JDK動態(tài)代理
?在JDK1.3以后提供了動態(tài)代理的技術,允許開發(fā)者在運營期創(chuàng)建接口的代理實例。在Sun剛推出動態(tài)代理時,還很難想象它有多大的實際用途,現(xiàn)在我們終于發(fā)現(xiàn)動態(tài)代理是實現(xiàn)AOP的絕好底層技術。
JDK的動態(tài)代理重要涉及到java.lang.reflect包中的兩個類:Proxy和InvocationHandler。其中InvocationHandler是一個接口,可以通過實現(xiàn)該接口定義橫切邏輯,在并通過反射機制調用目的類的代碼,動態(tài)將橫切邏輯和業(yè)務邏輯編織在一起。
?而Proxy為InvocationHandler實現(xiàn)類動態(tài)創(chuàng)建一個符合某一接口的代理實例。這樣講一定很抽象,我們立即著手動用Proxy和InvocationHandler這兩個魔法戒對上一節(jié)中的性能監(jiān)視代碼進行AOP式的改造。
?一方面,我們從業(yè)務類ForumServiceImpl中刪除性能監(jiān)視的橫切代碼,使ForumServiceImpl只負責具體的業(yè)務邏輯,如所示:代碼清單5ForumServiceImpl:移除性能監(jiān)視橫切代碼packagecom.baobaotao.proxy;
publicclassForumServiceImplimplementsForumService{
publicvoidremoveTopic(inttopicId){
①
System.out.println("模擬刪除Topic記錄:"+topicId);?
try{
Thread.currentThread().sleep(20);?
}catch(Exceptione){?
thrownewRuntimeException(e);
}
②
}?
publicvoidremoveForum(intforumId){?
①
System.out.println("模擬刪除Forum記錄:"+forumId);?
try{?
Thread.currentThread().sleep(40);
}catch(Exceptione){
thrownewRuntimeException(e);?
}
②?
}?}在代碼清單5中的①和②處,本來的性能監(jiān)視代碼被移除了,我們只保存了真正的業(yè)務邏輯。
?從業(yè)務類中移除的橫切代碼當然還得找到一個寄居之所,InvocationHandler就是橫切代碼的家園樂土,我們將性能監(jiān)視的代碼安頓在PerformaceHandler中,如代碼清單6所示:代碼清單6PerformaceHandlerpackagecom.baobaotao.proxy;
importjava.lang.reflect.Invocat(yī)ionHandler;?importjava.lang.reflect.Method;publicclassPerformaceHandlerimplementsInvocat(yī)ionHandler{
privateObjecttarget;?
publicPerformaceHandler(Objecttarget){//①target為目的的業(yè)務類?
this.target=target;
}?
publicObjectinvoke(Objectproxy,Methodmethod,Object[]args)
throwsThrowable{?
PerformanceMonitor.begin(target.getClass().getName()+"."+method.getName());?
Objectobj=method.invoke(target,args);//②通過反射方法調用目的業(yè)務類的業(yè)務方法?
PerformanceMonitor.end();?
returnobj;?
}?}粗體部分的代碼為性能監(jiān)視的橫切代碼,我們發(fā)現(xiàn),橫切代碼只出現(xiàn)一次,而不是本來那樣星灑各處。大家注意②處的method.invoke(),該語句通過反射的機制調用目的對象的方法,這樣Invocat(yī)ionHandler的invoke(Objectproxy,Methodmethod,Object[]args)方法就將橫切代碼和目的業(yè)務類代碼編織到一起了,所以我們可以將InvocationHandler當作是業(yè)務邏輯和橫切邏輯的編織器。下面,我們對這段代碼做進一步的說明。一方面,我們實現(xiàn)InvocationHandler接口,該接口定義了一個invoke(Objectproxy,Methodmethod,Object
溫馨提示
- 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
- 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權益歸上傳用戶所有。
- 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內容里面會有圖紙預覽,若沒有圖紙預覽就沒有圖紙。
- 4. 未經權益所有人同意不得將文件中的內容挪作商業(yè)或盈利用途。
- 5. 人人文庫網(wǎng)僅提供信息存儲空間,僅對用戶上傳內容的表現(xiàn)方式做保護處理,對用戶上傳分享的文檔內容本身不做任何修改或編輯,并不能對任何下載內容負責。
- 6. 下載文件中如有侵權或不適當內容,請與我們聯(lián)系,我們立即糾正。
- 7. 本站不保證下載資源的準確性、安全性和完整性, 同時也不承擔用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。
最新文檔
- 事業(yè)單位職工固定期限勞動協(xié)議樣本2024版一
- 臨時工地建設協(xié)議2024版版B版
- 魚類疾病防控技術研究與應用-洞察分析
- 移動端無限滾動用戶體驗優(yōu)化-洞察分析
- 系統(tǒng)優(yōu)化與決策-洞察分析
- 行權價與期權定價誤差-洞察分析
- 印刷油墨環(huán)?;芯?洞察分析
- 虛擬現(xiàn)實保齡球系統(tǒng)設計-洞察分析
- 二零二五年度鍋爐設備更換與維護承包合同3篇
- 2025年度海洋油氣平臺租賃及股權交易合同3篇
- DB33T 2570-2023 營商環(huán)境無感監(jiān)測規(guī)范 指標體系
- 上海市2024年中考英語試題及答案
- 房屋市政工程生產安全重大事故隱患判定標準(2024版)宣傳海報
- 房屋市政工程生產安全重大事故隱患判定標準(2024版)宣傳畫冊
- 2025年道路運輸企業(yè)客運駕駛員安全教育培訓計劃
- 南京工業(yè)大學浦江學院《線性代數(shù)(理工)》2022-2023學年第一學期期末試卷
- 2024版機床維護保養(yǎng)服務合同3篇
- 《論拒不執(zhí)行判決、裁定罪“執(zhí)行能力”之認定》
- 工程融資分紅合同范例
- 2024年貴州省公務員錄用考試《行測》真題及答案解析
- 2024國家安全員資格考試題庫加解析答案
評論
0/150
提交評論