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); } } }
上面的代码主要步骤是:
- 从系统变量中获取驱动的实现类。
- 利用 SPI 来获取所有驱动的实现类。
- 遍历所有驱动,尝试实例化各个实现类。
- 根据第 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 类的实例是不安全的。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战