java-spi机制

起因

在看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机制我们很容易的就实现了接口和接口的解耦。


工程目录.png

上图就是我工程的目录结构,上面只是我们的示例项目,如果在我们的工作中,我们完全可以将接口定义单独打成包,而我可以将实现单独打成包(实现包依赖接口包),通过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
spring-web中SPI.png

SpringServletContainerInitializer该类中会筛选出传递进来的webAppInitializerClasses集合中不是接口、抽象类且是WebApplicationInitializer实现类的Class,然后将其实例化放入到initializers集合中,然后循环调用它的onStartup方法。

上面就是SpringMVC中通过SPI机制实现WebApplicationInitializer代替web.xml配置的过程。

总结

使用SPI机制的优势就是接口与实现的解耦,但是它也有部分限制。通过ServiceLoader延迟加载实现算是实现了延迟加载,但是接口的实现的实例化只能通过无参函数构建。而对于存在多种实现时,我们只能全部遍历一遍所有实现造成了资源的浪费,并且想要获取指定的实现也不太灵活。

示例代码地址:spi示例代码

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容