美文网首页
java-spi机制

java-spi机制

作者: 一个菜鸟JAVA | 来源:发表于2021-03-31 23:02 被阅读0次

起因

在看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示例代码

相关文章

  • Java-SPI机制

    Java--SPI机制 SPI全称为Service Provider Interface,是JDK内置的一种服务提...

  • java-spi机制

    java spi的具体约定如下 : 当服务的提供者,提供了服务接口的一种实现之后,在jar包的META-INF/s...

  • java-spi机制

    起因 在看SpringMVC官方文档中,有这么一个类WebApplicationInitializer,通过这个类...

  • java-spi开发

    SPI(Service Provide Interface)即服务提供接口规范,其功能是为了实现可插拔的服务提供,...

  • JAVA-SPI(dubbo的基石)实现原理

    1:SPI---Service Provider Interface : 服务提供接口,结构图如下: 2:本质:J...

  • 摘要

    监狱安全管理的机制,由领导责任机制,安全防控机制,隐患排除机制,应急处置机制,狱情搜集机制组成。领导责任机制,坚持...

  • Android开发者需掌握的进阶技能

    1:熟悉各大Android 机制(handler机制,事件分发机制,binder机制...) 2:熟悉 view ...

  • RunTime

    前言 RunTime简称运行时机制,其实OC就是一种运行时机制(消息机制是运行时机制中最重要的机制) 消息机制:任...

  • android handler 机制 很简单

    1.android handler机制 handler机制,也就是android的异步消息处理机制。但是这个机制不...

  • python内存管理机制

    Python内存管理机制 Python内存管理机制主要包括以下三个方面: 引用计数机制 垃圾回收机制 内存池机制 ...

网友评论

      本文标题:java-spi机制

      本文链接:https://www.haomeiwen.com/subject/mghzhltx.html