zoukankan      html  css  js  c++  java
  • java基础:SPI机制

    简介

    SPI(Service Provider Interface),是JDK内置的一种服务提供发现机制,可以用来启用框架扩展和替换组件,主要是被框架的开发人员使用,比如java.sql.Driver接口,其他不同厂商可以针对同一接口做出不同的实现,MySQL和PostgreSQL都有不同的实现提供给用户,而Java的SPI机制可以为某个接口寻找服务实现。Java中SPI机制主要思想是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要,其核心思想就是解耦

    SPI整体机制图如下:

    image-20211208152124395

    当服务的提供者提供了一种接口的实现之后,需要在classpath下的META-INF/services/目录里创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体的实现类。当其他的程序需要这个服务的时候,就可以通过查找这个jar包(一般都是以jar包做依赖)的META-INF/services/中的配置文件,配置文件中有接口的具体实现类名,可以根据这个类名进行加载实例化,就可以使用该服务了。JDK中查找服务的实现的工具类是:java.util.ServiceLoader

    基本使用

    创建如下结构的maven项目:

    image-20211208154031656

    其中app子项目maven:

        <dependencies>
            <dependency>
                <groupId>com.wj</groupId>
                <artifactId>interface</artifactId>
                <version>1.0-SNAPSHOT</version>
            </dependency>
    
            <dependency>
                <groupId>com.wj</groupId>
                <artifactId>mysql-impl</artifactId>
                <version>1.0-SNAPSHOT</version>
            </dependency>
    
            <dependency>
                <groupId>com.wj</groupId>
                <artifactId>redis-impl</artifactId>
                <version>1.0-SNAPSHOT</version>
            </dependency>
        </dependencies>
    

    我在interface项目中声明了一个接口com.wj.ISaveService

    public interface ISaveService {
        void save();
    }
    

    mysql-impl项目和redis-impl项目对ISaveService接口实现如下:

    public class MysqlSaveService implements ISaveService {
        @Override
        public void save() {
            System.out.println("mysql save...");
        }
    }
    public class RedisSaveService implements ISaveService {
        @Override
        public void save() {
            System.out.println("redis save...");
        }
    }
    

    mysql-impl的resource目录下新建META-INF/services/包,创建名称为com.wj.ISaveService的文件与interface中接口全类名相同,文件内容如下(为该接口的实现类的全类名相同):

    com.wj.impl.MysqlSaveService
    

    redis-impl与mysql-impl类似,文件内容如下:

    com.wj.impl.RedisSaveService
    

    app项目的main方法:

    public class Main {
    
        public static void main(String[] args) {
            //使用ServiceLoader加载所有的ISaveService接口
            ServiceLoader<ISaveService> serviceLoader = ServiceLoader.load(ISaveService.class);
    
            for (ISaveService iSaveService : serviceLoader) {
                iSaveService.save();
            }
        }
    }
    

    运行结果:

    image-20211208154620733

    应用场景

    SPI扩展机制应用场景有很多,比如Common-Logging,JDBC,Dubbo等等。

    比如JDBC场景下:

    • 首先在Java中定义了接口java.sql.Driver,并没有具体的实现,具体的实现都是由不同厂商提供。

    在MySQL的jar包mysql-connector-java-6.0.6.jar中,可以找到META-INF/services目录,该目录下会有一个名字为java.sql.Driver的文件,文件内容是com.mysql.cj.jdbc.Driver,这里面的内容就是针对Java中定义的接口的实现。

    同样在PostgreSQL的jar包PostgreSQL-42.0.0.jar中,也可以找到同样的配置文件,文件内容是org.postgresql.Driver,这是PostgreSQL对Java的java.sql.Driver的实现。

    原理

    如果想要打断点调试,可以修改一下main方法:

            ServiceLoader<ISaveService> serviceLoader = ServiceLoader.load(ISaveService.class);
            Iterator<ISaveService> iterator = serviceLoader.iterator();
            while (iterator.hasNext()) {
                ISaveService next = iterator.next();
                next.save();
            }
    

    源码不算复杂,当调用iterator的hasNext方法时,内部使用的LazyIterator会去找到对应的文件。

    可以看:

    private class LazyIterator
            implements Iterator<S>
        {
    
            Class<S> service;
            ClassLoader loader;
            Enumeration<URL> configs = null;
            Iterator<String> pending = null;
            String nextName = null;
    
            private LazyIterator(Class<S> service, ClassLoader loader) {
                this.service = service;
                this.loader = loader;
            }
    
            private boolean hasNextService() {
                if (nextName != null) {
                    return true;
                }
                if (configs == null) {
                    try {
                        //private static final String PREFIX = "META-INF/services/";
                        String fullName = PREFIX + service.getName();
                        if (loader == null)
                            configs = ClassLoader.getSystemResources(fullName);
                        else
                            configs = loader.getResources(fullName);
                    } catch (IOException x) {
                        fail(service, "Error locating configuration files", x);
                    }
                }
                while ((pending == null) || !pending.hasNext()) {
                    if (!configs.hasMoreElements()) {
                        return false;
                    }
                    pending = parse(service, configs.nextElement());
                }
                nextName = pending.next();
                return true;
            }
    
            private S nextService() {
                if (!hasNextService())
                    throw new NoSuchElementException();
                String cn = nextName;
                nextName = null;
                Class<?> c = null;
                try {
                    //获取Class
                    c = Class.forName(cn, false, loader);
                } catch (ClassNotFoundException x) {
                    fail(service,
                         "Provider " + cn + " not found");
                }
                if (!service.isAssignableFrom(c)) {
                    fail(service,
                         "Provider " + cn  + " not a subtype");
                }
                try {
                    //实例化对象
                    S p = service.cast(c.newInstance());
                    providers.put(cn, p);
                    return p;
                } catch (Throwable x) {
                    fail(service,
                         "Provider " + cn + " could not be instantiated",
                         x);
                }
                throw new Error();          // This cannot happen
            }
    
            public boolean hasNext() {
                if (acc == null) {
                    return hasNextService();
                } else {
                    PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
                        public Boolean run() { return hasNextService(); }
                    };
                    return AccessController.doPrivileged(action, acc);
                }
            }
    
            public S next() {
                if (acc == null) {
                    return nextService();
                } else {
                    PrivilegedAction<S> action = new PrivilegedAction<S>() {
                        public S run() { return nextService(); }
                    };
                    return AccessController.doPrivileged(action, acc);
                }
            }
    
            public void remove() {
                throw new UnsupportedOperationException();
            }
    
        }
    

    缺点

    1.不能按需加载,需要遍历所有的实现,并实例化,然后在循环中才能找到我们需要的实现。如果不想用某些实现类,或者某些类实例化很耗时,它也被载入并实例化了,这就造成了浪费。

    2.获取某个实现类的方式不够灵活,只能通过 Iterator 形式获取,不能根据某个参数来获取对应的实现类。

    3.多个并发多线程使用 ServiceLoader 类的实例是不安全的。

    参考:https://zhuanlan.zhihu.com/p/84337883

  • 相关阅读:
    WCF添加服务失败。服务元数据可能无法访问。请确保服务正在运行并且正在公开元数据。
    【C#】 实现WinForm中只能启动一个实例
    centos7防火墙问题
    ftp搭建记录
    centos7常用命令
    RocketMQ部署
    mongedb主从
    redis 主从复制+读写分离+哨兵
    keepalive+nginx
    分布架构分析
  • 原文地址:https://www.cnblogs.com/wwjj4811/p/15662229.html
Copyright © 2011-2022 走看看