網頁

聊聊 Eclipse Gradle 相依關係管理



Eclipse Gradle (plugin) 有一個不錯的功能叫Dependency Management,平常幾乎不太會去注意到它的存在,也不太會理會它是enabled還是disabled,唯一知道的是當compile自己gradle加入的所有dependency時,它不要跳紅字出錯就可以,其他的交給IDE去煩惱就行。

在某個專案裡,這個功能不知為何一直被關閉的,自己也開始好奇這之間的開跟關到底有甚麼差別。

再切入主題之前,先來聊聊Transitive Dependency是甚麼。簡單說就是一個專案的相依的相依,第二個相依就叫Transitive dependency。更確切來說如果專案A引入lib B,而要讓lib B正常運行必須要引入lib C,那專案A勢必要也有lib C才能正常的使用lib B,C就算transitive dependency。

當專案小引入的dependency不多時,通常在相依的部分比較不會出錯,所以也比較無感。但一旦專案開始變大,用了更多不同的框架,有更多的相依時,其實開始會有相依版本相衝的可能。例如 A引入 B和C,而B引入D1,但C引入D2時,專案又是如何知道到底該用D1還是D2? 如果沒有特別注意,很常會發生NoSuchMethodError、ClassNotFoundException、NoClassDefFoundError等等的冏境。

在一般沒特別設定的情況下,Eclipse Gradle會幫我們自動引用版號比較高的相依,以上例來說,A最後會引用D2如果transitive沒有特別設定false。

所以回到主題,照上述的說法,其實我們會有很多個dependency,而這些相依是可以被畫成一個dependency tree。如果今天dependency management是開啟的狀態下,gradle會自動幫我們管理這個相依樹所有層級裡的相依遞迴,並選擇最適合的相依版號下載。但如果是關閉的話,則只會去對相依樹的第一層去做相依的下載。

Dependency management很重要,特別是要長期的考量一個軟體的維護性、安全性跟擴充性。

假設一個lib版號1.0.0 有三個method A, B, C,當它升級到1.1.0時,一個好的、有向上相容的lib通常是要無痛升級的,它應該還是要保有A,B,C三個方法,或許會在加個D方法或修改一下C方法,但基本上都不該影響線上軟體使用。當然,不可能每個lib都可以做到這麼夢幻,有時候升上去時,可能會發現原本的A方法整個被拿掉,C方法又引入其他不相容的相依等等情況,而原本專案有用到A和C方法時,就容易跳出上述的三種錯誤,甚至是不跳錯,但執行的最終結果卻是跟預期的不同,因為方法被異動了。

還有種情況是原本的lib它的group id換了,可能是做這個lib的公司被買走了、可能是改朝換代,不管怎樣,這情況很有可能造成一個專案裡同時持有多個版本的同一個相依。遇到這種情況時,其實自己也說不情清系統到底是吃哪一個版號的相依。例:大家常用的datasource c3p0,在0.9.1.2之前的group都是c3p0,但之後卻改成com.mchange,如果此時專案有引用quartz和hibernate-c3p0那就有可能有衝突,因為quartz是用group c3p0的0.9.1.1,而hibernate用的可能是com.mchange的0.9.2.1;照理說quartz的transitive=true時,它應該是要吃0.9.2.1的版本,但因為group id的不同,最後會照成兩者同時存在。這種時候可能就要特別地去把舊的group c3p0特別排除如下:

quartz的dependencies
compile("org.quartz-scheduler:quartz:2.2.2"){
        exclude group: "c3p0", module: "c3p0"
}

相依關係幾本上先敘述到這邊。未來如果要做相依的版號升級,其實都還是建議先跑一次gradle task dependencies,把相依樹跑出來,仔細看看每個相依關係後再去做升級,這樣才比較不容易出錯。



最後,eclipse gradle裡面的dependency management建議是啟用,讓它來幫我們管理這複雜的相依關係,提早知道自己的相依關係是有衝突的,好進一步的去做排解。