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如下:


    
        org.springframework.web.context.ContextLoaderListener
    
    
        contextConfigLocation
        /WEB-INF/app-context.xml
    
    
        app
        org.springframework.web.servlet.DispatcherServlet
        
            contextConfigLocation
            
        
        1
    
    
        app
        /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 userServices = ServiceLoader.load(UserService.class);
        Iterator 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机制我们很容易的就实现了接口和接口的解耦。


java-spi机制_第1张图片
工程目录.png

上图就是我工程的目录结构,上面只是我们的示例项目,如果在我们的工作中,我们完全可以将接口定义单独打成包,而我可以将实现单独打成包(实现包依赖接口包),通过SPI机制我们就可以实现工程依赖哪个实现包就用哪个实现。如果需要替换实现只用简单的替换实现包即可,达到了完全的解耦合。

Servlet3.0中的SPI机制

回到我们之前的疑问,在Servlet3.0中提供了代码配置wen.xml的功能,而这个功能就是通过SPI机制实现的。

public interface ServletContainerInitializer {
    public void onStartup(Set> c, ServletContext ctx)
        throws ServletException; 
}

这个就是Servlet3.0提供的接口,而这个接口在SpringMVC中的实现就是SpringServletContainerInitializer。该类的实现如下:

public class SpringServletContainerInitializer implements ServletContainerInitializer {
    @Override
    public void onStartup(@Nullable Set> webAppInitializerClasses, ServletContext servletContext)
            throws ServletException {
        List 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
java-spi机制_第2张图片
spring-web中SPI.png

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

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

总结

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

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

你可能感兴趣的:(java-spi机制)