美文网首页秦怀杂货店
JDBC(3)-- SPI技术以及数据库连接中的使用

JDBC(3)-- SPI技术以及数据库连接中的使用

作者: 秦怀杂货店 | 来源:发表于2020-11-21 16:44 被阅读0次

    1.SPI是什么?

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

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

    我们来写一个简单的例子:


    image

    整个项目结构:

    • 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

    image

    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/DemoCode/tree/main/SPI-Project,仅供参考

    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.Driver
    com.mysql.fabric.jdbc.FabricMySQLDriver
    

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

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

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

    【作者简介】
    秦怀,公众号【秦怀杂货店】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。这个世界希望一切都很快,更快,但是我希望自己能走好每一步,写好每一篇文章,期待和你们一起交流。

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

    相关文章

      网友评论

        本文标题:JDBC(3)-- SPI技术以及数据库连接中的使用

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