代码托管在: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是哪里来的呢?

在ServiceLoader angleServiceLoader = 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();
//    }


}

看到两次位置的对比,你基本就应该可以猜到发生问题的原因了,这里就不说明了

版权声明:本文为faberry原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/faberry/p/10594002.html