JDBC SPI和打破双亲委派

本文详细探讨了Java的SPI(Service Provider Interface)机制,包括ServiceLoader的源码分析,以及如何通过SPI加载JDBC的Driver实现类。同时,文章分析了JDBC在数据库连接中的应用,如DriverManager的加载过程。此外,还讨论了打破双亲委派机制的情况,特别是在使用SPI时如何利用线程上下文类加载器。最后,文章介绍了Statement接口的不同类型及其在数据库查询中的应用。
摘要由CSDN通过智能技术生成

一、SPI机制

参考文章:深入理解SPI机制

1.1 什么是SPI

SPI ,全称为 Service Provider Interface,是一种服务发现机制。它通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类。
这一机制为很多框架扩展提供了可能,比如在DubboJDBC中都使用到了SPI机制。

  • sun.misc.Service
    Service.providers(SPIService.class)
  • java.util.ServiceLoader
    ServiceLoader.load(SPIService.class)
1.2 ServiceLoader 源码分析
1.2.1 类结构
	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;
}
1.2.1 Load方法

load方法创建了一些属性,重要的是实例化了内部类,LazyIterator。最后返回ServiceLoader的实例。

public final class ServiceLoader<S> implements Iterable<S>
    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);
    }
}
1.2.2 查找实现类

查找实现类创建实现类的过程,都在LazyIterator完成。当我们调用iterator.hasNextiterator.next方法的时候,实际上调用的都是LazyIterator的相应方法。

public Iterator<S> iterator() {
    return new Iterator<S>() {
        public boolean hasNext() {
            return lookupIterator.hasNext();
        }
        public S next() {
            return lookupIterator.next();
        }
        .......
    };
}
private boolean hasNextService() {
 	Class<S> service;
    ClassLoader loader;
    Enumeration<URL> configs = null;
    Iterator<String> pending = null;
    String nextName = null; 
	// 第二次调用,已经解析完了,直接返回
    if (nextName != null) {
        return true;
    }
    if (configs == null) {
        try {
        	// 获取文件全名 
            String fullName = PREFIX + service.getName();
            // 将文件路径转成URL对象
            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;
        }
        //  解析URL文件对象,读取内容,最后返回
        pending = parse(service, configs.nextElement());
    }
    // 拿到第一个实现类的类名
    nextName = pending.next();
    return true;
}
1.2.3 创建实例

当然,调用next方法的时候,实际调用到的是,lookupIterator.nextService。它通过反射的方式,创建实现类的实例并返回。

private class LazyIterator implements Iterator<S>{
    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的应用

  • JDBC 3.0 以前的标准,没有使用SPI机制,需要手动加载Driver的实现类
  • JDBC 4.0 Class.forName("**")代码可以省去
  • DriverManagerloadInitialDrivers 可以自动去加载实现类
2.1 DriverManager加载Driver源码
public class DriverManager {
   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;
           }
       });
   }
}
2.2 Driver实现类源码

它会向DriverManager注册自己,DriverManager.getConnection("","","")取得数据库连接

package com.mysql.jdbc;

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

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!");
        }
    }
}
2.3 数据库连接 Connection

使用JDBC连接数据库,代码如下:

 		Class.forName("com.mysql.jdbc.Driver"); // jdbc 4.0 标准可以不加
        Connection connection =DriverManager.getConnection("jdbcUriName","username","password");
        Statement state = connection .createStatement();
        state .execute("sql");
        //state .executeUpdate("sql");

DriverManager.getConnection 方法用所有注册的Driver实现类循环遍历去连接,返回成功的那个,我们自己new Driver( host,user,password) 去连接也是可以成功的。

三、打破双亲委派机制

已JDBC为例谈双亲委派模式的破坏

3.1 不破坏双亲委派模式的情况(不使用JDNI服务)

调用 Class.forName("**")JDBC 4.0以前,没有使用SPI,不会破坏

3.2 破坏双亲委派模式的情况

在使用SPI模式的情况下

  • 从META-INF/services/java.sql.Driver文件中获取具体的实现类名“com.mysql.jdbc.Driver”
  • 加载这个类,这里肯定只能用class.forName(“com.mysql.jdbc.Driver”)来加载

Class.forName()加载用的是调用者的Classloader,这个调用者DriverManager是在rt.jar中的,ClassLoader是启动类加载器,而com.mysql.jdbc.Driver肯定不在<JAVA_HOME>/lib下,所以肯定是无法加载mysql中的这个类的。这就是双亲委派模型的局限性了,父级加载器无法加载子级类加载器路径中的类

那么我们只要在启动类加载器中有方法获取应用程序类加载器,然后通过它去加载就可以了。这就是所谓的线程上下文加载器
线程上下文类加载器可以通过Thread.setContextClassLoaser()方法设置,如果不特殊设置会从父类继承,一般默认使用的是应用程序类加载器

很明显,线程上下文类加载器让父级类加载器能通过调用子级类加载器来加载类,这打破了双亲委派模型的原则

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

nextService 中调用刚才传递的线程上下文类加载器加载

3.3 类加载机制

JVM加载器是否可以加载自定义的String
Class.forName()和ClassLoader.loadClass()区别

  • Class.forName():初将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块
  • ClassLoader.loadClass(): 只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。
  • Class.forName(name,initialize,loader) 带参函数也可控制是否加载static块。并且只有调用了newInstance()方法采用调用构造函数,创建类的对象

四、statement

Statement详细用法以及解释
更详细解析

4.1 常用方法
  • executeQuery
    用于产生单个结果集的语句
  • executeUpdate
    用于执行 INSERT、UPDATE 或 DELETE 语句以及 SQLDDL(数据定义语言)语句,例如 CREATE TABLE 和 DROP TABLE。INSERT、UPDATE 或 DELETE语句的效果是修改表中零行或多行中的一列或多列。executeUpdate 的返回值是一个整数,指示受影响的行数(即更新计数)对于CREATE TABLE 或 DROP TABLE 等不操作行的语句,executeUpdate 的返回值总为零
  • execute
    用于执行返回多个结果集、多个更新计数或二者组合的语句。因为多数程序员不会需要该高级功能,所以本概述后面将在单独一节中对其进行介绍
4.2 注意事项

执行语句的所有方法都将关闭所调用的 Statement 对象的当前打开结果集(如果存在)。这意味着在重新执行 Statement对象之前,需要完成对当前 ResultSet 对象的处理。

应注意,继承了 Statement 接口中所有方法的 PreparedStatement 接口都有自己的executeQuery、executeUpdate 和 execute 方法。Statement 对象本身不包含 SQL语句,因而必须给 Statement.execute 方法提供 SQL 语句作为参数。PreparedStatement 对象并 不将SQL 语句作为参数提供给这些方法,因为它们已经包含预编译 SQL 语句。CallableStatement 对象继承这些方法的PreparedStatement 形式。对于这些方法的 PreparedStatement 或 CallableStatement版本,使用查询参数将抛出 SQLException。

4.3 PreparedStatement CallableStatement

实际上有三种 Statement 对象,它们都作为在给定连接上执行 SQL语句的包容器:Statement、PreparedStatement(它从 Statement 继承而来)和CallableStatement(它从 PreparedStatement 继承而来)

它们都专用于发送特定类型的 SQL 语句:

  • Statement 对象用于执行不带参数的简单 SQL 语句
  • PreparedStatement 对象用于执行带或不带 IN参数预编译 SQL 语句;
  • CallableStatement 对象用于执行对数据库已存储过程的调用

PreparedStatement 接口添加了处理 IN 参数的方法;而CallableStatement 添加了处理 OUT 参数的方法。

PreparedStatement预编译的,使用PreparedStatement有几个好处

  1. 在执行可变参数的一条SQL时,PreparedStatement比Statement的效率高,因为DBMS预编译一条SQL当然会比多次编译一条SQL的效率要高。
  2. 安全性好,有效防止Sql注入等问题。
  3. 对于多次重复执行的语句,使用PreparedStament效率会更高一点,并且在这种情况下也比较适合使用batch;
  4. 代码的可读性和可维护性。
4.4 普通、流式、游标查询

https://www.jianshu.com/p/c7c5dbe63019

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值