探秘類載入器和類載入機制

来源:https://www.cnblogs.com/jasongan/archive/2018/12/20/10151681.html
-Advertisement-
Play Games

在面向對象編程實踐中,我們通過眾多的類來組織一個複雜的系統,這些類之間相互關聯、調用使他們的關係形成了一個複雜緊密的網路。當系統啟動時,出於性能、資源利用多方面的考慮,我們不可能要求 JVM 一次性將全部的類都載入完成,而是只載入能夠支持系統順利啟動和運行的類和資源即可。那麼在系統運行過程中如果需要 ...


在面向對象編程實踐中,我們通過眾多的類來組織一個複雜的系統,這些類之間相互關聯、調用使他們的關係形成了一個複雜緊密的網路。當系統啟動時,出於性能、資源利用多方面的考慮,我們不可能要求 JVM 一次性將全部的類都載入完成,而是只載入能夠支持系統順利啟動和運行的類和資源即可。那麼在系統運行過程中如果需要使用未在啟動時載入的類或資源時該怎麼辦呢?這就要靠類載入器來完成了。

什麼是類載入器

類載入器(ClassLoader)就是在系統運行過程中動態的將位元組碼文件載入到 JVM 中的工具,基於這個工具的整套類載入流程,我們稱作類載入機制。我們在 IDE 中編寫的都是源代碼文件,以尾碼名 .java 的文件形式存在於磁碟上,通過編譯後生成尾碼名 .class 的位元組碼文件,ClassLoader 載入的就是這些位元組碼文件。

有哪些類載入器

Java 預設提供了三個 ClassLoader,分別是 AppClassLoader、ExtClassLoader、BootStrapClassLoader,依次後者分別是前者的「父載入器」。父載入器不是「父類」,三者之間沒有繼承關係,只是因為類載入的流程使三者之間形成了父子關係,下文會詳細講述。

BootStrapClassLoader

BootStrapClassLoader 也叫「根載入器」,它是脫離 Java 語言,使用 C/C++ 編寫的類載入器,所以當你嘗試使用 ExtClassLoader 的實例調用 getParent() 方法獲取其父載入器時會得到一個 null 值。

// 返回一個 AppClassLoader 的實例
ClassLoader appClassLoader = this.getClass().getClassLoader();
// 返回一個 ExtClassLoader 的實例
ClassLoader extClassLoader = appClassLoader.getParent();
// 返回 null,因為 BootStrapClassLoader 是 C/C++ 編寫的,無法在 Java 中獲得其實例
ClassLoader bootstrapClassLoader = extClassLoader.getParent();

根載入器會預設載入系統變數 sun.boot.class.path 指定的類庫(jar 文件和 .class 文件),預設是 $JRE_HOME/lib 下的類庫,如 rt.jar、resources.jar 等,具體可以輸出該環境變數的值來查看。

String bootClassPath = System.getProperty("sun.boot.class.path");
String[] paths = bootClassPath.split(":");
for (String path : paths) {
    System.out.println(path);
}

// output
// /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/resources.jar
// /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/rt.jar
// /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/sunrsasign.jar
// /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/jsse.jar
// /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/jce.jar
// /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/charsets.jar
// /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/jfr.jar
// /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/classes

除了載入這些預設的類庫外,也可以使用 JVM 參數 -Xbootclasspath/a 來追加額外需要讓根載入器載入的類庫。比如我們自定義一個 com.ganpengyu.boot.DateUtils 類來讓根載入器載入。

package com.ganpengyu.boot;

import java.text.SimpleDateFormat;
import java.util.Date;

public class DateUtils {
    public static void printNow() {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        System.out.println(sdf.format(new Date()));
    }
}

我們將其製作成一個名為 gpy-boot 的 jar 包放到 /Users/yu/Desktop/lib 下,然後寫一個測試類去嘗試載入 DateUtils。

public class Test {
    public static void main(String[] args) throws Exception {
        Class<?> clz = Class.forName("com.ganpengyu.boot.DateUtils");
        ClassLoader loader = clz.getClassLoader();
        System.out.println(loader == null);
    }
}

運行這個測試類:

java -Xbootclasspath/a:/Users/yu/Desktop/lib/gpy-boot.jar -cp /Users/yu/Desktop/lib/gpy-boot.jar:. Test

可以看到輸出為 true,也就是說載入 com.ganpengyu.boot.DateUtils 的類載入器在 Java 中無法獲得其引用,而任何類都必須通過類載入器載入才能被使用,所以推斷出這個類是被 BootStrapClassLoader 載入的,也證明瞭 -Xbootclasspath/a 參數確實可以追加需要被根載入器額外載入的類庫。

總之,對於 BootStrapClassLoader 這個根載入器我們需要知道三點:

  1. 根載入器使用 C/C++ 編寫,我們無法在 Java 中獲得其實例
  2. 根載入器預設載入系統變數 sun.boot.class.path 指定的類庫
  3. 可以使用 -Xbootclasspath/a 參數追加根載入器的預設載入類庫

ExtClassLoader

ExtClassLoader 也叫「擴展類載入器」,它是一個使用 Java 實現的類載入器(sun.misc.Launcher.ExtClassLoader),用於載入系統所需要的擴展類庫。預設載入系統變數 java.ext.dirs 指定位置下的類庫,通常是 $JRE_HOME/lib/ext 目錄下的類庫。

public static void main(String[] args) {
    String extClassPath = System.getProperty("java.ext.dirs");
    String[] paths = extClassPath.split(":");
    for (String path : paths) {
        System.out.println(path);
    }
}

// output
// /Users/leon/Library/Java/Extensions
// /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/ext
// /Library/Java/Extensions
// /Network/Library/Java/Extensions
// /System/Library/Java/Extensions
// /usr/lib/java

我們可以在啟動時修改java.ext.dirs 變數的值來修改擴展類載入器的預設類庫載入目錄,但通常並不建議這樣做。如果我們真的有需要擴展類載入器在啟動時載入的類庫,可以將其放置在預設的載入目錄下。總之,對於 ExtClassLoader 這個擴展類載入器我們需要知道兩點:

  1. 擴展類載入器是使用 Java 實現的類載入器,我們可以在程式中獲得它的實例並使用。
  2. 通常不建議修改java.ext.dirs 參數的值來修改預設載入目錄,如有需要,可以將要載入的類庫放到這個預設目錄下。

AppClassLoader

AppClassLoader 也叫「應用類載入器」,它和 ExtClassLoader 一樣,也是使用 Java 實現的類載入器(sun.misc.Launcher.AppClassLoader)。它的作用是載入應用程式 classpath 下所有的類庫。這是我們最常打交道的類載入器,我們在程式中調用的很多 getClassLoader() 方法返回的都是它的實例。在我們自定義類載入器時如果沒有特別指定,那麼我們自定義的類載入器的預設父載入器也是這個應用類載入器。總之,對於 AppClassLoader 這個應用類載入器我們需要知道兩點:

  1. 應用類載入器是使用 Java 實現的類載入器,負責載入應用程式 classpath 下的類庫。
  2. 應用類載入器是和我們最常打交道的類載入器。
  3. 沒有特別指定的情況下,自定義類載入器的父載入器就是應用類載入器。

自定義類載入器

除了上述三種 Java 預設提供的類載入器外,我們還可以通過繼承 java.lang.ClassLoader 來自定義一個類載入器。如果在創建自定義類載入器時沒有指定父載入器,那麼預設使用 AppClassLoader 作為父載入器。關於自定義類載入器的創建和使用,我們會在後面的章節詳細講解。

類載入器的啟動順序

上文已經提到過 BootStrapClassLoader 是一個使用 C/C++ 編寫的類載入器,它已經嵌入到了 JVM 的內核之中。當 JVM 啟動時,BootStrapClassLoader 也會隨之啟動並載入核心類庫。當核心類庫載入完成後,BootStrapClassLoader 會創建 ExtClassLoader 和 AppClassLoader 的實例,兩個 Java 實現的類載入器將會載入自己負責路徑下的類庫,這個過程我們可以在 sun.misc.Launcher 中窺見。

ExtClassLoader 的創建過程

我們將 Launcher 類的構造方法源碼精簡展示如下:

public Launcher() {
    // 創建 ExtClassLoader
    Launcher.ExtClassLoader var1;
    try {
        var1 = Launcher.ExtClassLoader.getExtClassLoader();
    } catch (IOException var10) {
        throw new InternalError("Could not create extension class loader", var10);
    }
    // 創建 AppClassLoader
    try {
        this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
    } catch (IOException var9) {
        throw new InternalError("Could not create application class loader", var9);
    }
    // 設置線程上下文類載入器
    Thread.currentThread().setContextClassLoader(this.loader);
    // 創建 SecurityManager

}

可以看到當 Launcher 被初始化時就會依次創建 ExtClassLoader 和 AppClassLoader。我們進入 getExtClassLoader() 方法並跟蹤創建流程,發現這裡又調用了 ExtClassLoader 的構造方法,在這個構造方法里調用了父類的構造方法,這便是 ExtClassLoader 創建的關鍵步驟,註意這裡傳入父類構造器的第二個參數為 null。接著我們去查看這個父類構造方法,它位於 java.net.URLClassLoader 類中:

URLClassLoader(URL[] urls, ClassLoader parent,
                          URLStreamHandlerFactory factory)

通過這個構造方法的簽名和註釋我們可以明確的知道,第二個參數 parent 表示的是當前要創建的類載入器的父載入器。結合前面我們提到的 ExtClassLoader 的父載入器是 JVM 內核中 C/C++ 開發的 BootStrapClassLoader,且無法在 Java 中獲得這個類載入器的引用,同時每個類載入器又必然有一個父載入器,我們可以反證出,ExtClassLoader 的父載入器就是 BootStrapClassLoader。

AppClassLoader 的創建過程

理清了 ExtClassLoader 的創建過程,我們來看 AppClassLoader 的創建過程就清晰很多了。跟蹤 getAppClassLoader() 方法的調用過程,可以看到這個方法本身將 ExtClassLoader 的實例作為參數傳入,最後還是調用了 java.net.URLClassLoader 的構造方法,將 ExtClassLoader 的實例作為父構造器 parent 參數值傳入。所以這裡我們又可以確定,AppClassLoader 的父構造器就是 ExtClassLoader。

怎麼載入一個類

將一個 .class 位元組碼文件載入到 JVM 中成為一個 java.lang.Class 實例需要載入這個類的類載入器及其所有的父級載入器共同參與完成,這主要是遵循「雙親委派原則」。

雙親委派

當我們要載入一個應用程式 classpath 下的自定義類時,AppClassLoader 會首先查看自己是否已經載入過這個類,如果已經載入過則直接返回類的實例,否則將載入任務委托給自己的父載入器 ExtClassLoader。同樣,ExtClassLoader 也會先查看自己是否已經載入過這個類,如果已經載入過則直接返回類的實例,否則將載入任務委托給自己的父載入器 BootStrapClassLoader。

BootStrapClassLoader 收到類載入任務時,會首先檢查自己是否已經載入過這個類,如果已經載入則直接返回類的實例,否則在自己負責的載入路徑下搜索這個類並嘗試載入。如果找到了這個類,則執行載入任務並返回類實例,否則將載入任務交給 ExtClassLoader 去執行。

ExtClassLoader 同樣也在自己負責的載入路徑下搜索這個類並嘗試載入。如果找到了這個類,則執行載入任務並返回類實例,否則將載入任務交給 AppClassLoader 去執行。

由於自己的父載入器 ExtClassLoader 和 BootStrapClassLoader 都沒能成功載入到這個類,所以最後由 AppClassLoader 來嘗試載入。同樣,AppClassLoader 會在 classpath 下所有的類庫中查找這個類並嘗試載入。如果最後還是沒有找到這個類,則拋出 ClassNotFoundException 異常。

綜上,當類載入器要載入一個類時,如果自己曾經沒有載入過這個類,則層層向上委托給父載入器嘗試載入。對於 AppClassLoader 而言,它上面有 ExtClassLoader 和 BootStrapClassLoader,所以我們稱作「雙親委派」。但是如果我們是使用自定義類載入器來載入類,且這個自定義類載入器的預設父載入器是 AppClassLoader 時,它上面就有三個父載入器,這時再說「雙親」就不太合適了。當然,理解了載入一個類的整個流程,這些名字就無關痛癢了。

為什麼需要雙親委派機制

「雙親委派機制」最大的好處是避免自定義類和核心類庫衝突。比如我們大量使用的 java.lang.String 類,如果我們自己寫的一個 String 類被載入成功,那對於應用系統來說完全是毀滅性的破壞。我們可以嘗試著寫一個自定義的 String 類,將其包也設置為 java.lang

package java.lang;

public class String {

    private int n;

    public String(int n) {
        this.n = n;
    }

    public String toLowerCase() {
        return new String(this.n + 100);
    }

}

我們將其製作成一個 jar 包,命名為 thief-jdk,然後寫一個測試類嘗試載入 java.lang.String 並使用接收一個 int 類型參數的構造方法創建實例。

import java.lang.reflect.Constructor;

public class Test {

    public static void main(String[] args) throws Exception {
        Class<?> clz = Class.forName("java.lang.String");
        System.out.println(clz.getClassLoader() == null);
        Constructor<?> c = clz.getConstructor(int.class);
        String str = (String) c.newInstance(5);
        str.toLowerCase();
    }
}

運行測試程式

java -cp /Users/yu/Desktop/lib/thief/thief-jdk.jar:. Test

程式拋出 NoSuchMethodException 異常,因為 JVM 不能夠載入我們自定義的 java.lang.String,而是從 BootStrapClassLoader 的緩存中返回了核心類庫中的 java.lang.String 的實例,且核心類庫中的 String 沒有接收 int 類型參數的構造方法。同時我們也看到 Class 實例的類載入器是 null,這也說明瞭我們拿到的 java.lang.String 的實例確實是由 BootStrapClassLoader 載入的。

總之,「雙親委派」機制的作用就是確保類的唯一性,最直接的例子就是避免我們自定義類和核心類庫衝突。

JVM 怎麼判斷兩個類是相同的

「雙親委派」機制用來保證類的唯一性,那麼 JVM 通過什麼條件來判斷唯一性呢?其實很簡單,只要兩個類的全路徑名稱一致,且都是同一個類載入器載入,那麼就判斷這兩個類是相同的。如果同一份位元組碼被不同的兩個類載入器載入,那麼它們就不會被 JVM 判斷為同一個類。

Person 類

public class Person {
    private Person p;
    public void setPerson(Object obj) {
        this.p = (Person) obj;
    }
}

setPerson(Object obj) 方法接收一個對象,並將其強制轉換為 Person 類型賦值給變數 p。

測試類

import java.lang.reflect.Method;
public class Test {
    public static void main(String[] args) {
        CustomClassLoader classLoader1 = new CustomClassLoader("/Users/yu/Desktop/lib");
        CustomClassLoader classLoader2 = new CustomClassLoader("/Users/yu/Desktop/lib");
        try {
            Class c1 = classLoader1.findClass("Person");
            Object instance1 = c1.newInstance();

            Class c2 = classLoader2.findClass("Person");
            Object instance2 = c2.newInstance();

            Method method = c1.getDeclaredMethod("setPerson", Object.class);
            method.invoke(instance1, instance2);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

CustomClassLoader 是一個自定義的類載入器,它將位元組碼文件載入為字元數組,然後調用 ClassLoader 的 defineClass() 方法創建類的實例,後文會詳細講解怎麼自定義類載入器。在測試類中,我們創建了兩個類載入器的實例,讓他們分別去載入同一份位元組碼文件,即 Person 類的位元組碼。然後在實例一上調用 setPerson() 方法將實例二傳入,將實例二強制轉型為實例一。

運行程式會看到 JVM 拋出了 ClassCastException 異常,異常信息為 Person cannot be cast to Person。從這我們就可以知道,同一份位元組碼文件,如果使用的類載入器不同,那麼 JVM 就會判斷他們是不同的類型。

全盤負責

「全盤負責」是類載入的另一個原則。它的意思是如果類 A 是被類載入器 X 載入的,那麼在沒有顯示指定別的類載入器的情況下,類 A 引用的其他所有類都由類載入器 X 負責載入,載入過程遵循「雙親委派」原則。我們編寫兩個類來驗證「全盤負責」原則。

Worker 類

package com.ganpengyu.full;

import com.ganpengyu.boot.DateUtils;

public class Worker {

    public Worker() {
    }
    public void say() {
        DateUtils dateUtils = new DateUtils();
        System.out.println(dateUtils.getClass().getClassLoader() == null);
        dateUtils.printNow();
    }
}

DateUtils 類

package com.ganpengyu.boot;

import java.text.SimpleDateFormat;
import java.util.Date;

public class DateUtils {

    public void printNow() {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        System.out.println(sdf.format(new Date()));
    }
}

測試類

import com.ganpengyu.full.Worker;
import java.lang.reflect.Constructor;
public class Test {
    public static void main(String[] args) throws Exception {
        Class<?> clz = Class.forName("com.ganpengyu.full.Worker");
        System.out.println(clz.getClassLoader() == null);
        Worker worker = (Worker) clz.newInstance();
        worker.say();
    }
}

運行測試類

java -Xbootclasspath/a:/Users/yu/Desktop/lib/worker.jar Test

運行結果

true
true
2018-09-16 22:34:43

我們將 Worker 類和 DateUtils 類製作成名為worker 的 jar 包,將其設置為由根載入器載入,這樣 Worker 類就必然是被根載入器載入的。然後在 Worker 類的 say() 方法中初始化了 DateUtils 類,然後判斷 DateUtils 類是否由根載入器載入。從運行結果看到,Worker 和其引用的 DateUtils 類都被跟載入器載入,符合類載入的「全盤委托」原則。

「全盤委托」原則實際是為「雙親委派」原則提供了保證。如果不遵守「全盤委托」原則,那麼同一份位元組碼可能會被 JVM 載入出多個不同的實例,這就會導致應用系統中對該類引用的混亂,具體可以參考上文「JVM 怎麼判斷兩個類是相同的」這一節的示例。

自定義類載入器

除了使用 JVM 預定義的三種類載入器外,Java 還允許我們自定義類載入器以讓我們系統的類載入方式更靈活。要自定義類載入器非常簡單,通常只需要三個步驟:

  1. 繼承 java.lang.ClassLoader 類,讓 JVM 知道這是一個類載入器
  2. 重寫 findClass(String name) 方法,告訴 JVM 在使用這個類載入器時應該按什麼方式去尋找 .class 文件
  3. 調用 defineClass(String name, byte[] b, int off, int len) 方法,讓 JVM 載入上一步讀取的 .class 文件
import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class CustomClassLoader extends ClassLoader {
    private String classpath;
    
    public CustomClassLoader(String classpath) {
        this.classpath = classpath;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        String classFilePath = getClassFilePath(name);
        byte[] classData = readClassFile(classFilePath);
        return defineClass(name, classData, 0, classData.length);
    }

    public String getClassFilePath(String name) {
        if (name.lastIndexOf(".") == -1) {
            return classpath + "/" + name + ".class";
        } else {
            name = name.replace(".", "/");
            return classpath + "/" + name + ".class";
        }
    }

    public byte[] readClassFile(String filepath) {
        Path path = Paths.get(filepath);
        if (!Files.exists(path)) {
            return null;
        }
        try {
            return Files.readAllBytes(path);
        } catch (IOException e) {
            throw new RuntimeException("Can not read class file into byte array");
        }
    }

    public static void main(String[] args) {
        CustomClassLoader loader = new CustomClassLoader("/Users/leon/Desktop/lib");
        try {
            Class<?> clz = loader.loadClass("com.ganpengyu.demo.Person");
            System.out.println(clz.getClassLoader().toString());

            Constructor<?> c = clz.getConstructor(String.class);
            Object instance = c.newInstance("Leon");
            Method method = clz.getDeclaredMethod("say", null);
            method.invoke(instance, null);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

示例中我們通過繼承 java.lang.ClassLoader 創建了一個自定義類載入器,通過構造方法指定這個類載入器的類路徑(classpath)。重寫 findClass(String name) 方法自定義類載入的方式,其中 getClassFilePath(String filepath) 方法和 readClassFile(String filepath) 方法用於找到指定的 .class 文件並載入成一個字元數組。最後調用 defineClass(String name, byte[] b, int off, int len) 方法完成類的載入。

main() 方法中我們測試載入了一個 Person 類,通過 loadClass(String name) 方法載入一個 Person 類。我們自定義的 findClass(String name) 方法,就是在這裡面調用的,我們把這個方法精簡展示如下:

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
        // 先檢查是否已經載入過這個類
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                // 否則的話遞歸調用父載入器嘗試載入
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    // 所有父載入器都無法載入,使用根載入器嘗試載入
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {}
            if (c == null) {
                // 所有父載入器和根載入器都無法載入
                // 使用自定義的 findClass() 方法查找 .class 文件
                c = findClass(name);
            }
        }
        return c;
    }
}

可以看到 loadClass(String name) 方法內部是遵循「雙親委派」機制來完成類的載入。在「雙親」都沒能成功載入類的情況下才調用我們自定義的 findClass(String name) 方法查找目標類執行載入。

為什麼需要自定義類載入器

自定義類載入器的用處有很多,這裡簡單列舉一些常見的場景。

  1. 從任意位置載入類。JVM 預定義的三個類載入器都被限定了自己的類路徑,我們可以通過自定義類載入器去載入其他任意位置的類。
  2. 解密類文件。比如我們可以對編譯後的類文件進行加密,然後通過自定義類載入器進行解密。當然這種方法實際並沒有太大的用處,因為自定義的類載入器也可以被反編譯。
  3. 支持更靈活的記憶體管理。我們可以使用自定義類載入器在運行時卸載已載入的類,從而更高效的利用記憶體。

就這樣吧

類載入器是 Java 中非常核心的技術,本文僅對類載入器進行了較為粗淺的分析,如果需要深入更底層則需要我們打開 JVM 的源碼進行研讀。「Java 有路勤為徑,JVM 無涯苦作舟」,與君共勉。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1.反射 主要是用到了4個函數( 用的最多的就是getattr()和 hasattr() ): getattr() 從xxx對象中獲取到xxx屬性值 hasattr() 判斷xxx對象中是否有xxx屬性值delattr() 從xxx對象中刪除xxx屬性setattr() 設置xxx對象中的xxx屬性 ...
  • https://docs.scipy.org/doc/scipy/reference/interpolate.html#module-scipy.interpolate https://stackoverflow.com/questions/31464345/fitting-a-closed-cur ...
  • python爬蟲+數據可視化項目(一) 爬取目標:中國天氣網(起始url:http://www.weather.com.cn/textFC/hb.shtml#) 爬取內容:全國實時溫度最低的十個城市氣溫排行榜 使用工具:requests庫實現發送請求、獲取響應。 beautifulsoup實現數據解 ...
  • 例題import lxml.html test_data = """ <div> <ul> <li class="item-0"><a href="link1.html" id="places_neighbours__row">9,596,960first item</a></li> <li cla ...
  • sql註入後可以通過該資料庫獲取所有表的欄位信息 1. COLLATIONS表 提供有關每個字元集的排序規則的信息。 COLLATIONS表包含以下列: COLLATION_NAME 排序規則名稱。 CHARACTER_SET_NAME 與排序規則關聯的字元集的名稱。 ID 排序規則ID。 IS_D... ...
  • 在理解有向圖和強連通分量前必須理解與其對應的兩個概念,連通圖(無向圖)和連通分量。 連通圖的定義是:如果一個圖中的任何一個節點可以到達其他節點,那麼它就是連通的。 例如以下圖形: 這是最簡單的一個連通圖,即使它並不閉合。由於節點間的路徑是沒有方向的,符合從任意一個節點出發,都可以到達其他剩餘的節點這 ...
  • 1. isinstance, type, issubclass isinstance: 判斷你給對象是否是xx類型的. (向上判斷 type: 返回xxx對象的數據類型 issubclass: 判斷xxx類是否xxx的子類 2. 如何區分方法和函數 在類中: 實例方法 如果是類名.方法 函數 如果是 ...
  • 1. isinstance, type, issubclass的區別 2. 反射 主要是用到了4個函數( 用的最多的就是getattr()和 hasattr() ): getattr() 從xxx對象中獲取到xxx屬性值 hasattr() 判斷xxx對象中是否有xxx屬性值 delattr() 從 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...