呦呦,這些代碼有點(diǎn)臭,重構(gòu)大法帶你秀(SPI接口化),skr~
如果說(shuō)
正常的重構(gòu)是為了消除代碼的壞味道, 那么高層次的重構(gòu)就是消除架構(gòu)的壞味道
最近由于需要將公司基礎(chǔ)架構(gòu)的組件進(jìn)行各種
兼容,適配以及二開(kāi),所以很多時(shí)候就需要對(duì)組件進(jìn)行重構(gòu),大家是不是在拿到公司老項(xiàng)目老代碼,又需要二開(kāi)或者重構(gòu)的時(shí)候,會(huì)頭很大,無(wú)從下手,我之前也一直是這樣的狀態(tài),不過(guò)在慢慢熟悉了一些重構(gòu)的思想和方法之后,就能稍微的得心應(yīng)手一些,下面我就開(kāi)始講下重構(gòu),然后會(huì)著重講下重構(gòu)中的SPI接口化。
先給大家看看最近通過(guò)使用SPI接口化,重構(gòu)的一個(gè)組件-分布式存儲(chǔ)。
重構(gòu)前的代碼結(jié)構(gòu)
好家伙,所有的第三方存儲(chǔ)都是寫(xiě)在一個(gè)模塊中的,各種阿里云,騰訊云,華為云等等,這樣的代碼架構(gòu)在前期可能在不需要經(jīng)常擴(kuò)展,二開(kāi)的時(shí)候,還是能用的。
但是當(dāng)某個(gè)新需求來(lái)的時(shí)候,比如我遇到的:需要支持多個(gè)云的多個(gè)賬號(hào)上傳下載功能,這個(gè)是因?yàn)樵诓煌脑粕?,不同賬號(hào)的權(quán)限,安全認(rèn)證等都是不太一樣的,所以在某一刻,這個(gè)需求就被提出來(lái)了,也就是你想上傳到哪個(gè)云的哪個(gè)賬號(hào)都可以。
然后拿到這個(gè)代碼,看了下這樣的架構(gòu),可能在這樣的基礎(chǔ)上完成需求也是沒(méi)有問(wèn)題的,但是擴(kuò)展很麻煩,而且代碼會(huì)越來(lái)越繁重,架構(gòu)會(huì)越來(lái)越復(fù)雜,不清晰。
所以我索性趁著這個(gè)機(jī)會(huì),就重構(gòu)一把,和其他同事也商量了下,決定分模塊,SPI化,好處就是根據(jù)你想使用的引入對(duì)應(yīng)的依賴,讓代碼架構(gòu)更加清晰,后續(xù)更加容易擴(kuò)展了!下面就是重構(gòu)后的大體架構(gòu):
是不是清楚多了,之后哪怕某個(gè)云存儲(chǔ)需要增加新功能,或者需要兼容更多的云也是比較容易的了。
好了,下面就讓我們開(kāi)始講講重構(gòu)大法~
重構(gòu)
重構(gòu)是什么?
重構(gòu)(Refactoring)就是通過(guò)調(diào)整程序代碼改善軟件的質(zhì)量、性能,使其程序的設(shè)計(jì)模式和架構(gòu)更趨合理,提高軟件的擴(kuò)展性和維護(hù)性。
重構(gòu)最重要的思想就是讓普通程序員也能寫(xiě)出優(yōu)秀的程序。
把優(yōu)化代碼質(zhì)量的過(guò)程拆解成一個(gè)個(gè)小的步驟,這樣重構(gòu)一個(gè)項(xiàng)目的巨大工作量就變成比如修改變量名、提取函數(shù)、抽取接口等等簡(jiǎn)單的工作目標(biāo)。
作為一個(gè)普通的程序員就可以通過(guò)實(shí)現(xiàn)這些易完成的工作目標(biāo)來(lái)提升自己的編碼能力,加深自己的項(xiàng)目認(rèn)識(shí),從而為最高層次的重構(gòu)打下基礎(chǔ)。
而且高層次的重構(gòu)依然是由無(wú)數(shù)個(gè)小目標(biāo)構(gòu)成,而不是長(zhǎng)時(shí)間、大規(guī)模地去實(shí)現(xiàn)。
重構(gòu)本質(zhì)是極限編程的一部分,完整地實(shí)現(xiàn)極限編程才能最大化地發(fā)揮重構(gòu)的價(jià)值。而極限編程本身就提倡擁抱變化,增強(qiáng)適應(yīng)性,因此分解極限編程中的功能去適應(yīng)項(xiàng)目的需求、適應(yīng)團(tuán)隊(duì)的現(xiàn)狀才是最好的操作模式。
重構(gòu)的重點(diǎn)
重復(fù)代碼,過(guò)長(zhǎng)函數(shù),過(guò)大的類,過(guò)長(zhǎng)參數(shù)列,發(fā)散式變化,霰彈式修改,依戀情結(jié),數(shù)據(jù)泥團(tuán),基本類型偏執(zhí),平行繼承體系,冗余類等
下面舉一些常用的或者比較基礎(chǔ)的例子:
一些基本的原則我覺(jué)得還是需要了解的
盡量避免過(guò)多過(guò)長(zhǎng)的創(chuàng)建Java對(duì)象 盡量使用局部變量 盡量使用StringBuilder和StringBuffer進(jìn)行字符串連接 盡量減少對(duì)變量的重復(fù)計(jì)算 盡量在finally塊中釋放資源 盡量緩存經(jīng)常使用的對(duì)象 不使用的對(duì)象及時(shí)設(shè)置為null 盡量考慮使用靜態(tài)方法 盡量在合適的場(chǎng)合使用單例 盡量使用final修飾符
下面是關(guān)于類和方法優(yōu)化:
重復(fù)代碼的提取 冗長(zhǎng)方法的分割 嵌套條件分支或者循環(huán)遞歸的優(yōu)化 提取類或繼承體系中的常量 提取繼承體系中重復(fù)的屬性與方法到父類
這里先簡(jiǎn)單介紹這些比較常規(guī)的重構(gòu)思想和原則,方法,畢竟今天的主角是SPI,下面有請(qǐng)SPI登場(chǎng)!
SPI
什么是SPI?
SPI全稱Service Provider Interface,是Java提供的一套用來(lái)被第三方實(shí)現(xiàn)或者擴(kuò)展的API,它可以用來(lái)啟用框架擴(kuò)展和替換組件。
它是一種服務(wù)發(fā)現(xiàn)機(jī)制,它通過(guò)在ClassPath路徑下的META-INF/services文件夾查找文件,自動(dòng)加載文件里所定義的類。
這一機(jī)制為很多框架擴(kuò)展提供了可能,比如在Dubbo、JDBC中都使用到了SPI機(jī)制。
下面就是SPI的機(jī)制過(guò)程

SPI實(shí)際上是基于接口的編程+策略模式+配置文件組合實(shí)現(xiàn)的動(dòng)態(tài)加載機(jī)制。
系統(tǒng)設(shè)計(jì)的各個(gè)抽象,往往有很多不同的實(shí)現(xiàn)方案,在面向的對(duì)象的設(shè)計(jì)里,一般推薦模塊之間基于接口編程,模塊之間不對(duì)實(shí)現(xiàn)類進(jìn)行硬編碼。
一旦代碼里涉及具體的實(shí)現(xiàn)類,就違反了可拔插的原則,如果需要替換一種實(shí)現(xiàn),就需要修改代碼。為了實(shí)現(xiàn)在模塊裝配的時(shí)候能不在程序里動(dòng)態(tài)指明,這就需要一種服務(wù)發(fā)現(xiàn)機(jī)制。
SPI就是提供這樣的一個(gè)機(jī)制:為某個(gè)接口尋找服務(wù)實(shí)現(xiàn)的機(jī)制。有點(diǎn)類似IOC的思想,就是將裝配的控制權(quán)移到程序之外,在模塊化設(shè)計(jì)中這個(gè)機(jī)制尤其重要。所以SPI的核心思想就是解耦。
SPI使用介紹
要使用Java SPI,一般需要遵循如下約定:
當(dāng)服務(wù)提供者提供了接口的一種具體實(shí)現(xiàn)后,在jar包的 META-INF/services目錄下創(chuàng)建一個(gè)以接口全限定名`為命名的文件,內(nèi)容為實(shí)現(xiàn)類的全限定名;接口實(shí)現(xiàn)類所在的jar包放在主程序的 classpath中;主程序通過(guò) java.util.ServiceLoder動(dòng)態(tài)裝載實(shí)現(xiàn)模塊,它通過(guò)掃描META-INF/services目錄下的配置文件找到實(shí)現(xiàn)類的全限定名,把類加載到JVM;SPI的實(shí)現(xiàn)類必須攜帶一個(gè)不帶參數(shù)的構(gòu)造方法;
SPI使用場(chǎng)景
概括地說(shuō),適用于:調(diào)用者根據(jù)實(shí)際使用需要,啟用、擴(kuò)展、或者替換框架的實(shí)現(xiàn)策略
以下是比較常見(jiàn)的例子:
數(shù)據(jù)庫(kù)驅(qū)動(dòng)加載接口實(shí)現(xiàn)類的加載 JDBC加載不同類型數(shù)據(jù)庫(kù)的驅(qū)動(dòng) 日志門(mén)面接口實(shí)現(xiàn)類加載 SLF4J加載不同提供商的日志實(shí)現(xiàn)類 Spring Spring中大量使用了SPI,比如:對(duì)servlet3.0規(guī)范對(duì)ServletContainerInitializer的實(shí)現(xiàn)、自動(dòng)類型轉(zhuǎn)換Type Conversion SPI(Converter SPI、Formatter SPI)等 Dubbo Dubbo中也大量使用SPI的方式實(shí)現(xiàn)框架的擴(kuò)展, 不過(guò)它對(duì)Java提供的原生SPI做了封裝,允許用戶擴(kuò)展實(shí)現(xiàn)Filter接口
SPI簡(jiǎn)單例子
先定義接口類
package com.test.spi.learn;
import java.util.List;
public interface Search {
public List<String> searchDoc(String keyword);
}
文件搜索實(shí)現(xiàn)
package com.test.spi.learn;
import java.util.List;
public class FileSearch implements Search{
@Override
public List<String> searchDoc(String keyword) {
System.out.println("文件搜索 "+keyword);
return null;
}
}
數(shù)據(jù)庫(kù)搜索實(shí)現(xiàn)
package com.test.spi.learn;
import java.util.List;
public class DBSearch implements Search{
@Override
public List<String> searchDoc(String keyword) {
System.out.println("數(shù)據(jù)庫(kù)搜索 "+keyword);
return null;
}
}
接下來(lái)可以在resources下新建META-INF/services/目錄,然后新建接口全限定名的文件:com.test.spi.learn.Search
里面加上我們需要用到的實(shí)現(xiàn)類
com.test.spi.learn.FileSearch
com.test.spi.learn.DBSearch
然后寫(xiě)一個(gè)測(cè)試方法
package com.test.spi.learn;
import java.util.Iterator;
import java.util.ServiceLoader;
public class TestCase {
public static void main(String[] args) {
ServiceLoader<Search> s = ServiceLoader.load(Search.class);
Iterator<Search> iterator = s.iterator();
while (iterator.hasNext()) {
Search search = iterator.next();
search.searchDoc("hello world");
}
}
}
可以看到輸出結(jié)果:
文件搜索 hello world
數(shù)據(jù)庫(kù)搜索 hello world
SPI原理解析
通過(guò)查看ServiceLoader的源碼,梳理了一下,實(shí)現(xiàn)的流程如下:
應(yīng)用程序調(diào)用ServiceLoader.load方法 ServiceLoader.load方法內(nèi)先創(chuàng)建一個(gè)新的ServiceLoader,并實(shí)例化該類中的成員變量,包括以下:
loader(ClassLoader類型,類加載器) acc(AccessControlContext類型,訪問(wèn)控制器) providers(LinkedHashMap<String,S>類型,用于緩存加載成功的類) lookupIterator(實(shí)現(xiàn)迭代器功能)
應(yīng)用程序通過(guò)迭代器接口獲取對(duì)象實(shí)例 ServiceLoader先判斷成員變量providers對(duì)象中(LinkedHashMap<String,S>類型)是否有緩存實(shí)例對(duì)象,
如果有緩存,直接返回。如果沒(méi)有緩存,執(zhí)行類的裝載,實(shí)現(xiàn)如下:
(1)讀取META-INF/services/下的配置文件,獲得所有能被實(shí)例化的類的名稱,值得注意的是,ServiceLoader可以跨越j(luò)ar包獲取META-INF下的配置文件(2)通過(guò)反射方法Class.forName()加載類對(duì)象,并用instance()方法將類實(shí)例化。(3)把實(shí)例化后的類緩存到providers對(duì)象中,(LinkedHashMap<String,S>類型) 然后返回實(shí)例對(duì)象。
總結(jié)
優(yōu)點(diǎn)
使用
SPI機(jī)制的優(yōu)勢(shì)是實(shí)現(xiàn)解耦,使得接口的定義與具體業(yè)務(wù)實(shí)現(xiàn)分離,而不是耦合在一起。應(yīng)用進(jìn)程可以根據(jù)實(shí)際業(yè)務(wù)情況啟用或替換具體組件。
缺點(diǎn)
不能按需加載。雖然 ServiceLoader做了延遲載入,但是基本只能通過(guò)遍歷全部獲取,也就是接口的實(shí)現(xiàn)類得全部載入并實(shí)例化一遍。如果你并不想用某些實(shí)現(xiàn)類,或者某些類實(shí)例化很耗時(shí),它也被載入并實(shí)例化了,這就造成了浪費(fèi)。獲取某個(gè)實(shí)現(xiàn)類的方式不夠靈活,只能通過(guò) Iterator形式獲取,不能根據(jù)某個(gè)參數(shù)來(lái)獲取對(duì)應(yīng)的實(shí)現(xiàn)類。多個(gè)并發(fā)多線程使用 ServiceLoader類的實(shí)例是不安全的。加載不到實(shí)現(xiàn)類時(shí)拋出并不是真正原因的異常,錯(cuò)誤很難定位。
看到上面這么多的缺點(diǎn),你肯定會(huì)想,有這些弊端為什么還要使用呢,沒(méi)錯(cuò),在重構(gòu)的過(guò)程中,
SPI接口化是一個(gè)非常有用的方式,當(dāng)你需要擴(kuò)展的時(shí)候,適配的時(shí)候,越早的使用你就會(huì)受利越早,在一個(gè)合適的時(shí)間,恰當(dāng)?shù)臋C(jī)會(huì)的時(shí)候,就鼓起勇氣,重構(gòu)吧!
好了。今天就說(shuō)到這了,我還會(huì)不斷分享自己的所學(xué)所想,希望我們一起走在成功的道路上!
