新聞中心
本文轉(zhuǎn)載自微信公眾號「狼王編程」,作者狼王。轉(zhuǎn)載本文請聯(lián)系狼王編程公眾號。

10余年的蒼溪網(wǎng)站建設(shè)經(jīng)驗(yàn),針對設(shè)計(jì)、前端、開發(fā)、售后、文案、推廣等六對一服務(wù),響應(yīng)快,48小時(shí)及時(shí)工作處理。營銷型網(wǎng)站的優(yōu)勢是能夠根據(jù)用戶設(shè)備顯示端的尺寸不同,自動(dòng)調(diào)整蒼溪建站的顯示方式,使網(wǎng)站能夠適用不同顯示終端,在瀏覽器中調(diào)整網(wǎng)站的寬度,無論在任何一種瀏覽器上瀏覽網(wǎng)站,都能展現(xiàn)優(yōu)雅布局與設(shè)計(jì),從而大程度地提升瀏覽體驗(yàn)。創(chuàng)新互聯(lián)建站從事“蒼溪網(wǎng)站設(shè)計(jì)”,“蒼溪網(wǎng)站推廣”以來,每個(gè)客戶項(xiàng)目都認(rèn)真落實(shí)執(zhí)行。
如果說 正常的重構(gòu)是為了消除代碼的壞味道, 那么高層次的重構(gòu)就是消除架構(gòu)的壞味道
最近由于需要將公司基礎(chǔ)架構(gòu)的組件進(jìn)行各種兼容,適配以及二開,所以很多時(shí)候就需要對組件進(jìn)行重構(gòu),大家是不是在拿到公司老項(xiàng)目老代碼,又需要二開或者重構(gòu)的時(shí)候,會(huì)頭很大,無從下手,我之前也一直是這樣的狀態(tài),不過在慢慢熟悉了一些重構(gòu)的思想和方法之后,就能稍微的得心應(yīng)手一些,下面我就開始講下重構(gòu),然后會(huì)著重講下重構(gòu)中的SPI接口化。
先給大家看看最近通過使用SPI接口化,重構(gòu)的一個(gè)組件-分布式存儲(chǔ)。
重構(gòu)前的代碼結(jié)構(gòu)
好家伙,所有的第三方存儲(chǔ)都是寫在一個(gè)模塊中的,各種阿里云,騰訊云,華為云等等,這樣的代碼架構(gòu)在前期可能在不需要經(jīng)常擴(kuò)展,二開的時(shí)候,還是能用的。
但是當(dāng)某個(gè)新需求來的時(shí)候,比如我遇到的:需要支持多個(gè)云的多個(gè)賬號上傳下載功能,這個(gè)是因?yàn)樵诓煌脑粕?,不同賬號的權(quán)限,安全認(rèn)證等都是不太一樣的,所以在某一刻,這個(gè)需求就被提出來了,也就是你想上傳到哪個(gè)云的哪個(gè)賬號都可以。
然后拿到這個(gè)代碼,看了下這樣的架構(gòu),可能在這樣的基礎(chǔ)上完成需求也是沒有問題的,但是擴(kuò)展很麻煩,而且代碼會(huì)越來越繁重,架構(gòu)會(huì)越來越復(fù)雜,不清晰。
所以我索性趁著這個(gè)機(jī)會(huì),就重構(gòu)一把,和其他同事也商量了下,決定分模塊,SPI化,好處就是根據(jù)你想使用的引入對應(yīng)的依賴,讓代碼架構(gòu)更加清晰,后續(xù)更加容易擴(kuò)展了!下面就是重構(gòu)后的大體架構(gòu):
是不是清楚多了,之后哪怕某個(gè)云存儲(chǔ)需要增加新功能,或者需要兼容更多的云也是比較容易的了。
好了,下面就讓我們開始講講重構(gòu)大法~
重構(gòu)
重構(gòu)是什么?
重構(gòu)(Refactoring)就是通過調(diào)整程序代碼改善軟件的質(zhì)量、性能,使其程序的設(shè)計(jì)模式和架構(gòu)更趨合理,提高軟件的擴(kuò)展性和維護(hù)性。
重構(gòu)最重要的思想就是讓普通程序員也能寫出優(yōu)秀的程序。
把優(yōu)化代碼質(zhì)量的過程拆解成一個(gè)個(gè)小的步驟,這樣重構(gòu)一個(gè)項(xiàng)目的巨大工作量就變成比如修改變量名、提取函數(shù)、抽取接口等等簡單的工作目標(biāo)。
作為一個(gè)普通的程序員就可以通過實(shí)現(xiàn)這些易完成的工作目標(biāo)來提升自己的編碼能力,加深自己的項(xiàng)目認(rèn)識,從而為最高層次的重構(gòu)打下基礎(chǔ)。
而且高層次的重構(gòu)依然是由無數(shù)個(gè)小目標(biāo)構(gòu)成,而不是長時(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ù)代碼,過長函數(shù),過大的類,過長參數(shù)列,發(fā)散式變化,霰彈式修改,依戀情結(jié),數(shù)據(jù)泥團(tuán),基本類型偏執(zhí),平行繼承體系,冗余類等
下面舉一些常用的或者比較基礎(chǔ)的例子:
一些基本的原則我覺得還是需要了解的
- 盡量避免過多過長的創(chuàng)建Java對象
- 盡量使用局部變量
- 盡量使用StringBuilder和StringBuffer進(jìn)行字符串連接
- 盡量減少對變量的重復(fù)計(jì)算
- 盡量在finally塊中釋放資源
- 盡量緩存經(jīng)常使用的對象
- 不使用的對象及時(shí)設(shè)置為null
- 盡量考慮使用靜態(tài)方法
- 盡量在合適的場合使用單例
- 盡量使用final修飾符
下面是關(guān)于類和方法優(yōu)化:
- 重復(fù)代碼的提取
- 冗長方法的分割
- 嵌套條件分支或者循環(huán)遞歸的優(yōu)化
- 提取類或繼承體系中的常量
- 提取繼承體系中重復(fù)的屬性與方法到父類
這里先簡單介紹這些比較常規(guī)的重構(gòu)思想和原則,方法,畢竟今天的主角是SPI,下面有請SPI登場!
SPI
什么是SPI?
SPI全稱Service Provider Interface,是Java提供的一套用來被第三方實(shí)現(xiàn)或者擴(kuò)展的API,它可以用來啟用框架擴(kuò)展和替換組件。
它是一種服務(wù)發(fā)現(xiàn)機(jī)制,它通過在ClassPath路徑下的META-INF/services文件夾查找文件,自動(dòng)加載文件里所定義的類。
這一機(jī)制為很多框架擴(kuò)展提供了可能,比如在Dubbo、JDBC中都使用到了SPI機(jī)制。
下面就是SPI的機(jī)制過程
SPI實(shí)際上是基于接口的編程+策略模式+配置文件組合實(shí)現(xiàn)的動(dòng)態(tài)加載機(jī)制。
系統(tǒng)設(shè)計(jì)的各個(gè)抽象,往往有很多不同的實(shí)現(xiàn)方案,在面向的對象的設(shè)計(jì)里,一般推薦模塊之間基于接口編程,模塊之間不對實(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中;
- 主程序通過java.util.ServiceLoder動(dòng)態(tài)裝載實(shí)現(xiàn)模塊,它通過掃描META-INF/services目錄下的配置文件找到實(shí)現(xiàn)類的全限定名,把類加載到JVM;
- SPI的實(shí)現(xiàn)類必須攜帶一個(gè)不帶參數(shù)的構(gòu)造方法;
SPI使用場景
概括地說,適用于:調(diào)用者根據(jù)實(shí)際使用需要,啟用、擴(kuò)展、或者替換框架的實(shí)現(xiàn)策略
以下是比較常見的例子:
數(shù)據(jù)庫驅(qū)動(dòng)加載接口實(shí)現(xiàn)類的加載 JDBC加載不同類型數(shù)據(jù)庫的驅(qū)動(dòng)
日志門面接口實(shí)現(xiàn)類加載 SLF4J加載不同提供商的日志實(shí)現(xiàn)類
Spring Spring中大量使用了SPI,比如:對servlet3.0規(guī)范對ServletContainerInitializer的實(shí)現(xiàn)、自動(dòng)類型轉(zhuǎn)換Type Conversion SPI(Converter SPI、Formatter SPI)等
Dubbo Dubbo中也大量使用SPI的方式實(shí)現(xiàn)框架的擴(kuò)展, 不過它對Java提供的原生SPI做了封裝,允許用戶擴(kuò)展實(shí)現(xiàn)Filter接口
SPI簡單例子
先定義接口類
- package com.test.spi.learn;
- import java.util.List;
- public interface Search {
- public List
searchDoc(String keyword); - }
文件搜索實(shí)現(xiàn)
- package com.test.spi.learn;
- import java.util.List;
- public class FileSearch implements Search{
- @Override
- public List
searchDoc(String keyword) { - System.out.println("文件搜索 "+keyword);
- return null;
- }
- }
數(shù)據(jù)庫搜索實(shí)現(xiàn)
- package com.test.spi.learn;
- import java.util.List;
- public class DBSearch implements Search{
- @Override
- public List
searchDoc(String keyword) { - System.out.println("數(shù)據(jù)庫搜索 "+keyword);
- return null;
- }
- }
接下來可以在resources下新建META-INF/services/目錄,然后新建接口全限定名的文件:com.test.spi.learn.Search
里面加上我們需要用到的實(shí)現(xiàn)類
- com.test.spi.learn.FileSearch
- com.test.spi.learn.DBSearch
然后寫一個(gè)測試方法
- package com.test.spi.learn;
- import java.util.Iterator;
- import java.util.ServiceLoader;
- public class TestCase {
- public static void main(String[] args) {
- ServiceLoader
s = ServiceLoader.load(Search.class); - Iterator
iterator = s.iterator(); - while (iterator.hasNext()) {
- Search search = iterator.next();
- search.searchDoc("hello world");
- }
- }
- }
可以看到輸出結(jié)果:
- 文件搜索 hello world
- 數(shù)據(jù)庫搜索 hello world
SPI原理解析
通過查看ServiceLoader的源碼,梳理了一下,實(shí)現(xiàn)的流程如下:
應(yīng)用程序調(diào)用ServiceLoader.load方法 ServiceLoader.load方法內(nèi)先創(chuàng)建一個(gè)新的ServiceLoader,并實(shí)例化該類中的成員變量,包括以下:
loader(ClassLoader類型,類加載器) acc(AccessControlContext類型,訪問控制器) providers(LinkedHashMap
應(yīng)用程序通過迭代器接口獲取對象實(shí)例 ServiceLoader先判斷成員變量providers對象中(LinkedHashMap
如果有緩存,直接返回。如果沒有緩存,執(zhí)行類的裝載,實(shí)現(xiàn)如下:
(1) 讀取META-INF/services/下的配置文件,獲得所有能被實(shí)例化的類的名稱,值得注意的是,ServiceLoader可以跨越j(luò)ar包獲取META-INF下的配置文件
(2) 通過反射方法Class.forName()加載類對象,并用instance()方法將類實(shí)例化。
(3) 把實(shí)例化后的類緩存到providers對象中,(LinkedHashMap
總結(jié)
優(yōu)點(diǎn)
使用SPI機(jī)制的優(yōu)勢是實(shí)現(xiàn)解耦,使得接口的定義與具體業(yè)務(wù)實(shí)現(xiàn)分離,而不是耦合在一起。應(yīng)用進(jìn)程可以根據(jù)實(shí)際業(yè)務(wù)情況啟用或替換具體組件。
缺點(diǎn)
不能按需加載。雖然ServiceLoader做了延遲載入,但是基本只能通過遍歷全部獲取,也就是接口的實(shí)現(xiàn)類得全部載入并實(shí)例化一遍。如果你并不想用某些實(shí)現(xiàn)類,或者某些類實(shí)例化很耗時(shí),它也被載入并實(shí)例化了,這就造成了浪費(fèi)。
獲取某個(gè)實(shí)現(xiàn)類的方式不夠靈活,只能通過 Iterator 形式獲取,不能根據(jù)某個(gè)參數(shù)來獲取對應(yīng)的實(shí)現(xiàn)類。
多個(gè)并發(fā)多線程使用 ServiceLoader 類的實(shí)例是不安全的。
加載不到實(shí)現(xiàn)類時(shí)拋出并不是真正原因的異常,錯(cuò)誤很難定位。
看到上面這么多的缺點(diǎn),你肯定會(huì)想,有這些弊端為什么還要使用呢,沒錯(cuò),在重構(gòu)的過程中,SPI接口化是一個(gè)非常有用的方式,當(dāng)你需要擴(kuò)展的時(shí)候,適配的時(shí)候,越早的使用你就會(huì)受利越早,在一個(gè)合適的時(shí)間,恰當(dāng)?shù)臋C(jī)會(huì)的時(shí)候,就鼓起勇氣,重構(gòu)吧!
好了。今天就說到這了,我還會(huì)不斷分享自己的所學(xué)所想,希望我們一起走在成功的道路上!
當(dāng)前文章:呦呦,這些代碼有點(diǎn)臭,重構(gòu)大法帶你秀(SPI接口化)
標(biāo)題來源:http://www.fisionsoft.com.cn/article/cdigseh.html


咨詢
建站咨詢
