1. ホーム
  2. Java

Java SPIメカニズム解説

2022-03-03 16:59:25
<パス

Java SPI機構詳細説明

1. SPIとは何ですか?

SPIは、(Service Provider Interface)と呼ばれています。
JDKに組み込まれたサービスプロバイダ発見機構で、動的な置き換え発見機構である。

例えば、あるインタフェースがあり、実行時に動的にそのインタフェースに実装を追加したい場合、実装を追加すればよいのです。java.sql.Driverインターフェイスでは、よくこのようなケースに遭遇します。
mysql と postgresql はそれぞれ異なる実装をユーザが利用できるようにしています。
また、JavaのSPI機構は、あるインターフェースに対するサービス実装を見つけることができます。


このように、インタフェースは抽象的なSPIインタフェースに対応し、実装者はSPIインタフェースを実装し、呼び出し側はSPIインタフェースに依存することになります。
SPIインターフェースは呼び出し側で定義され、概念的には呼び出し側への依存度が高く、組織は呼び出し側があるパッケージにあり、実装は別のパッケージにある。

サービスプロバイダがインターフェースの実装を提供する場合、クラスパス下のMETA-INF/services/ディレクトリに、サービスインターフェースと同じ名前のファイルを作成する必要があります。
このファイルの中身は、インターフェイスの具体的な実装クラスです。他のアプリケーションがこのサービスを必要とするとき、(通常、依存関係として使用される)jarパッケージを調べることができます。
サービス実装を見つけるためのJDKのツールクラスは、java.util.ServiceLoader.ServiceLoaderです。

2. SPIの用途

データベースDriverManager、Spring、ConfigurableBeanFactoryなどは、すべてSPIの仕組みを利用している。ここでは、データベースDriverManagerの例で、その実装の内部構造を見ることができます。
DriverManagerは、異なるデータベースドライバを管理・登録するための、jdbcにおけるツールクラスです。1つのデータベースに対して、異なるデータベースドライバの実装が存在する場合があります。
特定のドライバ実装を使用する際に、既存のコードを修正せず、簡単な設定で結果を出したい。
mysqlドライバを使用する場合、DriverManagerが特定の識別されたドライバクラスをどのように取得するかという問題があります。
Class.forName("com.mysql.jdbc.Driver") を使って mysql ドライバをロードした後、その中の静的コードを実行して DriverManager にドライバを登録しています。
を使用します。

ドライバの実装

ドライバクラスの静的コードブロック内で DriverManager メソッドに登録し、ドライバマネージャにパラメータとして渡します。

package com.mysql.jdbc;

import java.sql.DriverManager;
import java.sql.SQLException;

public class Driver extends NonRegisteringDriver implements java.sql.
    public Driver() throws SQLException {
    }

    static {
        try {
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can't register driver!");
        }
    }
}

 /**
     * Load the initial JDBC drivers by checking the System property
     * jdbc.properties and then use the {@code ServiceLoader} mechanism
     */
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }


Mysql DriverManagerの実装

loadInitialDrivers

その内部静的コードブロックに loadInitialDrivers メソッドを使用します。 ServiceLoader 使い方は、上記の spi tool クラスを使用します。 private static void loadInitialDrivers() { String drivers; try { drivers = AccessController.doPrivileged(new PrivilegedAction () { public String run() { return System.getProperty("jdbc.drivers"); } }); } catch (Exception ex) { drivers = null; } // If the driver is packaged as a Service Provider, load it. // Get all the drivers through the classloader // exposed as a java.sql.Driver.class service. // ServiceLoader.load() replaces the sun.misc.Providers() AccessController.doPrivileged(new PrivilegedAction () { public Void run() { ServiceLoader loadedDrivers = ServiceLoader.load(Driver.class); Iterator 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 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 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. * In that case a java.util. ServiceConfigurationError * will be thrown at runtime by the VM trying to locate ServiceConfigurationError * will be thrown at runtime by the VM trying to locate * and load the service. *In that case a java.util. * Adding a try catch block to catch those runtime errors Adding a try catch block to catch those runtime errors * if driver not available in classpath but it's Adding a try catch block to catch those runtime errors * if driver is 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; } }); println("DriverManager.initialize: jdbc.drivers = " + drivers); if (drivers == null || drivers.equals("")) { return; } String[] driversList = drivers.split(":"); println("number of Drivers:" + driversList.length); for (String aDriver : driversList) { try { println("DriverManager.Initialize: loading " + aDriver); Class.forName(aDriver, true, ClassLoader.getSystemClassLoader()); } catch (Ex :

ServiceLoader

spiツールクラス
public final class ServiceLoader

    implements Iterable

{

    private static final String PREFIX = "META-INF/services/";

最初にjdbc.driversプロパティの値を見つけ、次にSPIメカニズムを通してドライバを見つける。

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;
        }

loadInitialDrivers

META-INF/services/ フォルダ下のクラス名 filename (ここでは Driver.class.getName() に相当) のリソースがロードされ、VM に読み込まれていることがわかります。

/**
     * Registers the given driver with the {@code DriverManager}.
     * A newly-loaded driver class should call
     * the method {@code registerDriver} to make itself
     A newly-loaded driver class should call * the method {@code registerDriver} to make itself known to the {@code DriverManager}. If the driver is currently
     If the driver is currently * registered, no action is taken.
     If the driver is currently * registered, no action is taken.
     * @param driver the new JDBC Driver that is to be registered with the
     * {@code DriverManager}
     * @param da the {@code DriverAction} implementation to be used when
     * {@code DriverManager#deregisterDriver} is called
     * @exception SQLException if a database access error occurs
     * @exception NullPointerException if {@code driver} is null
     * @since 1.8
     @since 1.8 */
    public static synchronized void registerDriver(java.sql.Driver driver,
            DriverAction da)
        throws SQLException {

        /* Register the driver if it has not already been added to our list */
        if(driver ! = null) {
            registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
        } else {
            // This is for compatibility with the original DriverManager
            throw new NullPointerException();
        }

        println("registerDriver: " + driver);

    }

public class DriverManager {


    // List of registered JDBC drivers
    private final static CopyOnWriteArrayList
 
    registeredDrivers = new CopyOnWriteArrayList<>();


 @CallerSensitive
    public static Connection getConnection(String url,
        String user, String password) throws SQLException {
        Properties info = new java.util;

        if (user ! = null) {
            info.put("user", user);
        }
        if (password ! = null) {
            info.put("password", password);
        }

        return (getConnection(url, info, Reflection.getCallerClass()));
    }

 private static Connection getConnection(
        String url, java.util.Properties info, Class<? > caller) throws SQLException {
        /*
         * When callerCl is null, we should check the application's
         * (which is invoking this class indirectly)
         /* classloader, so that the JDBC driver class outside rt.jar
         * can be loaded from here.
         */
        ClassLoader callerCL = caller ! = null ? caller.getClassLoader() : null;
        synchronized(DriverManager.class) {
            // synchronize loading of the correct classloader.
            if (callerCL == null) {
                callerCL = Thread.currentThread().getContextClassLoader();
            }
        }

        if(url == null) {
            throw new SQLException("The url cannot be null", "08001");
        }

        println("DriverManager.getConnection(\"" + url + "\")");

        // Walk through the loaded registeredDrivers attempting to make a connection.
        // Remember the first exception that gets raised so we can reraise it.
        SQLException reason = null;

        for(DriverInfo aDriver : registeredDrivers) {
            // If the caller does not have permission to load the driver then
            // skip it.
            if(isDriverAllowed(aDriver.driver, callerCL)) {
                try {
                    println(" trying " + aDriver.driver.getClass().getName());
                    Connection con = aDriver.driver.connect(url, info);
                    if (con ! = null) {
                        // Success!
                        println("getConnection returning " + aDriver.driver.getClass().getName());
                        return (con);
                    }
                } catch (SQLException ex) {
                    if (reason == null) {
                        reason = ex;
                    }
                }

            } else {
                println(" skipping: " + aDriver.getClass().getName());
            }

        }

        // if we got here nobody could connect.
        if (reason ! = null) {
            println("getConnection failed: " + reason);
            throw reason;
        }

        println("getConnection: no suitable driver found for " + url);
        throw new SQLException("No suitable driver found for " + url, "08001");
    }


private static boolean isDriverAllowed(Driver driver, ClassLoader classLoader) {
        boolean result = false;
        if(driver ! = null) {
            Class<? > aClass = null;
            try {
                aClass = Class.forName(driver.getClass().getName(), true, classLoader);
            } catch (Exception ex) {
                result = false;
            result = false; }

             result = ( aClass == driver.getClass() ) ? true : false;
        }

        return result;
    }


loadInitialDrivers コメント "Load these drivers, so they can be instantiated." は、SPIによってスキャンされたドライバをロードして、初期化をトリガーすることを意味します。つまり、静的コードブロックのトリガーです。

/**
     * Registers the given driver with the {@code DriverManager}.
     * A newly-loaded driver class should call
     * the method {@code registerDriver} to make itself
     A newly-loaded driver class should call * the method {@code registerDriver} to make itself known to the {@code DriverManager}. If the driver is currently
     If the driver is currently * registered, no action is taken.
     If the driver is currently * registered, no action is taken.
     * @param driver the new JDBC Driver that is to be registered with the
     * {@code DriverManager}
     * @param da the {@code DriverAction} implementation to be used when
     * {@code DriverManager#deregisterDriver} is called
     * @exception SQLException if a database access error occurs
     * @exception NullPointerException if {@code driver} is null
     * @since 1.8
     @since 1.8 */
    public static synchronized void registerDriver(java.sql.Driver driver,
            DriverAction da)
        throws SQLException {

        /* Register the driver if it has not already been added to our list */
        if(driver ! = null) {
            registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
        } else {
            // This is for compatibility with the original DriverManager
            throw new NullPointerException();
        }

        println("registerDriver: " + driver);

    }


ドライバマネージャのドライバリストに自分を登録する

public class DriverManager {


    // List of registered JDBC drivers
    private final static CopyOnWriteArrayList
 
    registeredDrivers = new CopyOnWriteArrayList<>();



接続を取得する際に、リストから取得するドライバマネージャのconnectionメソッドを呼び出します。

 @CallerSensitive
    public static Connection getConnection(String url,
        String user, String password) throws SQLException {
        Properties info = new java.util;

        if (user ! = null) {
            info.put("user", user);
        }
        if (password ! = null) {
            info.put("password", password);
        }

        return (getConnection(url, info, Reflection.getCallerClass()));
    }


 private static Connection getConnection(
        String url, java.util.Properties info, Class<? > caller) throws SQLException {
        /*
         * When callerCl is null, we should check the application's
         * (which is invoking this class indirectly)
         /* classloader, so that the JDBC driver class outside rt.jar
         * can be loaded from here.
         */
        ClassLoader callerCL = caller ! = null ? caller.getClassLoader() : null;
        synchronized(DriverManager.class) {
            // synchronize loading of the correct classloader.
            if (callerCL == null) {
                callerCL = Thread.currentThread().getContextClassLoader();
            }
        }

        if(url == null) {
            throw new SQLException("The url cannot be null", "08001");
        }

        println("DriverManager.getConnection(\"" + url + "\")");

        // Walk through the loaded registeredDrivers attempting to make a connection.
        // Remember the first exception that gets raised so we can reraise it.
        SQLException reason = null;

        for(DriverInfo aDriver : registeredDrivers) {
            // If the caller does not have permission to load the driver then
            // skip it.
            if(isDriverAllowed(aDriver.driver, callerCL)) {
                try {
                    println(" trying " + aDriver.driver.getClass().getName());
                    Connection con = aDriver.driver.connect(url, info);
                    if (con ! = null) {
                        // Success!
                        println("getConnection returning " + aDriver.driver.getClass().getName());
                        return (con);
                    }
                } catch (SQLException ex) {
                    if (reason == null) {
                        reason = ex;
                    }
                }

            } else {
                println(" skipping: " + aDriver.getClass().getName());
            }

        }

        // if we got here nobody could connect.
        if (reason ! = null) {
            println("getConnection failed: " + reason);
            throw reason;
        }

        println("getConnection: no suitable driver found for " + url);
        throw new SQLException("No suitable driver found for " + url, "08001");
    }



private static boolean isDriverAllowed(Driver driver, ClassLoader classLoader) {
        boolean result = false;
        if(driver ! = null) {
            Class<? > aClass = null;
            try {
                aClass = Class.forName(driver.getClass().getName(), true, classLoader);
            } catch (Exception ex) {
                result = false;
            result = false; }

             result = ( aClass == driver.getClass() ) ? true : false;
        }

        return result;
    }