代码托管在: https://github.com/fabe2ry/classloaderDemo
初始化数据库
如果你写过操作数据库的程序的话, 可能会注意, 有的代码会在程序的开头, 有 Class.forName("com.mysql.jdbc.Driver"); 的代码, 并且告诉你这是在进行数据库的初始化, 注册 jdbc 的驱动; 但是其实如果你去掉这段代码, 并不会影响程序的正常运行, 当然这是需要在 JDK6 之后才行这样
- import java.sql.*;
- public class MySQLDemo {
- // JDBC 驱动名及数据库 URL
- static final String JDBC_DRIVER = "com.mysql.jdbc.Driver";
- static final String DB_URL = "jdbc:mysql://localhost:3306/RUNOOB";
- // 数据库的用户名与密码, 需要根据自己的设置
- static final String USER = "root";
- static final String PASS = "123456";
- public static void main(String[] args) {
- Connection conn = null;
- Statement stmt = null;
- try{
- // 注册 JDBC 驱动
- Class.forName("com.mysql.jdbc.Driver");
- // 打开链接
- System.out.println("连接数据库...");
- conn = DriverManager.getConnection(DB_URL,USER,PASS);
- // 执行查询
- System.out.println("实例化 Statement 对象...");
- stmt = conn.createStatement();
- String sql;
- sql = "SELECT id, name, url FROM websites";
- ResultSet rs = stmt.executeQuery(sql);
- // 展开结果集数据库
- while(rs.next()){
- // 通过字段检索
- int id = rs.getInt("id");
- String name = rs.getString("name");
- String url = rs.getString("url");
- // 输出数据
- System.out.print("ID:" + id);
- System.out.print(", 站点名称:" + name);
- System.out.print(", 站点 URL:" + url);
- System.out.print("\n");
- }
- // 完成后关闭
- rs.close();
- stmt.close();
- conn.close();
- }catch(SQLException se){
- // 处理 JDBC 错误
- se.printStackTrace();
- }catch(Exception e){
- // 处理 Class.forName 错误
- e.printStackTrace();
- }finally{
- // 关闭资源
- try{
- if(stmt!=null) stmt.close();
- }catch(SQLException se2){
- }// 什么都不做
- try{
- if(conn!=null) conn.close();
- }catch(SQLException se){
- se.printStackTrace();
- }
- }
- System.out.println("Goodbye!");
- }
- }
- com.MySQL.jdbc.Driver
首先我们要知道 Class.forName()与 ClassLoader.loadClass()的区别, 二者都可以返回一个类对象
Class.forName()根据重载形式的不同, 分别为 public Class forName(String name)来初始化类, 根据 public Class forName(String name, boolean init, ClassLoader classLoader); 来选择对于的 classloader 进行加载, 并且是否需要初始化; 二者没有互相调用的关系
而 ClassLoader.loadCLass()根据重载形式的不同, 分别为 public CLass loadClass(String name); 和 protect Class ClassLoader(String name, boolean reslove);, 前者是我们调用加载器的方法, 后者则是我们应该继承重写的方法, 方法对应的第二个参数的意思是是否需要解析, 这个解析就是我们在类加载机制中的一个环节了; 二者的关系是前者默认调用带 false 常数的后者
知道了这个区别之后, 就应该了解使用 Class.forName 是希望完成类从加载, 连接 (包括验证, 准备和解析) 以及初始化的全过程, 但是代码之后也没有使用过这个方法加载出来的类对象, 说明使用这个方法, 目的就是完成类的初始化, 所以查看一下 com.MySQL.jdbc.Driver 这个类的实现
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) // package com.MySQL.cj.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!"); } } }
可以看到 static 代码块里有初始化的操作, 使用这种方式初始化, 可以保证初始化一次(jvm 加载类的时候, 默认会进行加锁同步, 避免多线程下加载多个一样的类, 自然也只有一次初始化操作)
为什么需要 SPI(service provider interface)
首先我们可以看到如果没有引入 spi, 我们必须显示的调用 Class.forName()来调用数据库初始化的操作, 并且这种操作, 有以下的问题
使用 String 的字面值, 存在可能写错的情况, 毕竟不是强类型的操作, 像 idea 这些编译器也不能提前发现, 只有程序运行才能检测出了
需要硬编码到程序中, 如果我更改了另一个数据库的驱动, 需要修改到代码, 你可能会说这只是改动一下, 没什么关系, 但是如果很蛋疼的是, 你实际项目中, 可能测试环境用一种驱动, 生成环境用另一个驱动, 你这会不就需要重复更改代码了么, 而且更改代码还意味着需要重写编译, 当项目很大的时候, 这么做就会浪费很长的时间了
那么有没有一种方法, 只需要我们引入了某个驱动的 jar 包, 程序就知道自动加载驱动, 也就是帮我们根据 jar 包来调用 Class.forName()的操作呢
有, 这就是 spi 的作用, 下面我们通过一个例子, 写一个自己的 API 接口, 并且另外写两个 jar 包, 分别提供不同的 API 接口的实现, 使用 spi 来, 帮助我们达到我们自动初始化的目的
实现 spi
我们先在项目一中新建一个接口 Angle, 并且写一个 AngleManager 管理类, 这个类保存着我们的实现类, 实现类需要向该类注册; 再新建项目二与三, 分别实现接口, 并且打包成为 jar 包, 同样, 因为实现接口前, 必须知道接口是啥, 我们使用 maven 管理 jar 包, 同时在项目二和三把项目一的 jar 给引入; 最后, 我们在项目四, 引入项目一, 并且根据需求, 引入项目二或者项目三, 来进行测试
项目一
Angle.java
package API; public interface Angle { void love(String singleDog); void hate(String coupleDog); }
AngleManager.java
package API; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.ServiceLoader; public class AngleManager { private static int angleIndex = 0; private static List<Angle> angles = new ArrayList<Angle>(); /** * 提供注册功能 * @param angle */ public static void registerAngle(Angle angle){ angles.add(angle); } /** * 获取一个接口实现 * @return */ public static Angle angleFall(){ if(angles.size()> 0 && angleIndex <angles.size()){ return angles.get(angleIndex ++); } return null; } /** * 提供初始化操作, 里面使用 spi, 来发现第三方的接口实现 */ private static void angleManagerInit() { ServiceLoader<Angle> angleServiceLoader = ServiceLoader.load(Angle.class); Iterator<Angle> angleIterator = angleServiceLoader.iterator(); while(angleIterator.hasNext()) { // 这里会调用 Class.forName(name, init, classloader); angleIterator.next(); } } static { System.out.println("angleManagerInit"); angleManagerInit(); } }
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.fabe2ry</groupId> <artifactId>paradise</artifactId> <version>1.0-SNAPSHOT</version> </project>
写完 install 下, 发布到本地仓库, 给后面项目引入
项目二
FireAngle.java
package impl; import API.Angle; import API.AngleManager; public class FireAngle implements Angle { static { // 自定义的初始化操作 System.out.println("i am fire angle, i init"); AngleManager.registerAngle(new FireAngle()); } public void love(String singleDog) { System.out.println("single dog is happy, very very happy"); } public void hate(String coupleDog) { System.out.println("Burning coupleDog"); } }
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.fabe2ry</groupId> <artifactId>fire</artifactId> <version>1.0-SNAPSHOT</version> <dependencies> <dependency> <groupId>com.fabe2ry</groupId> <artifactId>paradise</artifactId> <version>1.0-SNAPSHOT</version> </dependency> </dependencies> </project>
同时为了使用 SPI, 我们还需要遵守规范, 在 resource 文件下, 新建 META-INF/services 文件夹, 在下面以接口名称命名一个文本文件, 在文件内写入接口实现类的全限定名称
项目三
项目三就只贴实现类了
Lucifer.java
package hell; import API.Angle; import API.AngleManager; public class Lucifer implements Angle { static { // 自定义的初始化操作 System.out.println("i am lucifer, i init"); AngleManager.registerAngle(new Lucifer()); } public void love(String s) { System.out.println("Lucifer love single dog"); } public void hate(String s) { System.out.println("Lucifer hate couple dog"); } }
现在同样将项目二和三给 install 一下, 发布到本地仓库
项目四
TestMain.java
import API.Angle; import API.AngleManager; public class TestMain { public static void main(String[] args) { Angle who = AngleManager.angleFall(); who.love("zxzhang"); who.hate("tr3eee"); } }
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.fabe2ry</groupId> <artifactId>world</artifactId> <version>1.0-SNAPSHOT</version> <dependencies> <dependency> <groupId>com.fabe2ry</groupId> <artifactId>paradise</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>com.fabe2ry</groupId> <artifactId>fire</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <!-- 注释掉 --> <!--<dependency>--> <!--<groupId>com.fabe2ry</groupId>--> <!--<artifactId>hell</artifactId>--> <!--<version>1.0-SNAPSHOT</version>--> <!--</dependency>--> </dependencies> </project>
当项目运行后
修改 pom 文件, 引入项目三的 jar 包
可以看到我们全程面对接口编程, 在没有修改代码的情况下, 就更改的代码的实现, 可以说一种控制反转了吧, 同时第三方开发 API 接口实现类, 需要做的初始化操作, 全部通过静态代码块的方式执行了, 用户完全不用参与
破坏双亲委托机制
明白了 SPI 的作用后, 再来看看为什么说 SPI 会破坏双亲委托机制呢
类加载器分工
当一个类 (A 类) 使用到另一个类 (B 类) 的时候, 被使用到的类 (B 类) 如果没有被加载, 这时候, 应该由哪个类加载器来加载这个类呢? 结论是由使用类 (A 类) 的类加载器, 下面我们用代码验证一下
我们自定义一个类加载器 MyClassLoader, 这个类加载负责加载 D 盘下的 class 文件(不再 classpath 底下), 同时我们定义 A 和 B 类, 在 A 类中引用 B 类, 然后看看 B 是会被哪个类加载器加载
Entry.java
import java.lang.reflect.Method; public class Entry { public static void main(String[] args) throws Exception{ MyClassLoader secondClassLoader = new MyClassLoader(); Class aClazz = Class.forName("test.AClass", true, secondClassLoader); System.out.println("!!!!"); Object a = aClazz.newInstance(); System.out.println("!!!!"); Method printMethod = aClazz.getMethod("print"); printMethod.invoke(a); } }
A.java
package test; public class AClass { static { System.out.println("AClass init"); } private BClass b; public AClass(){ b = new BClass(); } public void print(){ System.out.println(this.getClass().getClassLoader().getClass().getName()); System.out.println(b.getClass().getClassLoader().getClass().getName()); } }
B.java
package test; public class BClass { static { System.out.println("BClass init"); } }
MyClassLoader.java
import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.nio.ByteBuffer; import java.nio.channels.Channels; import java.nio.channels.FileChannel; import java.nio.channels.WritableByteChannel; public class MyClassLoader extends ClassLoader{ @Override public Class<?> loadClass(String name) throws ClassNotFoundException { System.out.println("my class loader load class:" + name); File file = getClassFile(name); try { byte[] bytes = getClassBytes(file); Class<?> c = this.defineClass(name, bytes, 0, bytes.length); return c; }catch (Exception e){ } // 这里不用这个的话, 会出现加载问题 return super.loadClass(name); } private File getClassFile(String name){ name = name.substring(name.lastIndexOf('.') + 1); File file = new File("D:/" + name + ".class"); return file; } private byte[] getClassBytes(File file) throws Exception{ // 这里要读入. class 的字节, 因此要使用字节流 FileInputStream fis = new FileInputStream(file); FileChannel fc = fis.getChannel(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); WritableByteChannel wbc = Channels.newChannel(baos); ByteBuffer by = ByteBuffer.allocate(1024); while (true) { int i = fc.read(by); if (i == 0 || i == -1) break; by.flip(); wbc.write(by); by.clear(); } fis.close(); return baos.toByteArray(); } }
这个加载器会加载 D 盘下的 class 文件, 如果找不到才会交给父加载器加载, 显然是不遵守双亲委托机制的; 对于为什么需要调用 super.loadClass(name)这个方法的, 需要知道, 对于类的解析过程, 在这个过程中, 会将符号引用转换为直接引用, 对于类和接口的解析过程, 是需要将递归解析父类的, 如果父类没有进行加载, 就会加载父类, 如果这里我们在 D 盘找不到, 就返回 null 的话, 然后程序在解析的过程中就就会运行不起来, 因为所有类的父类 Object 这个类加载器是加载不到的, 所以必须调用 super.loadClass(name)
错误示范 (将 super.loadClass(name) 改为 null)
到这里, 其实已经可以证明我们的观点了, Object 被 AClass 的类加载器引用, 而不是使用应用程序加载器
继续原来的步骤
我们修改会类加载器的代码, 让它在找不到的时候, 在委托给父类查找, 保证程序正常运行
同时, 我们手动编译 AClass.java 和 BClass.java, 将 class 文件放入 D 盘(当然你也可以在 idea 里面写好 AClass 和 BClass, 然后运行一下, 可以在 target 目录下找到编译的 class 文件, 就不用手动编译了)
现在运行代码
了解 SPI 的实现过程
现在我们明白了一个类使用到另一个的类的时候, 会用自己的类加载器去加载该类, 那么就不难理解 SPI 破坏双亲委托机制了; 不过先来了解一下, SPI 做了什么
可以看到我们之前是通过以下代码, 来实现 SPI 的功能的
// 导入类 import java.util.ServiceLoader; /** * 提供初始化操作, 里面使用 spi, 来发现第三方的接口实现 */ private static void angleManagerInit() { ServiceLoader<Angle> angleServiceLoader = ServiceLoader.load(Angle.class); Iterator<Angle> angleIterator = angleServiceLoader.iterator(); while(angleIterator.hasNext()) { // 这里会调用 Class.forName(name, init, classloader); angleIterator.next(); } }
通过打断点, 调试, 可以发现在 angleIterator.next(); 的时候, 会进入到 ServiceLoader 的匿名内部类 Iterator
public S next() { if (knownProviders.hasNext()) return knownProviders.next().getValue(); return lookupIterator.next(); }
正常情况下, 没有加载过, 就会到 lookupIterator.next(); 这个方法也是进入 ServiceLoader 的另一个内部类, 最终会跳转到下面, 完成类的加载
private S nextService() { if (!hasNextService()) throw new NoSuchElementException(); String cn = nextName; nextName = null; Class<?> c = null; try { c = Class.forName(cn, false, loader); } catch (ClassNotFoundException x) { fail(service, "Provider" + cn + "not found"); } if (!service.isAssignableFrom(c)) { fail(service, "Provider" + cn + "not a subtype"); } try { S p = service.cast(c.newInstance()); providers.put(cn, p); return p; } catch (Throwable x) { fail(service, "Provider" + cn + "could not be instantiated", x); } throw new Error(); // This cannot happen }
可以看到使用了 c = Class.forName(cn, false, loader); 来进行类的加载, 并且实例化了该类 S p = service.cast(c.newInstance());, 并且加入了缓存中; 这里调用的 loader 是哪里来的呢?
在 ServiceLoaderangleServiceLoader = ServiceLoader.load(Angle.class); 过程, 除了设置了对于 API 接口, 其实也就是对于我们 META-INF/services 底下的文件名称, 还在函数内部获取了线程上下文类加载器, 并设置为了 loader
public static <S> ServiceLoader<S> load(Class<S> service) { ClassLoader cl = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl); }
结合之前的结论
到这里, 基本就清楚了整个流程, 首先, ServiceLoader 是一个基础类, 因为它所在的包名称 java.util.ServiceLoader; 这个类是使用 Bootstrap classloader 来加载的, 你可以在代码中使用获取它的类加载器, 会出现空指针, 因为 Bootstrap classloader 是 c++ 实现的, java 中获取不到这个对象
ServiceLoader.class.getClassLoader().getClass().getName()
而结合我们之前得出的结论, 一个类的加载会被使用它的类所属的类价值器加载的话, 那么 ServiceLoader 使用 Class.forName(name)来加载类对象, 而不是 Class.forName(cn, false, loader)指定类加载器加载对象的话, 那么就会出现无法找到类对象的问题, 因为 Bootstrap classloader 找的路径是 JDK\jre\lib, 所以就需要使用线程上下文类加载器, 通过线程先获取到当前的类加载器, 这个加载器具体在什么时候设置进入的话, 暂时不清楚, 但是可以确定如果没有通过 Thread.currentThread().setContextClassLoader(); 去修改过的话, 那么这个类加载器, 会是应用程序加载器(application classloader), 接下来, 如果你的实现类在 classpath(引入 jar 就会包含在这里), 就可以被正常加载
回顾一下
在《深入理解 JVM》这本书中, 提过第二次破环是该双亲委托模弊端引起的
一个例子: JNDI 服务, 它的代码由启动类加载器加载(在 rt.jar 中), 但 JNDI 目的就是对整个程序的资源进行几种管理和查找, 需要调用由每个不同独立厂商实现并且部署在应用程序的 ClassPath 下的 JNDI 接口提供者的代码. 但是在应用启动时候读取 rt.jar 包时候, 是不认识这些三方厂商定义的类的, 那么如何解决?
java 设计团队引入了一个新设计: 线程上下文类加载器 (Thread Context ClassLoader). 这个类加载器可以通过 java.lang.Thread 类的 setContextClassLoader() 方法进行设置, 如果创建线程时候, 还未设置, 将会从父线程中继承一个. 如果在应用程序全局范围都没有设置, 默认是 appClassLoader 类加载器.
这次破坏双亲委托并不是通过修改了 loadClass 方式, 而是说对于类加载的行为, 我们不是让其使用默认的类加载器, 而是显示的指定类加载器加载, 并且这个类加载器通过线程上下文加载器来传递的
无关的几个问题
是否可以实现一个自己的 java.lang.String 类
你可以看到网络上的回答是可能可以, 打破类双亲委托的机制, 是可能可以进行加载
首先我们看看如果可以加载进入会发生什么问题
首先重复加载了 String 类, 虽然这个类是不一样的实现
假设可以加载进去, 那么在类加载过程中, 解析的时候, 如何将符号引用转化为正确的直接应用呢, 现在堆里面有两个全限定名称都堆 java.lang.String 的类对象, 应该指向那一个, 而且在进行方法的动态绑定的过程中, 自己实现的 String 类没有对应的方法, 就会出现程序异常, 不能正常运行了
这么严重的问题, 显然是不可能让它发生的, 那么 java 是怎么避免这些问题发生呢
java 在加载以 java. 或者 javax. 开头的类, 是不让你命名的, 如果你这么命名, 你编译能过, 但是你的这个代码是不由 Bootstrap classloader 加载的, 其他类加载器会对这个命名进行检测, 抛出异常 java.lang.SecurityException: Prohibited package name
结论就应该是不可以, 其实类加载也只是仅仅只能控制类加载过程个一部分, 类加载过程中加载的部分可以细分分为 3 步:
通过一个类的全限定名来获取其定义的二进制字节流.
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构.
在 Java 堆中生成一个代表这个类的 java.lang.Class 对象, 作为对方法区中这些数据的访问入口.
而 classloader.loadClass 只可以控制第一点, 后面两点都是通过调用 defineClass 来达成的, 这个方法里面, 就有对上面提到的包名的检测, 并且它最终是调用 native 方法来实现的, 你不能跳过它
下面的程序是否可以正常运行
这个我在写 demo 的时候, 发生的一个问题, 将 AngleManager 代码修改成如下, 然后重新打包, 运行程序, 就会出现以下错误和空指针的问题
Exception in thread "main" java.util.ServiceConfigurationError: API.Angle: Provider hell.Lucifer could not be instantiated
代码如下
package API; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.ServiceLoader; public class AngleManager { // 新位置 static { System.out.println("angleManagerInit"); angleManagerInit(); } private static int angleIndex = 0; private static List<Angle> angles = new ArrayList<Angle>(); /** * 提供注册功能 * @param angle */ public static void registerAngle(Angle angle){ angles.add(angle); } /** * 获取一个接口实现 * @return */ public static Angle angleFall(){ if(angles.size()> 0 && angleIndex <angles.size()){ return angles.get(angleIndex ++); } return null; } /** * 提供初始化操作, 里面使用 spi, 来发现第三方的接口实现 */ private static void angleManagerInit() { ServiceLoader<Angle> angleServiceLoader = ServiceLoader.load(Angle.class); Iterator<Angle> angleIterator = angleServiceLoader.iterator(); while(angleIterator.hasNext()) { // 这里会调用 Class.forName(name, init, classloader); angleIterator.next(); } } // 旧位置 // static { // System.out.println("angleManagerInit"); // angleManagerInit(); // } }
看到两次位置的对比, 你基本就应该可以猜到发生问题的原因了, 这里就不说明了
来源: https://www.cnblogs.com/faberry/p/10594002.html