предисловие
СПИ, полное имя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.jarspring-beans-5.1.10.RELEASE.jarspring-boot-autoconfigure-2.1.9.RELEASE.jarmybatis-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
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