Подробное объяснение механизма Java SPI

Java задняя часть база данных MySQL

Что такое СПИ?

Полное название SPI (Service Provider Interface) представляет собой механизм обнаружения поставщиков услуг, встроенный в JDK. SPI — это механизм динамической замены обнаружения, например, если есть интерфейс, если вы хотите динамически добавить к нему реализацию во время выполнения, вам нужно только добавить реализацию. С чем мы часто сталкиваемся, так это с интерфейсом java.sql.Driver. Другие производители могут создавать разные реализации одного и того же интерфейса. MySQL и postgresql имеют разные реализации для пользователей, а механизм SPI Java может найти службы для интерфейса.

На диаграмме классов интерфейс соответствует определенному абстрактному интерфейсу SPI; разработчик реализует интерфейс SPI; вызывающая сторона полагается на интерфейс SPI.

Определение интерфейса SPI находится на звонилке, и от него больше зависит по концепции, организовано в пакете, где находится звонилка, реализация находится в отдельном пакете.

Когда интерфейс принадлежит реализатору, реализатор предоставляет интерфейс и реализацию.Это использование очень распространено и относится к вызову API. Мы можем обратиться к интерфейсу, чтобы получить функцию вызова класса реализации.

Пример приложения Java SPI

После того, как поставщик услуг предоставит реализацию интерфейса, необходимо создать файл с именем интерфейса службы в каталоге META-INF/services/ в пути к классам.Содержимое этого файла является конкретным классом реализации интерфейса. Когда другие программы нуждаются в этой службе, они могут найти файл конфигурации в META-INF/services/ пакета jar (обычно полагаясь на пакет jar).Файл конфигурации содержит конкретное имя класса реализации интерфейса.Вы можете загрузить и создать экземпляр в соответствии с этим именем класса, и вы можете использовать службу. Класс инструмента для поиска реализаций службы в JDK: java.util.ServiceLoader.

SPI-интерфейс

public interface ObjectSerializer {

    byte[] serialize(Object obj) throws ObjectSerializerException;

    <T> T deSerialize(byte[] param, Class<T> clazz) throws ObjectSerializerException;

    String getSchemeName();
}

Определяет интерфейс сериализации объекта с тремя методами: метод сериализации, метод десериализации и имя сериализации.

Специальная реализация SPI

public class KryoSerializer implements ObjectSerializer {

    @Override
    public byte[] serialize(Object obj) throws ObjectSerializerException {
        byte[] bytes;
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        try {
            //获取kryo对象
            Kryo kryo = new Kryo();
            Output output = new Output(outputStream);
            kryo.writeObject(output, obj);
            bytes = output.toBytes();
            output.flush();
        } catch (Exception ex) {
            throw new ObjectSerializerException("kryo serialize error" + ex.getMessage());
        } finally {
            try {
                outputStream.flush();
                outputStream.close();
            } catch (IOException e) {

            }
        }
        return bytes;
    }

    @Override
    public <T> T deSerialize(byte[] param, Class<T> clazz) throws ObjectSerializerException {
        T object;
        try (ByteArrayInputStream inputStream = new ByteArrayInputStream(param)) {
            Kryo kryo = new Kryo();
            Input input = new Input(inputStream);
            object = kryo.readObject(input, clazz);
            input.close();
        } catch (Exception e) {
            throw new ObjectSerializerException("kryo deSerialize error" + e.getMessage());
        }
        return object;
    }

    @Override
    public String getSchemeName() {
        return "kryoSerializer";
    }

}

Используйте метод сериализации Kryo. Kryo — это быстрая и эффективная среда сериализации объектных графов Java, которая изначально поддерживает Java и даже лучше, чем знаменитая структура сериализации protobuf от Google в сериализации Java.

public class JavaSerializer implements ObjectSerializer {
    @Override
    public byte[] serialize(Object obj) throws ObjectSerializerException {
        ByteArrayOutputStream arrayOutputStream;
        try {
            arrayOutputStream = new ByteArrayOutputStream();
            ObjectOutput objectOutput = new ObjectOutputStream(arrayOutputStream);
            objectOutput.writeObject(obj);
            objectOutput.flush();
            objectOutput.close();
        } catch (IOException e) {
            throw new ObjectSerializerException("JAVA serialize error " + e.getMessage());
        }
        return arrayOutputStream.toByteArray();
    }

    @Override
    public <T> T deSerialize(byte[] param, Class<T> clazz) throws ObjectSerializerException {
        ByteArrayInputStream arrayInputStream = new ByteArrayInputStream(param);
        try {
            ObjectInput input = new ObjectInputStream(arrayInputStream);
            return (T) input.readObject();
        } catch (IOException | ClassNotFoundException e) {
            throw new ObjectSerializerException("JAVA deSerialize error " + e.getMessage());
        }
    }

    @Override
    public String getSchemeName() {
        return "javaSerializer";
    }

}

Собственный метод сериализации Java.

Добавить файл каталога META-INF

Создайте файл с именем интерфейса службы в каталоге META-INF/services в разделе Resource.

com.blueskykong.javaspi.serializer.KryoSerializer
com.blueskykong.javaspi.serializer.JavaSerializer

Класс обслуживания

@Service
public class SerializerService {


    public ObjectSerializer getObjectSerializer() {
        ServiceLoader<ObjectSerializer> serializers = ServiceLoader.load(ObjectSerializer.class);

        final Optional<ObjectSerializer> serializer = StreamSupport.stream(serializers.spliterator(), false)
                .findFirst();

        return serializer.orElse(new JavaSerializer());
    }
}

Получить определенный метод сериализации и взять только первый (мы прописали два в конфигурации), если он не найден, вернуть собственный метод сериализации Java.

тестовый класс

    @Autowired
    private SerializerService serializerService;

    @Test
    public void serializerTest() throws ObjectSerializerException {
        ObjectSerializer objectSerializer = serializerService.getObjectSerializer();
        System.out.println(objectSerializer.getSchemeName());
        byte[] arrays = objectSerializer.serialize(Arrays.asList("1", "2", "3"));
        ArrayList list = objectSerializer.deSerialize(arrays, ArrayList.class);
        Assert.assertArrayEquals(Arrays.asList("1", "2", "3").toArray(), list.toArray());
    }

Тестовый пример проходит, и выводkryoSerializer.

Цель СПИ

Механизм SPI используется в базе данных DriverManager, Spring, ConfigurableBeanFactory и т. д. Здесь в качестве примера можно взять базу данных DriverManager, чтобы увидеть внутреннюю историю ее реализации.

DriverManager — это класс инструментов для управления и регистрации различных драйверов баз данных в jdbc. Для базы данных могут быть разные реализации драйвера базы данных. Когда мы используем конкретную реализацию драйвера, мы не хотим модифицировать существующий код, а надеемся, что эффекта можно добиться за счет простой настройки. При использовании драйвера mysql возникнет вопрос, как DriverManager получает определенный класс драйвера? После того, как мы используем Class.forName("com.mysql.jdbc.Driver") для загрузки драйвера mysql, мы выполним в нем статический код, чтобы зарегистрировать драйвер в DriverManager для последующего использования.

До JDBC4.0 при подключении к БД обычно использовалосьClass.forName("com.mysql.jdbc.Driver")Это предложение сначала загружает драйверы, связанные с базой данных, а затем выполняет такие операции, как получение соединений. А после JDBC4.0 не нужноClass.forNameЧтобы загрузить драйвер, вы можете получить соединение напрямую, здесь для этого используется механизм расширения SPI Java.

Интерфейс java.sql.Driver определен в java, конкретной реализации нет, конкретные реализации предоставляются разными производителями.

mysql

В mysql-connector-java-5.1.45.jar будет файл с именем java.sql.Driver в каталоге META-INF/services:

com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver

pg

В postgresql-42.2.2.jar будет файл с именем java.sql.Driver в каталоге META-INF/services:

org.postgresql.Driver

использование

String url = "jdbc:mysql://localhost:3306/test";
Connection conn = DriverManager.getConnection(url,username,password);

Выше показано использование mysql, аналогичное использование pg. Не нужно использоватьClass.forName("com.mysql.jdbc.Driver")для загрузки драйвера.

Реализация Mysql DriverManager

В приведенном выше коде нет кода для загрузки драйвера.Как мы можем определить, какой драйвер подключения к базе данных использовать? Это включает в себя использование механизма расширения Java SPI для поиска вещей, связанных с драйверами. Поиск драйверов фактически осуществляется в DriverManager. DriverManager — это реализация на Java для получения соединений с базой данных. В DriverManager есть следующий статический блок кода:

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

Вы можете видеть, что внутри него находится статический блок кода.loadInitialDriversметод,loadInitialDriversИспользование использует класс инструментов spi, упомянутый выше.ServiceLoader:

    public Void run() {

        ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
        Iterator<Driver> driversIterator = loadedDrivers.iterator();

        /* Load these drivers, so that they can be instantiated.
         * It may be the case that the driver class may not be there
         * i.e. there may be a packaged driver with the service class
         * as implementation of java.sql.Driver but the actual class
         * may be missing. In that case a java.util.ServiceConfigurationError
         * will be thrown at runtime by the VM trying to locate
         * and load the service.
         *
         * Adding a try catch block to catch those runtime errors
         * if driver not available in classpath but it's
         * packaged as service and that service is there in classpath.
         */
        try{
            while(driversIterator.hasNext()) {
                driversIterator.next();
            }
        } catch(Throwable t) {
        // Do nothing
        }
        return null;
    }

Пройдите конкретную реализацию, полученную с помощью SPI, и создайте экземпляр каждого класса реализации. При обходе первый звонокdriversIterator.hasNext()метод, файл java.sql.Driver в пути к классам и все каталоги META-INF/services в пакете jar будут найдены, и будет найдено имя класса реализации в файле.В настоящее время нет конкретного класса реализации будет создан экземпляр.

Суммировать

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

Код для этой статьи: https://github.com/keets2012/Spring-Boot-Samples/tree/master/java-spi

Подписывайтесь на свежие статьи, приглашаю обратить внимание на мой публичный номер

微信公众号

Ссылаться на

  1. Углубленный анализ исходного кода механизма SPI в Java
  2. Прочесывание мыслей Java SPI