JAVA基礎|從Class.forName初始化數據庫到SPI破壞雙親委托機制


代碼托管在: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();
//    }


}

看到兩次位置的對比,你基本就應該可以猜到發生問題的原因了,這里就不說明了


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM