• 使用SPI解耦你的实现类


    什么是SPI机制

    最近我建了另一个文章分类,用于扩展JDK中一些重要但不常用的功能。

    SPI,全名Service Provider Interface,是一种服务发现机制。它可以看成是一种针对接口实现类的解耦方案。我们只需要采用配置文件方式配置好接口的实现类,就可以利用SPI机制去加载到它们了,当我们需要修改实现类时,改改配置文件就可以了,而不需要去改代码。

    当然,有的同学可能会问,spring也可以做接口实现类的解耦,是不是SPI就没用了呢?虽然两者都可以达到相同的目的,但是不一定所有应用都可以引入spring框架,例如JDBC自动发现驱动并注册,它就是采用SPI机制,它就不大可能引入spring来解耦接口实现类。另外,druiddubbo等都采用了SPI机制。

    怎么使用SPI

    需求

    利用SPI机制加载用户服务接口的实现类并测试。

    工程环境

    JDK:1.8.0_201

    maven:3.6.1

    IDE:eclipse 4.12

    主要步骤

    1. 编写用户服务类接口和实现类;
    2. classpath路径下的META-INF/services文件夹下配置好接口的实现类;
    3. 利用SPI机制加载接口实现类并测试。

    创建项目

    项目类型Maven Project,打包方式jar

    引入依赖

            <dependency>
                <groupId>junit</groupId>
                <artifactId>junit</artifactId>
                <version>4.12</version>
                <scope>test</scope>
            </dependency>
    

    编写用户服务类接口

    路径:cn.zzs.spi

    public interface UserService {
    	void save();
    }
    

    编写接口实现类

    路径:cn.zzs.spi。这里就简单实现就好了。

    public class UserServiceImpl1 implements UserService {
    
    	@Override
    	public void save() {
    		System.err.println("执行服务1的save方法");
    	}
    }
    // ------------------------
    public class UserServiceImpl2 implements UserService {
    
    	@Override
    	public void save() {
    		System.err.println("执行服务2的save方法");
    	}
    }
    

    配置接口文件

    resources路径下创建META-INF/services文件夹,并以UserService的全限定类名为文件名,创建一个文件。如图所示。

    UserService接口实现类配置文件

    文件中写入接口实现类的全限定类名,多个用换行符隔开。

    cn.zzs.spi.UserServiceImpl1
    cn.zzs.spi.UserServiceImpl2
    

    编写测试方法

    路径:test下的cn.zzs.spi。如果实际项目中配置了比较多的接口文件,可以考虑抽取工具类。

    public class UserServiceTest {
    
    	@Test
    	public void test() {
            // 1. 创建一个ServiceLoader对象
            ServiceLoader<UserService> userServiceLoader = ServiceLoader.load(UserService.class);
            // 2. 创建一个迭代器
            Iterator<UserService> userServiceIterator = userServiceLoader.iterator();
            // 3. 加载配置文件并实例化接口实现类
            while(userServiceIterator.hasNext()) {
            	UserService userService = userServiceIterator.next();
            	userService.save();
            	System.out.println("==================");
            }
    	}
    }
    

    测试结果

    执行服务1的save方法
    ==================
    执行服务2的save方法
    ==================
    

    SPI在JDBC中的应用

    本文以mysql 8.0.15版本的驱动来说明。首先,当我们调用Class.forName("com.mysql.cj.jdbc.Driver")时,会去执行这个类的静态代码块,在静态代码块中就会完成驱动注册。

        static {
            try {
                //静态代码块中注册当前驱动
                java.sql.DriverManager.registerDriver(new Driver());
            } catch (SQLException E) {
                throw new RuntimeException("Can't register driver!");
            }
        }
    

    JDK6后不再需要Class.forName(driver)也能注册驱动。因为从JDK6开始,DriverManager增加了以下静态代码块,当类被加载时会执行static代码块的loadInitialDrivers方法。

    而这个方法会通过查询系统参数(jdbc.drivers)和SPI机制两种方式去加载数据库驱动。

    注意:考虑篇幅,以下代码经过修改,仅保留所需部分。

        static {
            loadInitialDrivers();
        }
        //这个方法通过两个渠道加载所有数据库驱动:
        //1. 查询系统参数jdbc.drivers获得数据驱动类名
        //2. SPI机制
        private static void loadInitialDrivers() {
            //通过系统参数jdbc.drivers读取数据库驱动的全路径名。该参数可以通过启动参数来设置,其实引入SPI机制后这一步好像没什么意义了。
            String drivers;
            try {
                drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
                    public String run() {
                        return System.getProperty("jdbc.drivers");
                    }
                });
            } catch (Exception ex) {
                drivers = null;
            }
            //使用SPI机制加载驱动
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                public Void run() {
                    //读取META-INF/services/java.sql.Driver文件的类全路径名。
                    ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                    Iterator<Driver> driversIterator = loadedDrivers.iterator();
                    //加载并初始化类
                    try{
                        while(driversIterator.hasNext()) {
                        	// 这里才会去实例化驱动
                            driversIterator.next();
                        }
                    } catch(Throwable t) {
                    // Do nothing
                    }
                    return null;
                }
            });
    
            if (drivers == null || drivers.equals("")) {
                return;
            }
            //加载jdbc.drivers参数配置的实现类
            String[] driversList = drivers.split(":");
            for (String aDriver : driversList) {
                try {
                    Class.forName(aDriver, true,
                            ClassLoader.getSystemClassLoader());
                } catch (Exception ex) {
                    println("DriverManager.Initialize: load failed: " + ex);
                }
            }
        }
    

    mysql的驱动包中,我们可以看到SPI的配置文件。

    Driver接口实现类配置文件

    源码分析

    本文将根据测试例子中方法的调用顺序来分析。

    	@Test
    	public void test() {
            // 1. 创建一个ServiceLoader对象
            ServiceLoader<UserService> userServiceLoader = ServiceLoader.load(UserService.class);
            // 2. 创建一个迭代器
            Iterator<UserService> userServiceIterator = userServiceLoader.iterator();
            // 3. 加载配置文件并实例化接口实现类
            while(userServiceIterator.hasNext()) {
            	UserService userService = userServiceIterator.next();
            	userService.save();
            	System.out.println("==================");
            }
    	}
    

    注意:考虑篇幅,以下代码经过修改,仅保留所需部分。

    创建一个ServiceLoader

    我们从load(Class service)方法开始分析,可以看到,调用这个方法时还不会去加载配置文件和初始化接口实现类。因为SPI采用延迟加载的方式,只有去调用hasNext()才会去加载配置文件,调用next()才会去实例化对象。

        public static <S> ServiceLoader<S> load(Class<S> service) {
        	// 获得当前线程上下文的类加载器
            ClassLoader cl = Thread.currentThread().getContextClassLoader();
            return ServiceLoader.load(service, cl);
        }
        public static <S> ServiceLoader<S> load(Class<S> service,
                                                ClassLoader loader)
        {	
        	// 创建一个ServiceLoader对象
            return new ServiceLoader<>(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 LinkedHashMap<String,S> providers = new LinkedHashMap<>();
        // 迭代器,有加载和实例化接口实现类的方法
        private LazyIterator lookupIterator;
        public void reload() {
        	// 清空存放的接口实现类对象
            providers.clear();
            // 创建一个LazyIterator
            lookupIterator = new LazyIterator(service, loader);
        }
        // LazyIterator是ServiceLoader的内部类
        private class LazyIterator implements Iterator<S> {
            private LazyIterator(Class<S> service, ClassLoader loader) {
                this.service = service;
                this.loader = loader;
            }
    	}
    

    创建一个迭代器

    因为SPI机制采用了延迟加载的方式,所以在没有调用next()之前,providers会是一个空的Map,也就是说以下的knownProviders也会是一个空的迭代器,所以,这个时候都必须去调用lookupIterator的方法,本文讨论的正是这种情况。

        public Iterator<S> iterator() {
            return new Iterator<S>() {
    			// providers的迭代器,一般为空
                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();
                }
    
            };
        }
    

    加载配置文件

    前面已经提到,当调用hasNext()时才会去加载配置文件。那么,我们直接看LazyIteratorhasNext()方法

    	// 接口类型
      	Class<S> service;
    	// 类加载器
    	ClassLoader loader;
    	// 配置文件列表,一般只有一个
    	Enumeration<URL> configs = null;
    	// 所有实现类全限定类名的迭代器
    	Iterator<String> pending = null;
    	// 下一个实现类全限定类名
    	String nextName = null;
    	public boolean hasNext() {
    		return hasNextService();
    	}
    	private boolean hasNextService() {
    		// 判断是否有下一个实现类全限定类名,有的话直接返回true
    		// 第一次调用这个方法nextName肯定是null的
    		if(nextName != null) {
    			return true;
    		}
    		// 下面就是加载配置文件了
    		if(configs == null) {
    			// 本文例子中:fullName = META-INF/services/cn.zzs.spi.UserService
    			String fullName = PREFIX + service.getName();
    			if(loader == null)
    				configs = ClassLoader.getSystemResources(fullName);
    			else
    				configs = loader.getResources(fullName);
    		}
    		// pending是所有实现类全限定类名的迭代器,此时是空
    		while((pending == null) || !pending.hasNext()) {
    			// 如果文件中没有配置实现类,直接返回false
    			if(!configs.hasMoreElements()) {
    				return false;
    			}
    			// 解析配置文件,并初始化pending迭代器
    			pending = parse(service, configs.nextElement());
    		}
    		// 将第一个实现类的全限定类名赋值给nextName
    		nextName = pending.next();
    		return true;
    	}
    

    解析的过程就是简单的IO操作,这里就不再扩展了。

    实例化接口实现类

    前面已经提到,当调用next()时才会去实例化接口实现类。那么,我们直接看LazyIteratornext()方法。

        public S next() {
        	return nextService();
        }
    
        private S nextService() {
        	// 判断是否有下一个接口实现类。因为前面已经有nextName,所以直接返回true
            if (!hasNextService())
                throw new NoSuchElementException();
            // 获得下一个接口实现类的全限定类名
            String cn = nextName;
            // 将nextName置空,这样下次调用hasNext()就会重新赋值nextName
            nextName = null;
            Class<?> c = null;
            // 加载接口实现类
            c = Class.forName(cn, false, loader);
            // 判断是否是指定接口的实现类
            if (!service.isAssignableFrom(c)) {
                fail(service,"Provider " + cn  + " not a subtype");
            }
            // 转化为指定类型
            S p = service.cast(c.newInstance());
            // 放入providers的Map中
            // 前面提到过,只有调用了next()方法,这个Map才会放入元素
            providers.put(cn, p);
            return p;
        }
    
    

    以上,SPI的源码基本分析完。

    参考资料

    -深入理解SPI机制

    相关源码请移步:https://github.com/ZhangZiSheng001/01-spi-demo

    本文为原创文章,转载请附上原文出处链接:https://www.cnblogs.com/ZhangZiSheng001/p/12114744.html

  • 相关阅读:
    Docker swarm集群增加节点和删除节点
    docker 基础
    Mac 下 java环境 maven环境配置
    git 常用命令(含删除文件) git提交本地分支到远程分支
    github添加ssh keys
    Git在mac中和远程仓库建立连接
    初始化Git仓库(Mac环境下)
    Mac系统显示隐藏文件
    Mac:jenkins忘记管理员账号登录密码如何修改管理员账号
    java实现链表的反转
  • 原文地址:https://www.cnblogs.com/ZhangZiSheng001/p/12114744.html
Copyright © 2020-2023  润新知