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

  • 相关阅读:
    AS将一个项目导入到另一个项目中
    Android Studio出现:Cause: unable to find valid certification path to requested target
    小米手机Toast带app名称
    PopupWindow 点击外部区域无法关闭的问题
    EditText inputType类型整理
    Fragment通过接口回调向父Activity传值
    Android selector一些坑
    Installation failed with message Failed to commit install session 634765663 with command cmd package
    旷视上海研究院机器人方向招聘
    语义SLAM的数据关联和语义定位(四)多目标测量概率模型
  • 原文地址:https://www.cnblogs.com/wwjj4811/p/15662229.html
Copyright © 2011-2022 走看看