Visualización del modelo de delegación parental y SPI desde JDBC

Visualización del modelo de delegación parental y SPI desde JDBC

(I. Resumen

Java en sí tiene un conjunto de servicios de gestión de recursos JNDI (interfaz de nombres y directorios de Java, interfaz de directorios y nombres de Java), que se coloca en rt.jar y se carga mediante el cargador de clases de inicio. JDBC también está en él. Usamos el que más nos es familiar. Tome JDBC como ejemplo para ver por qué JDBC rompe el modelo de delegación parental.

Todos sabemos que cuando usamos JDBC, necesitamos descargar el paquete jar del controlador de conexión de la base de datos proporcionado por el proveedor de la base de datos. Este paquete jar es en realidad la clase de implementación de la interfaz del controlador. La siguiente es la interfaz del controlador:

public interface Driver {
    
    
    Connection connect(String url, java.util.Properties info)
        throws SQLException;
    boolean acceptsURL(String url) throws SQLException;
    DriverPropertyInfo[] getPropertyInfo(String url, java.util.Properties info)
                         throws SQLException;
    int getMajorVersion();
    int getMinorVersion();
    boolean jdbcCompliant();
    public Logger getParentLogger() throws SQLFeatureNotSupportedException;
}

JDK solo puede proporcionar una interfaz estandarizada, pero no puede proporcionar la implementación correspondiente, esto requiere la implementación de varios proveedores de bases de datos. Varios proveedores de bases de datos implementan esta interfaz de controlador a través de la programación orientada a la interfaz, protegiendo así diferentes implementaciones de diferentes tipos de bases de datos, de modo que diferentes bases de datos pueden usar la misma API para operaciones de datos y consultas. Además, hay una clase responsable de administrar los controladores, DriverManager. La mayoría de las personas deberían estar familiarizadas con él, pero es posible que no comprendan los principios que lo sustentan. Primero veamos el código fuente eliminado:

public class DriverManager {
    
    
    // 这里用来保存所有Driver的具体实现
    private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
    public static synchronized void registerDriver(java.sql.Driver driver)
        throws SQLException {
    
    
        registerDriver(driver, null);
    }

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

Se puede ver que debemos registrarnos con registerDriver () en DriverManager antes de usar el controlador de la base de datos, y luego podemos usarlo normalmente.

(2) No destruya el modelo de delegación principal

De acuerdo con nuestro hábito anterior, usaremos el método Class.forName () para cargar el controlador, y luego obtendremos la conexión a través de DriverManager, así:

// 1.加载数据访问驱动
Class.forName("com.mysql.jdbc.Driver");
// 2.连接到数据库
Connection conn= DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb?characterEncoding=GBK", "root", "123456");

Obviamente, el método Class.forName () dispara la carga de la clase del controlador. Tomemos la clase del controlador MySQL como ejemplo para ver cómo se registra el controlador dentro de la clase del controlador.

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

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

Cuando la clase del controlador es cargada por Class.forName (), el bloque de código estático en la clase se ejecutará, y luego el objeto Driver implementado por MySQL será instanciado y registrado con DriverManager. Cuando obtenemos la conexión a través de DriverManager, solo necesitamos atravesar todas las implementaciones actuales de Driver y luego seleccionar una para establecer una conexión.

(3) Destrucción del modelo de delegación principal (mecanismo SPI)

Antes de hablar sobre este modo de carga, es necesario comprender primero SPI.

El nombre completo de SPI es Service Provider Interface, que se utiliza principalmente en complementos o componentes definidos por el proveedor. Se describe en detalle en la documentación de java.util.ServiceLoader. Un breve resumen de la idea del mecanismo java SPI: los módulos abstractos en el sistema a menudo tienen muchos esquemas de implementación diferentes, como el módulo de registro, el módulo de análisis xml y el módulo jdbc.

En el diseño orientado a objetos, generalmente recomendamos que los módulos se basen en la programación de la interfaz y que las clases de implementación no estén codificadas entre módulos. Una vez que el código involucra una clase de implementación específica, viola el principio de capacidad de conexión. Si necesita reemplazar una implementación, debe modificar el código. Para darse cuenta de que no se puede especificar dinámicamente en el programa cuando se ensambla el módulo, se necesita un mecanismo de descubrimiento de servicios. Java SPI proporciona un mecanismo de este tipo: busque una implementación de servicio para una interfaz. Es algo similar a la idea de IOC, que consiste en mover el control de montaje fuera del programa, mecanismo que es especialmente importante en el diseño modular.

El acuerdo específico de Java SPI es: cuando el proveedor de servicios proporciona una implementación de la interfaz de servicio, se crea un archivo con el nombre de la interfaz de servicio en el directorio META-INF / services / del paquete jar al mismo tiempo, y el archivo es la realización de este La clase de implementación concreta de la interfaz de servicio. Cuando el programa externo ensambla este módulo, puede encontrar el nombre de la clase de implementación específica a través del archivo de configuración en el paquete jar META-INF / services / y cargar la instanciación para completar la inyección del módulo. Con base en tal convención, la clase de implementación de la interfaz de servicio se puede encontrar bien, sin la necesidad de formularla en el código.

Después de JDBC4.0, comenzó a admitir el uso de SPI para registrar este controlador. El método específico es especificar la clase de implementación del controlador utilizada actualmente en el archivo META-INF / services / java.sql.Driver en el paquete jar del controlador MySQL, y luego Cuando lo usa, puede obtener la conexión directamente, guardando el proceso de registro de Class.forName () arriba:

 Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb?characterEncoding=GBK", "root", "123456");

Veamos el proceso de uso del método de carga SPI:

  1. Obtenga la clase de implementación específica denominada "com.mysql.jdbc.Driver" del archivo META-INF / services / java.sql.Driver.
  2. Cargue esta clase, aquí también se usa class.forName ("com.mysql.jdbc.Driver") para cargar.

Descubrimos que este método de carga solo ha cambiado de carga manual a carga automática, que parece no ser diferente del método anterior, pero esta carga encontrará muchos problemas. De acuerdo con las reglas de carga de clases, Class.forName () se carga con el cargador de clases del llamador de este método. Como se mencionó anteriormente, el llamador DriverManager está en rt.jar, y su cargador de clases es BootstrapClassLoader y com.mysql.jdbc. El controlador definitivamente no está bajo <JAVA_HOME> / lib, por lo que BootstrapClassLoader no debe poder cargar esta clase. Esta es la limitación del modelo de delegación principal. El cargador principal no puede cargar las clases en la ruta del cargador de clases secundario. Por lo tanto, en este caso, debemos romper las limitaciones del modelo de delegación parental.

Entonces, ¿cómo solucionar este problema? Según la situación actual, este controlador solo puede ser cargado por AppClassLoader, por lo que si hay un método en BootstrapClassLoader para obtener AppClassLoader, entonces puede cargarse a través de él. Este es el llamado cargador de contexto de subprocesos (ContextClassLoader). ContextClassLoader se puede configurar a través del método Thread.setContextClassLoader (). Si no hay una configuración especial, se heredará de la clase principal. Generalmente, AppClassLoader se usa de forma predeterminada. Obviamente, ContextClassLoader permite que el cargador de clases padre cargue clases llamando al cargador de clases hijo, lo que rompe el principio del modelo de delegación padre.

Primero podemos analizar el principio del registro del controlador con el mecanismo de carga del servicio SPI, el foco está en DriverManager.getConnection (). Sabemos que llamar a un método estático de una clase inicializará la clase, y ejecutar su bloque de código estático es una parte esencial del proceso de inicialización de una clase. El siguiente es el bloque de código estático de DriverManager:

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

El método loadInitialDrivers () se llama en el bloque de código estático. El código fuente de este método se enumera a continuación:

private static void loadInitialDrivers() {
    
    
    //省略代码
    //这里就是查找各个数据库厂商在自己的jar包中通过spi注册的驱动
    ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
    Iterator<Driver> driversIterator = loadedDrivers.iterator();
    try{
    
    
         while(driversIterator.hasNext()) {
    
    
         	driversIterator.next();
         }
    } catch(Throwable t) {
    
    
    	// Do nothing
    }
    //省略代码
}

Desde el punto de vista del código fuente, podemos ver que el proceso de carga de SPI es exactamente el mismo que el proceso que mencionamos anteriormente. Encontramos que la carga de SPI se encuentra en el método ServiceLoader.load (Driver.class). Echemos un vistazo a su implementación específica:

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){
    
    
    return new ServiceLoader<>(service, loader);
}

Puede ver que el núcleo del código es obtener ContextClassLoader y luego construir un ServiceLoader. ContextClassLoader almacena la referencia de AppClassLoader de forma predeterminada. Debido a que se coloca en el hilo en tiempo de ejecución, sin importar dónde esté el programa actual (BootstrapClassLoader o ExtClassLoader, etc.), puede usar Thread.currentThread () siempre que lo necesite. getContextClassLoader () saca el cargador de clases de la aplicación para completar la operación requerida.

Hay una oración driversIterator.next () en el método loadInitialDrivers () en DriverManager. Su implementación específica es la siguiente:

private S nextService() {
    
    
    if (!hasNextService())
        throw new NoSuchElementException();
    String cn = nextName;
    nextName = null;
    Class<?> c = null;
    try {
    
    
        //此处的cn就是产商在META-INF/services/java.sql.Driver文件中注册的Driver具体实现类的名称
       //此处的loader就是之前构造ServiceLoader时传进去的ContextClassLoader
        c = Class.forName(cn, false, loader);
    } catch (ClassNotFoundException x) {
    
    
        fail(service, "Provider " + cn + " not found");
    }
 	//省略部分代码
}

Obtuvimos con éxito AppClassLoader a través de ContextClassLoader y, al mismo tiempo, también encontramos el nombre de clase de implementación específico del controlador registrado por el fabricante de la base de datos en el paquete jar secundario, de modo que podamos tener éxito en DriverManager en el paquete rt.jar Se cargan las clases ubicadas en el paquete de la aplicación de terceros.

El mecanismo SPI casi se explica aquí. Para decirlo sin rodeos, el JDK proporciona una forma conveniente de ayudar a los implementadores de terceros a cargar servicios (como controladores de bases de datos, bibliotecas de registros), siempre que sigan la convención (escriba el nombre de la clase en / META) -INF), luego, cuando se inicia, escaneará todas las clases en el paquete jar que cumplan con el nombre de clase acordado, y luego llamará a forName para cargar. Pero el ClassLoader de la persona que llama no se puede cargar, así que cárguelo en el cargador de clases de contexto del hilo del hilo en ejecución actual y luego cárguelo.

16 de septiembre de 2020

Supongo que te gusta

Origin blog.csdn.net/weixin_43907422/article/details/108623940
Recomendado
Clasificación