[TOC]

1.SPI是什么?

SPI,即是Service Provider Interface,是一种服务提供(接口实现)发现机制,能够通过ClassPath门路下的META-INF/Service文件查找文件,加载外面定义的类。
个别能够用来启用框架拓展和替换组件,比方在最常见的数据库连贯JDBC中,java.sql.Driver,不同的数据库产商能够对接口做不一样的实现,然而JDK怎么晓得他人有哪些实现呢?这就须要SPI,能够查找到接口的实现,对其进行操作。
用两个字解释:解耦

2.如何应用SPI来提供自定义服务?

咱们来写一个简略的例子:

整个我的项目构造:

  • SPI-Project:maven我的项目

    • DBInterface:maven我的项目,parent是SPI-Project,定义了一个接口com.aphysia.sqlserver.DBConnectionService,本人不做实现。
    • MysqlConnection:prarent是SPI-Project,实现了接口DBConnectionService,也就是MysqlConnectionServiceImpl
    • SqlServerConnection:prarent 也是SPI-Project,实现了DBConnectionService,也就是SqlServerConnectionServiceImpl
    • WebProject:测试项目,模仿web我的项目外面应用数据库驱动。

不论是MySqlConnection还是SqlServerConnection两个module中,都是去实现了DBInterface的接口,并且在resource/META-INF/services下都须要申明所实现的类,文件名就是实现的接口全限定名com.aphysia.sql.DBConnectionService,文件外面就是具体的实现类的全限定名,比方:com.aphysia.mysql.MysqlConnectionServiceImpl

SPI-Project的pom文件:

<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0"         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">    <modelVersion>4.0.0</modelVersion>    <groupId>com.aphysia</groupId>    <artifactId>SPI-Project</artifactId>    <packaging>pom</packaging>    <version>1.0-SNAPSHOT</version>    <modules>        <module>DbInterface</module>        <module>MySqlConection</module>        <module>SqlServerConnection</module>        <module>WebProject</module>    </modules></project>

2.1 DBInterface定义接口

DBInterface是SPIProject的一个module,次要是定义一个标准(接口),不做任何实现。
pom文件如下:

<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0"         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">    <parent>        <artifactId>SPI-Project</artifactId>        <groupId>com.aphysia</groupId>        <version>1.0-SNAPSHOT</version>    </parent>    <modelVersion>4.0.0</modelVersion>    <artifactId>DbInterface</artifactId></project>

定义的接口(模仿了java提供的数据库驱动的情景,定义了驱动标准):DBConnectionService.java

package com.aphysia.sql;public interface DBConnectionService {    void connect();}

2.2 模仿Mysql实现驱动

接口的第一种实现,相当于模仿第三方Mysql对接口做了本人的拓展:
pom文件:

<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0"         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">    <parent>        <artifactId>SPI-Project</artifactId>        <groupId>com.aphysia</groupId>        <version>1.0-SNAPSHOT</version>    </parent>    <modelVersion>4.0.0</modelVersion>    <artifactId>MySqlConection</artifactId>    <dependencies>        <dependency>            <groupId>com.aphysia</groupId>            <artifactId>DbInterface</artifactId>            <version>1.0-SNAPSHOT</version>        </dependency>    </dependencies></project>

实现了后面定义的接口:
MysqlConnectionServiceImpl

package com.aphysia.mysql;import com.aphysia.sqlserver.DBConnectionService;public class MysqlConnectionServiceImpl implements DBConnectionService {    public void connect() {        System.out.println("mysql 正在连接...");    }}

申明实现,在resource/META-INF.services/下定义一个文件,名为com.aphysia.sql.DBConnection,外面内容是:

com.aphysia.mysql.MysqlConnectionServiceImpl

2.3 模仿SqlServer实现驱动

SqlServerConnection也是一个module,pom文件如下:

<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0"         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">    <parent>        <artifactId>SPI-Project</artifactId>        <groupId>com.aphysia</groupId>        <version>1.0-SNAPSHOT</version>    </parent>    <modelVersion>4.0.0</modelVersion>    <artifactId>SqlServerConnection</artifactId>    <dependencies>        <dependency>            <groupId>com.aphysia</groupId>            <artifactId>DbInterface</artifactId>            <version>1.0-SNAPSHOT</version>        </dependency>    </dependencies></project>

接口的第二种实现,相当于第三方SqlServer对接口做了本人的拓展:SqlServerConnectionServiceImpl

package com.aphysia.sqlserver;public class SqlServerConnectionServiceImpl implements DBConnectionService {    public void connect() {        System.out.println("sqlServer 正在连接...");    }}

申明实现,在resource/META-INF.services/下定义一个文件,名为com.aphysia.sql.DBConnection,外面内容是:

com.aphysia.sqlserver.SqlServerConnectionServiceImpl

2.4 模仿用户应用不同驱动

下面两种不同的接口实现,留神须要在resource下申明,文件名是基类的全限定名,外面内容是具体实现类的全限定名

而咱们本人应用我的项目的时候呢?必定是须要哪一个驱动就引入哪一个驱动的jar包。

比方咱们在webProject中导入两种实现:MysqlConnectionSqlServerConnection:

    <dependencies>        <dependency>            <groupId>com.aphysia</groupId>            <artifactId>DbInterface</artifactId>            <version>1.0-SNAPSHOT</version>        </dependency>        <dependency>            <groupId>com.aphysia</groupId>            <artifactId>MySqlConection</artifactId>            <version>1.0-SNAPSHOT</version>        </dependency>        <dependency>            <groupId>com.aphysia</groupId>            <artifactId>SqlServerConnection</artifactId>            <version>1.0-SNAPSHOT</version>        </dependency>    </dependencies>

测试代码如下:

import com.aphysia.sql.DBConnectionService;import java.util.ServiceLoader;public class Test {    public static void main(String[] args) {        ServiceLoader<DBConnectionService>  serviceLoader= ServiceLoader.load(DBConnectionService.class);        for (DBConnectionService dbConnectionService : serviceLoader) {            dbConnectionService.connect();        }    }}

输入:

mysql 正在连接...sqlServer 正在连接...

如果咱们只在pom文件外面引入mysql的实现呢?答案很显著,只会输入上面一句:

mysql 正在连接...

也就是对于应用的人来说,不须要本人再做什么操作,只须要把包引入进来即可,简略易用。

具体残缺代码: https://github.com/Damaer/Dem...

3. ServiceLoader实现原理

ServiceLoader位于java.util包下,其次要代码如下:

public final class ServiceLoader<S>    implements Iterable<S>{    private static final String PREFIX = "META-INF/services/";    private final Class<S> service;    private final ClassLoader loader;    private final AccessControlContext acc;    // Cached providers, in instantiation order    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();    // The current lazy-lookup iterator    private LazyIterator lookupIterator;    public void reload() {        providers.clear();        lookupIterator = new LazyIterator(service, loader);    }    private ServiceLoader(Class<S> svc, ClassLoader cl) {        service = Objects.requireNonNull(svc, "Service interface cannot be null");        loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;        acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;        reload();    }    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 {                    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 {                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();        }    }    public Iterator<S> iterator() {        return new Iterator<S>() {            Iterator<Map.Entry<String,S>> knownProviders                = providers.entrySet().iterator();            public boolean hasNext() {                if (knownProviders.hasNext())                    return true;                return lookupIterator.hasNext();            }            public S next() {                if (knownProviders.hasNext())                    return knownProviders.next().getValue();                return lookupIterator.next();            }            public void remove() {                throw new UnsupportedOperationException();            }        };    }    public static <S> ServiceLoader<S> load(Class<S> service) {        ClassLoader cl = Thread.currentThread().getContextClassLoader();        return ServiceLoader.load(service, cl);    }    public static <S> ServiceLoader<S> loadInstalled(Class<S> service) {        ClassLoader cl = ClassLoader.getSystemClassLoader();        ClassLoader prev = null;        while (cl != null) {            prev = cl;            cl = cl.getParent();        }        return ServiceLoader.load(service, prev);    }    public String toString() {        return "java.util.ServiceLoader[" + service.getName() + "]";    }}

咱们调用ServiceLoader.load()获取接口的实现,实际上也是调用了 ServiceLoader(Class<S> svc, ClassLoader cl),外面都是调用reload()reload()外面做了些什么操作呢?

先把provider清空,而后创立了LazyIterator对象,LazyIterator是一个外部类,实现了Iterator接口,实际上就是一个懒加载的迭代器。什么时候加载呢?
在迭代器调用的时候,调用hasNextService(),去解析resource/META-INF/services上面的实现,并实现实现类的实例化。这里的实例化是应用反射,也是通过全限定类名。class.forName()

解析的时候,每一行代表一个实现类,将曾经发现的接口进行缓存,放到private LinkedHashMap<String,S> providers中,同时对外提供遍历迭代的办法。

4. SPI的利用

咱们在应用mysql驱动的时候,在mysql-connector-java-version.jar中,有一个文件是Resource/service/java.sql.Driver文件,外面记录的是:

com.mysql.jdbc.Drivercom.mysql.fabric.jdbc.FabricMySQLDriver

也就是申明了java.sql.Driver的实现类是com.mysql.jdbc.Driver,不须要手动应用Class.forName()手动加载。

同样的,slf4j也是一样的机制去实现拓展性能。

这种思维,通过服务约定-->服务实现-->服务主动注册-->服务发现和应用,实现了提供者和应用方的解耦,真的很强...

此文章仅代表本人(本菜鸟)学习积攒记录,或者学习笔记,如有侵权,请分割作者删除。人无完人,文章也一样,文笔稚嫩,在下不才,勿喷,如果有谬误之处,还望指出,感激不尽~

技术之路不在一时,山高水长,纵使迟缓,驰而不息。
公众号:秦怀杂货店