起因
在看SpringMVC官方文档中,有这么一个类WebApplicationInitializer,通过这个类可以代替web.xml文件直接配置,而且文档中说这个类由Servelt容器自动检测调用。原文如下:
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/*");
}
}
然后我进入WebApplicationInitializer的源码中,通过文档中的描述发现了一个关键的东西SPI。然后我立马上网查了资料,大概是了解了为什么可以使用WebApplicationInitializer代替web.xml的配置了。
什么是SPI
单纯的解释概念太干涩,我们先从需求说起。在面向对象设计中,我们一般推荐模块之间基于接口来编程,如果直接使用实现类来编程,在代码实现改变时少不了的要修改代码,这使得代码耦合性太高了。最好是能提供一种可插拔的机制,能让我们在不改代码的情况下替换实现。在我们熟知的Spring中就有这种机制,而java中同样提供了这种机制,而这种机制就叫SPI。SPI通过将服务接口和服务实现分开大大提高了程序的扩展性,而这种机制在很多地方都有使用过。
spi应用实例
例如我现在定义一个UserService接口,而我现在还没有想好如何实现,接口定义如下:
package com.buydeem.share.service;
public interface UserService {
String getUserName();
}
为了便于立即,我接口定义的很简单,只有一个方法获取用户名称。现在我想到一种实现,就是从数据库中获取用户名称,它的实现如下:
package com.buydeem.share.service.impl;
import com.buydeem.share.service.UserService;
/**
* 基于Mysql的实现
*/
public class MySqlUserService implements UserService {
@Override
public String getUserName() {
return "我是DbUserService中的用户";
}
}
那我在程序中该如何获取到这个实现呢?我前面说过直接硬编码的方式不太适合,虽然这种方式可以实现。我这里就通过SPI来完成。
首先在resources下创建一个文件夹META-INF/services/,然后在该文件夹下创建文件com.buydeem.share.service.UserService,文件名就是我定义的接口的全类名。该文件的内容如下:
com.buydeem.share.service.impl.MySqlUserService
里面的内容就是MySqlUserService实现类的全名。
而我的程序调用代码如下:
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,实现类:%s\n",userService.getUserName(),userService.getClass().getName());
}
}
}
最后的运行结果如下:
用户信息:我是DbUserService中的用户,实现类:com.buydeem.share.service.impl.MySqlUserService
现在我想成从Redis中获取用户信息实现了如下:
package com.buydeem.share.service.impl;
import com.buydeem.share.service.UserService;
/**
* 基于Redis的实现
*/
public class RedisUserService implements UserService {
@Override
public String getUserName() {
return "我是RedisUserService中的用户";
}
}
换了实现我不需要修改代码,只需要将com.buydeem.share.service.UserService文件中的内容改成如下即可。
com.buydeem.share.service.impl.RedisUserService
再次运行程序执行结果如下:
用户信息:我是RedisUserService中的用户,实现类:com.buydeem.share.service.impl.RedisUserService
通过SPI机制我们很容易的就实现了接口和接口的解耦。
上图就是我工程的目录结构,上面只是我们的示例项目,如果在我们的工作中,我们完全可以将接口定义单独打成包,而我可以将实现单独打成包(实现包依赖接口包),通过SPI机制我们就可以实现工程依赖哪个实现包就用哪个实现。如果需要替换实现只用简单的替换实现包即可,达到了完全的解耦合。
Servlet3.0中的SPI机制
回到我们之前的疑问,在Servlet3.0中提供了代码配置wen.xml的功能,而这个功能就是通过SPI机制实现的。
public interface ServletContainerInitializer {
public void onStartup(Set<Class<?>> c, ServletContext ctx)
throws ServletException;
}
这个就是Servlet3.0提供的接口,而这个接口在SpringMVC中的实现就是SpringServletContainerInitializer。该类的实现如下:
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,而文件里面的内容就是:
org.springframework.web.SpringServletContainerInitializer
SpringServletContainerInitializer该类中会筛选出传递进来的webAppInitializerClasses集合中不是接口、抽象类且是WebApplicationInitializer实现类的Class,然后将其实例化放入到initializers集合中,然后循环调用它的onStartup方法。
上面就是SpringMVC中通过SPI机制实现WebApplicationInitializer代替web.xml配置的过程。
总结
使用SPI机制的优势就是接口与实现的解耦,但是它也有部分限制。通过ServiceLoader延迟加载实现算是实现了延迟加载,但是接口的实现的实例化只能通过无参函数构建。而对于存在多种实现时,我们只能全部遍历一遍所有实现造成了资源的浪费,并且想要获取指定的实现也不太灵活。
示例代码地址:spi示例代码