java-spi機制

起因

在看SpringMVC官方文檔中,有這么一個類WebApplicationInitializer熊咽,通過這個類可以代替web.xml文件直接配置儒将,而且文檔中說這個類由Servelt容器自動檢測調(diào)用卿闹。原文如下:

The following example of the Java configuration registers and initializes the DispatcherServlet, which is auto-detected by the Servlet container (see Servlet Config)

例如下面的web.xml如下:

<web-app>
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/app-context.xml</param-value>
    </context-param>
    <servlet>
        <servlet-name>app</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value></param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>app</servlet-name>
        <url-pattern>/app/*</url-pattern>
    </servlet-mapping>
</web-app>

而它替換成代碼如下:

public class MyWebApplicationInitializer implements WebApplicationInitializer {
    @Override
    public void onStartup(ServletContext servletContext) {
        // Load Spring web application configuration
        AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
        context.register(AppConfig.class);
        // Create and register the DispatcherServlet
        DispatcherServlet servlet = new DispatcherServlet(context);
        ServletRegistration.Dynamic registration = servletContext.addServlet("app", servlet);
        registration.setLoadOnStartup(1);
        registration.addMapping("/app/*");
    }
}

然后我進(jìn)入WebApplicationInitializer的源碼中,通過文檔中的描述發(fā)現(xiàn)了一個關(guān)鍵的東西SPI竞端。然后我立馬上網(wǎng)查了資料屎即,大概是了解了為什么可以使用WebApplicationInitializer代替web.xml的配置了。

什么是SPI

單純的解釋概念太干澀事富,我們先從需求說起技俐。在面向?qū)ο笤O(shè)計中,我們一般推薦模塊之間基于接口來編程统台,如果直接使用實現(xiàn)類來編程雕擂,在代碼實現(xiàn)改變時少不了的要修改代碼,這使得代碼耦合性太高了贱勃。最好是能提供一種可插拔的機制井赌,能讓我們在不改代碼的情況下替換實現(xiàn)。在我們熟知的Spring中就有這種機制贵扰,而java中同樣提供了這種機制仇穗,而這種機制就叫SPI。SPI通過將服務(wù)接口和服務(wù)實現(xiàn)分開大大提高了程序的擴(kuò)展性戚绕,而這種機制在很多地方都有使用過纹坐。

spi應(yīng)用實例

例如我現(xiàn)在定義一個UserService接口,而我現(xiàn)在還沒有想好如何實現(xiàn)列肢,接口定義如下:

package com.buydeem.share.service;
public interface UserService {
    String getUserName();
}

為了便于立即恰画,我接口定義的很簡單,只有一個方法獲取用戶名稱〈陕恚現(xiàn)在我想到一種實現(xiàn)拴还,就是從數(shù)據(jù)庫中獲取用戶名稱,它的實現(xiàn)如下:

package com.buydeem.share.service.impl;
import com.buydeem.share.service.UserService;
/**
 * 基于Mysql的實現(xiàn)
 */
public class MySqlUserService implements UserService {
    @Override
    public String getUserName() {
        return "我是DbUserService中的用戶";
    }
}

那我在程序中該如何獲取到這個實現(xiàn)呢欧聘?我前面說過直接硬編碼的方式不太適合片林,雖然這種方式可以實現(xiàn)。我這里就通過SPI來完成怀骤。
首先在resources下創(chuàng)建一個文件夾META-INF/services/费封,然后在該文件夾下創(chuàng)建文件com.buydeem.share.service.UserService,文件名就是我定義的接口的全類名蒋伦。該文件的內(nèi)容如下:

com.buydeem.share.service.impl.MySqlUserService

里面的內(nèi)容就是MySqlUserService實現(xiàn)類的全名弓摘。
而我的程序調(diào)用代碼如下:

public class App {
    public static void main(String[] args) {
        ServiceLoader<UserService> userServices = ServiceLoader.load(UserService.class);
        Iterator<UserService> it = userServices.iterator();
        while (it.hasNext()){
            UserService userService = it.next();
            System.out.printf("用戶信息:%s,實現(xiàn)類:%s\n",userService.getUserName(),userService.getClass().getName());
        }
    }
}

最后的運行結(jié)果如下:

用戶信息:我是DbUserService中的用戶,實現(xiàn)類:com.buydeem.share.service.impl.MySqlUserService

現(xiàn)在我想成從Redis中獲取用戶信息實現(xiàn)了如下:

package com.buydeem.share.service.impl;
import com.buydeem.share.service.UserService;
/**
 * 基于Redis的實現(xiàn)
 */
public class RedisUserService implements UserService {
    @Override
    public String getUserName() {
        return "我是RedisUserService中的用戶";
    }
}

換了實現(xiàn)我不需要修改代碼,只需要將com.buydeem.share.service.UserService文件中的內(nèi)容改成如下即可痕届。

com.buydeem.share.service.impl.RedisUserService

再次運行程序執(zhí)行結(jié)果如下:

用戶信息:我是RedisUserService中的用戶,實現(xiàn)類:com.buydeem.share.service.impl.RedisUserService

通過SPI機制我們很容易的就實現(xiàn)了接口和接口的解耦韧献。


工程目錄.png

上圖就是我工程的目錄結(jié)構(gòu),上面只是我們的示例項目研叫,如果在我們的工作中锤窑,我們完全可以將接口定義單獨打成包,而我可以將實現(xiàn)單獨打成包(實現(xiàn)包依賴接口包)嚷炉,通過SPI機制我們就可以實現(xiàn)工程依賴哪個實現(xiàn)包就用哪個實現(xiàn)渊啰。如果需要替換實現(xiàn)只用簡單的替換實現(xiàn)包即可,達(dá)到了完全的解耦合申屹。

Servlet3.0中的SPI機制

回到我們之前的疑問绘证,在Servlet3.0中提供了代碼配置wen.xml的功能,而這個功能就是通過SPI機制實現(xiàn)的哗讥。

public interface ServletContainerInitializer {
    public void onStartup(Set<Class<?>> c, ServletContext ctx)
        throws ServletException; 
}

這個就是Servlet3.0提供的接口迈窟,而這個接口在SpringMVC中的實現(xiàn)就是SpringServletContainerInitializer。該類的實現(xiàn)如下:

public class SpringServletContainerInitializer implements ServletContainerInitializer {
    @Override
    public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
            throws ServletException {
        List<WebApplicationInitializer> initializers = Collections.emptyList();
        if (webAppInitializerClasses != null) {
            initializers = new ArrayList<>(webAppInitializerClasses.size());
            for (Class<?> waiClass : webAppInitializerClasses) {
                // Be defensive: Some servlet containers provide us with invalid classes,
                // no matter what @HandlesTypes says...
                if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
                        WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
                    try {
                        initializers.add((WebApplicationInitializer)
                                ReflectionUtils.accessibleConstructor(waiClass).newInstance());
                    }
                    catch (Throwable ex) {
                        throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
                    }
                }
            }
        }
        if (initializers.isEmpty()) {
            servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
            return;
        }
        servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
        AnnotationAwareOrderComparator.sort(initializers);
        for (WebApplicationInitializer initializer : initializers) {
            initializer.onStartup(servletContext);
        }
    }
}

而在spring-web的包中的META-INF/services/文件夾下有一個文件忌栅,名字就是javax.servlet.ServletContainerInitializer车酣,而文件里面的內(nèi)容就是:

org.springframework.web.SpringServletContainerInitializer
spring-web中SPI.png

SpringServletContainerInitializer該類中會篩選出傳遞進(jìn)來的webAppInitializerClasses集合中不是接口、抽象類且是WebApplicationInitializer實現(xiàn)類的Class索绪,然后將其實例化放入到initializers集合中湖员,然后循環(huán)調(diào)用它的onStartup方法。

上面就是SpringMVC中通過SPI機制實現(xiàn)WebApplicationInitializer代替web.xml配置的過程瑞驱。

總結(jié)

使用SPI機制的優(yōu)勢就是接口與實現(xiàn)的解耦娘摔,但是它也有部分限制。通過ServiceLoader延遲加載實現(xiàn)算是實現(xiàn)了延遲加載唤反,但是接口的實現(xiàn)的實例化只能通過無參函數(shù)構(gòu)建凳寺。而對于存在多種實現(xiàn)時鸭津,我們只能全部遍歷一遍所有實現(xiàn)造成了資源的浪費,并且想要獲取指定的實現(xiàn)也不太靈活肠缨。

示例代碼地址:spi示例代碼

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末逆趋,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子晒奕,更是在濱河造成了極大的恐慌闻书,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,544評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件脑慧,死亡現(xiàn)場離奇詭異魄眉,居然都是意外死亡,警方通過查閱死者的電腦和手機闷袒,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評論 3 392
  • 文/潘曉璐 我一進(jìn)店門坑律,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人囊骤,你說我怎么就攤上這事脾歇。” “怎么了淘捡?”我有些...
    開封第一講書人閱讀 162,764評論 0 353
  • 文/不壞的土叔 我叫張陵藕各,是天一觀的道長。 經(jīng)常有香客問我焦除,道長激况,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,193評論 1 292
  • 正文 為了忘掉前任膘魄,我火速辦了婚禮乌逐,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘创葡。我一直安慰自己浙踢,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,216評論 6 388
  • 文/花漫 我一把揭開白布灿渴。 她就那樣靜靜地躺著洛波,像睡著了一般。 火紅的嫁衣襯著肌膚如雪骚露。 梳的紋絲不亂的頭發(fā)上蹬挤,一...
    開封第一講書人閱讀 51,182評論 1 299
  • 那天,我揣著相機與錄音棘幸,去河邊找鬼焰扳。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的吨悍。 我是一名探鬼主播扫茅,決...
    沈念sama閱讀 40,063評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼育瓜!你這毒婦竟也來了葫隙?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,917評論 0 274
  • 序言:老撾萬榮一對情侶失蹤爆雹,失蹤者是張志新(化名)和其女友劉穎停蕉,沒想到半個月后愕鼓,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體钙态,經(jīng)...
    沈念sama閱讀 45,329評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,543評論 2 332
  • 正文 我和宋清朗相戀三年菇晃,在試婚紗的時候發(fā)現(xiàn)自己被綠了册倒。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,722評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡磺送,死狀恐怖驻子,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情估灿,我是刑警寧澤崇呵,帶...
    沈念sama閱讀 35,425評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站馅袁,受9級特大地震影響域慷,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜汗销,卻給世界環(huán)境...
    茶點故事閱讀 41,019評論 3 326
  • 文/蒙蒙 一犹褒、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧弛针,春花似錦叠骑、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,671評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至茧跋,卻和暖如春朦拖,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背厌衔。 一陣腳步聲響...
    開封第一講書人閱讀 32,825評論 1 269
  • 我被黑心中介騙來泰國打工璧帝, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人富寿。 一個月前我還...
    沈念sama閱讀 47,729評論 2 368
  • 正文 我出身青樓睬隶,卻偏偏與公主長得像锣夹,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子苏潜,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,614評論 2 353

推薦閱讀更多精彩內(nèi)容