Принцип и применение механизма SPI

Java

предисловие

СПИ, полное имяService Provider Interface, представляет собой механизм обнаружения службы. Он передается в пути ClassPathMETA-INF/servicesПапки ищут файлы и автоматически загружают классы, определенные в файле.

Этот механизм обеспечивает возможность расширения многих фреймворков, например, вDubbo、JDBC、SpringBootВо всех них используется механизм SPI. Хотя они реализованы по-разному, принципы одинаковы. Сегодня мы рассмотрим, насколько священным является SPI и какую роль он играет во многих средах с открытым исходным кодом.

1. СПИ в JDK

Давайте начнем с JDK и посмотрим, как он используется на очень простом примере.

1. Маленькие каштаны

Во-первых, нам нужно определить интерфейс, SpiService

public interface SpiService {
    void println();
}

Затем определите класс реализации, ничего больше, просто напечатайте.

public class SpiServiceImpl implements SpiService {
    @Override
    public void println() {
        System.out.println("-------------");
    }
}

Наконец, добавьте файл конфигурации в путь к ресурсам. Имя файла — это полное имя класса интерфейса, содержимое — это полное имя класса реализации, а несколько классов реализации разделены символами новой строки.

Содержимое файла — это полное имя класса реализации:

com.youyouxunyin.service.impl.SpiServiceImpl

2. Тест

Тогда мы можем пройтиServiceLoader.loadМетод получает экземпляр реализующего класса и вызывает его методы.

public static void main(String[] args){
    ServiceLoader<SpiService> load = ServiceLoader.load(SpiService.class);
    Iterator<SpiService> iterator = load.iterator();
    while (iterator.hasNext()){
        SpiService service = iterator.next();
        service.println();
    }
}

3. Анализ исходного кода

Во-первых, давайте разбиратьсяServiceLoader, посмотрите на его структуру классов.

public final class ServiceLoader<S> implements Iterable<S>{
    //配置文件的路径
    private static final String PREFIX = "META-INF/services/";
    //加载的服务类或接口
    private final Class<S> service;
    //已加载的服务类集合
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
    //类加载器
    private final ClassLoader loader;
    //内部类,真正加载服务类
    private LazyIterator lookupIterator;
}

когда мы звонимloadметод, нет реальной загрузки и поиска класса обслуживания. вместо этого называетсяServiceLoaderКонструктор, самое главное здесь - создать экземпляр внутреннего классаLazyIterator, это следующий главный герой.

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;
    //先清空
    providers.clear();
    //实例化内部类 
    LazyIterator lookupIterator = new LazyIterator(service, loader);
}

Процесс поиска классов реализации и создания классов реализации находится вLazyIteratorЗаканчивать. когда мы звонимiterator.hasNext和iterator.nextКогда метод вызывается, фактический вызовLazyIteratorсоответствующий метод.

public Iterator<S> iterator() {

    return new Iterator<S>() {
	
    	public boolean hasNext() {
    		return lookupIterator.hasNext();
    	}
    	public S next() {
    		return lookupIterator.next();
    	}
    	.......
    };
}

Поэтому мы ориентируемся наlookupIterator.hasNext()метод, который в конечном итоге вызываетhasNextService, где возвращается имя класса реализации.

private class LazyIterator implements Iterator<S>{
    Class<S> service;
    ClassLoader loader;
    Enumeration<URL> configs = null;
    Iterator<String> pending = null;
    String nextName = null;	
    private boolean hasNextService() {
    	//第二次调用的时候,已经解析完成了,直接返回
    	if (nextName != null) {
    	    return true;
    	}
    	if (configs == null) {
    	    //META-INF/services/ 加上接口的全限定类名,就是文件服务类的文件
    	    //META-INF/services/com.viewscenes.netsupervisor.spi.SPIService
    	    String fullName = PREFIX + service.getName();
    	    //将文件路径转成URL对象
    	    configs = loader.getResources(fullName);
    	}
    	while ((pending == null) || !pending.hasNext()) {
    	    //解析URL文件对象,读取内容,最后返回
    	    pending = parse(service, configs.nextElement());
    	}
    	//拿到第一个实现类的类名
    	nextName = pending.next();
    	return true;
    }
}

тогда, когда мы звонимnext()метод, вызовlookupIterator.nextService. Он создает экземпляр класса реализации и возвращает его через отражение.

private S nextService() {
    //全限定类名
    String cn = nextName;
    nextName = null;
    //创建类的Class对象
    Class<?> c = Class.forName(cn, false, loader);
    //通过newInstance实例化
    S p = service.cast(c.newInstance());
    //放入集合,返回实例
    providers.put(cn, p);
    return p; 
}

На данный момент получен экземпляр класса.

Во-вторых, применение JDBC

Мы говорили в начале, что механизм SPI обеспечивает возможность расширения многих фреймворков, на самом деле к этому механизму применяется JDBC.

В прошлом необходимо было сначала настроить соединение с базой данных, а затем передатьDriverManager.getConnectionполучить одинConnection.

String url = "jdbc:mysql:///consult?serverTimezone=UTC";
String user = "root";
String password = "root";

Class.forName("com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection(url, user, password);

Теперь, при настройке подключения к базе данных, этот шаг больше не нужен, так как же узнать, какая это база данных? Ответ кроется в SPI.

1. Загрузить

Мы снова обращаем внимание наDriverManagerClass, который делает более важную вещь в статическом блоке кода. Очевидно, он инициализировал подключение драйвера базы данных через механизм SPI.

public class DriverManager {
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }
}

Конкретный процесс зависит отloadInitialDrivers, он ищет сервисный класс интерфейса Driver, поэтому его путь к файлу:

META-INF/services/java.sql.Driver

private static void loadInitialDrivers() {
    AccessController.doPrivileged(new PrivilegedAction<Void>() {
    	public Void run() {
    		//很明显,它要加载Driver接口的服务类,Driver接口的包为:java.sql.Driver
    		//所以它要找的就是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;
    	}
    });
}

Итак, где этот файл? Давайте посмотрим на пакет jar MySQL, который представляет собой этот файл.Содержимое файла:com.mysql.cj.jdbc.Driver.

2. Создайте экземпляр

Предыдущий шаг нашел MySQLcom.mysql.cj.jdbc.DriverПолное имя класса, экземпляр этого класса создается при вызове следующего метода. он делает одну вещь,DriverManagerЗарегистрируйте экземпляр самого себя.

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    static {
        try {
            //注册、调用DriverManager类的注册方法
            //往registeredDrivers集合中加入实例
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can't register driver!");
        }
    }
}

3. Создать соединение

DriverManager.getConnection()В методе создается соединение, оно проходит через зарегистрированный драйвер базы данных, вызывает свой метод соединения, получает соединение и возвращает значение.

private static Connection getConnection(String url, Properties info, Class<?> caller) throws SQLException {	
    //registeredDrivers中就包含com.mysql.cj.jdbc.Driver实例
    for(DriverInfo aDriver : registeredDrivers) {
    	if(isDriverAllowed(aDriver.driver, callerCL)) {
    	    try {
    	    	//调用connect方法创建连接
    	    	Connection con = aDriver.driver.connect(url, info);
    	    	if (con != null) {
    	    	    return (con);
    	    	}
    	    }catch (SQLException ex) {
    	    	if (reason == null) {
    	    	    reason = ex;
    	    	}
    	    }
    	} else {
    	    println("skipping: " + aDriver.getClass().getName());
    	}
    }
}

4. Расширение

Теперь, когда мы знаем, как JDBC создает соединения с базой данных, можем ли мы рассказать об этом подробнее? Если мы создадимjava.sql.Driverфайл, пользовательский класс реализации MySQLDriver, то некоторая информация может быть динамически изменена до и после получения соединения.

Или сначала создайте файл под ресурсами проекта, содержимое файла представляет собой пользовательский класс драйвера.com.youyouxunyin.driver.MySQLDriver

нашMySQLDriverКласс реализации, унаследованный отMySQL中的NonRegisteringDriver, но и для достиженияjava.sql.Driverинтерфейс. Таким образом, когда вызывается метод подключения, этот класс будет вызываться, но фактический процесс создания все еще выполняется MySQL.

public class MySQLDriver extends NonRegisteringDriver implements Driver{
    static {
        try {
            DriverManager.registerDriver(new MySQLDriver());
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    public MySQLDriver() throws SQLException {}

    @Override
    public Connection connect(String url, Properties info) throws SQLException {
        System.out.println("准备创建数据库连接.url:"+url);
        System.out.println("JDBC配置信息:"+info);
        info.setProperty("user", "root");
        Connection connection =  super.connect(url, info);
        System.out.println("数据库连接创建完成!"+connection.toString());
        return connection;
    }
}

В этом случае, когда мы получим соединение с базой данных, оно будет вызываться здесь.

--------------------输出结果---------------------
准备创建数据库连接.url:jdbc:mysql:///consult?serverTimezone=UTC
JDBC配置信息:{user=root, password=root}
数据库连接创建完成!com.mysql.cj.jdbc.ConnectionImpl@7cf10a6f

3. Приложение в SpringBoot

Spring Boot предоставляет быстрый способ создания готовых к работе приложений на базе Spring. Он основан на инфраструктуре Spring, предпочитает соглашение, а не настройку, и предназначен для того, чтобы вы могли начать работу как можно быстрее.

Даже без каких-либо файлов конфигурации веб-приложения SpringBoot могут работать нормально. Эта волшебная вещь, SpringBoot полагается на автоматическую настройку для завершения.

Говоря об этом, мы должны сосредоточиться на одном:SpringFactoriesLoader, от чего зависит загрузка автоматической конфигурации.

1. Файл конфигурации

SpringFactoriesLoaderотвечать за загрузку конфигурации. Мы открываем этот класс и видим, что путь, по которому он загружает файл:META-INF/spring.factories

Автор искал этот файл в проекте и обнаружил, что есть 4 пакета Jar, которые его содержат:

  • spring-boot-2.1.9.RELEASE.jar
  • spring-beans-5.1.10.RELEASE.jar
  • spring-boot-autoconfigure-2.1.9.RELEASE.jar
  • mybatis-spring-boot-autoconfigure-2.1.0.jar

Так что же в них? По сути, это сопоставление интерфейсов и классов. Автор не будет размещать это здесь, а заинтересованные друзья пойдут и увидят сами.

Например, при запуске SpringBoot всеApplicationContextInitializer, то вы можете сделать:

SpringFactoriesLoader.loadFactoryNames(ApplicationContextInitializer.class, classLoader)

2. Загрузите файл

loadSpringFactoriesотвечает за чтение всехspring.factoriesсодержание документа.

private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {

    MultiValueMap<String, String> result = cache.get(classLoader);
    if (result != null) {
    	return result;
    }
    try {
    	//获取所有spring.factories文件的路径
    	Enumeration<URL> urls = lassLoader.getResources("META-INF/spring.factories");
    	result = new LinkedMultiValueMap<>();
    	while (urls.hasMoreElements()) {
    	    URL url = urls.nextElement();
    	    //加载文件并解析文件内容
    	    UrlResource resource = new UrlResource(url);
    	    Properties properties = PropertiesLoaderUtils.loadProperties(resource);
    	    for (Map.Entry<?, ?> entry : properties.entrySet()) {
    	    	String factoryClassName = ((String) entry.getKey()).trim();
    	    	for (String factoryName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
    	    	    result.add(factoryClassName, factoryName.trim());
    	    	}
    	    }
    	}
    	cache.put(classLoader, result);
    	return result;
    }
    catch (IOException ex) {
    	throw new IllegalArgumentException("Unable to load factories from location [" +
    		FACTORIES_RESOURCE_LOCATION + "]", ex);
    }
}

Видно, что он не использует механизм SPI в JDK для загрузки этих классов, но принцип аналогичен. Все через файл конфигурации загрузите и проанализируйте содержимое файла, а затем создайте экземпляр с помощью отражения.

3. Примите участие

Если вы хотите участвоватьSpringBootВ процессе инициализации теперь у нас другой путь.

Мы также создаемspring.factoriesфайл, настройте инициализатор.

org.springframework.context.ApplicationContextInitializer=com.youyouxunyin.config.context.MyContextInitializer

Затем определите класс MyContextInitializer

public class MyContextInitializer implements ApplicationContextInitializer {
    @Override
    public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
        System.out.println(configurableApplicationContext);
    }
}

4. Приложение в Даббо

Не исключение и всем знакомый Dubbo, он тоже загружает все компоненты через механизм SPI. Точно так же Dubbo не использует встроенный в Java механизм SPI, а совершенствует его для лучшего удовлетворения потребностей. В Dubbo SPI — очень важный модуль. На основе SPI мы можем легко расширить Dubbo.

По поводу принципа, если вы не знакомы с принципом, то можете сослаться на статью автора:

SPI и механизм адаптивного расширения в Dubbo

Он также используется таким же образомMETA-INF/servicesСоздайте файл и напишите соответствующие имена классов.

Сценарии использования см.SpringBoot + Dubbo интегрированный бой ELK