起因
在看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)了接口和接口的解耦韧献。
上圖就是我工程的目錄結(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
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示例代碼