美文网首页
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机制

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