随笔 - 171  文章 - 0  评论 - 0  阅读 - 61962

SPI与双亲委派模型

类加载顺序图:

 这样做的好处就是:Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.object的类,并放在程序的ClassPath中,那系统中将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱。

其次是考虑到安全因素。假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。

JDBC为何要破坏双亲委派模型?

在JDBC 4.0后,使用数据库驱动程序,我们不在需要Class.forName来加载驱动程序,只需要把驱动的jar包放到工程的类加载路径中,驱动程序就会自动被加载。这得益于Java中的SPI技术,只需要将加载的类名称放在META-INF/services目录下的java.sql.Driver文件中,即可自动进行加载。

在使用上,我们只需下面一行代码就可以创建数据库连接。
Connection con = DriverManager.getConnection(url , username , password ) ;
因为类加载器收到加载范围的限制,在某些情况下父加载器无法加载到需要的文件,就需要委托给子类加载器去加载class文件。
JDBC的Driver接口定义在JDK中,其实现由数据库的各个厂商提供。DriverManager类中要加载各个实现了Driver接口的类统一进行管理,Driver类位于JAVA_HOME中jre/lib/rt.jar中,应该由Bootstrap类加载器进行加载,而各个Driver的实现类位于各个服务商提供
的jar包中。根据类加载机制,当被加载的类引用了另外一个类时,虚拟机就会使用装载第一个类(先加载父类)的类加载器装载被引用的类,也就是说应该使用Bootstrap类加载器去加载各个厂商提供的Driver类。但是,Bootstrap类加载器只负责加载JAVA_HOME中jre/lib/rt.jar中所有的class,所以需要由子类加载器去加载Driver的实现类,这就破坏了双亲委派模型。
查看DriverManager类的源码,看到在使用DriverMnager的时候会触发器静态代码块,调用loadInitialDrivers()方法,并调用ServiceLoader.load(Driver.class)加载所有在META-INF/Services/java.sql.Driver文件中的类到JVM内存,完成驱动的自动加载。
复制代码
static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
}
private static void loadInitialDrivers() {
  String drivers;
  try {
    drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
      public String run() {
        return System.getProperty("jdbc.drivers");
      }
    });
  } catch (Exception ex) {
    drivers = null;
  }
  // 通过 classloader 获取所有实现 java.sql.Driver 的驱动类
  AccessController.doPrivileged(new PrivilegedAction<Void>() {
    public Void run() {
            // 利用 SPI,记载所有 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;
    }
  });

    // 打印数据库驱动信息
  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 (Exception ex) {
      println("DriverManager.Initialize: load failed: " + ex);
    }
  }
}
复制代码

上面的代码主要步骤是:

  1. 从系统变量中获取驱动的实现类。
  2. 利用 SPI 来获取所有驱动的实现类。
  3. 遍历所有驱动,尝试实例化各个实现类。
  4. 根据第 1 步获取到的驱动列表来实例化具体的实现类。
子类的类加载器是通过Thread.currentThread().getContextClassLoader()得到的线程上下文加载器,如果不做任何的设置,Java 应用的线程的上下文类加载器默认就是 AppClassLoader。
public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
}

Java SPI 的不足

Java SPI 存在一些不足:

  • 不能按需加载,需要遍历所有的实现,并实例化,然后在循环中才能找到我们需要的实现。如果不想用某些实现类,或者某些类实例化很耗时,它也被载入并实例化了,这就造成了浪费。
  • 获取某个实现类的方式不够灵活,只能通过 Iterator 形式获取,不能根据某个参数来获取对应的实现类。
  • 多个并发多线程使用 ServiceLoader 类的实例是不安全的。
posted on   zhengbiyu  阅读(51)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
< 2025年2月 >
26 27 28 29 30 31 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 1
2 3 4 5 6 7 8

点击右上角即可分享
微信分享提示