電腦程式的思維邏輯 (87) - 類載入機制

来源:http://www.cnblogs.com/swiftma/archive/2017/05/25/6901301.html
-Advertisement-
Play Games

本節探討Java中的類載入機制,利用自定義的ClassLoader實現熱部署 ...


上節,我們探討了動態代理,在前幾節中,我們多次提到了類載入器ClassLoader,本節就來詳細討論Java中的類載入機制與ClassLoader。

類載入器ClassLoader就是載入其他類的類,它負責將位元組碼文件載入到記憶體,創建Class對象。與之前介紹的反射註解、和動態代理一樣,在大部分的應用編程中,我們不太需要自己實現ClassLoader。

不過,理解類載入的機制和過程,有助於我們更好的理解之前介紹的內容,更好的理解Java。在反射一節,我們介紹過Class的靜態方法Class.forName,理解類載入器有助於我們更好的理解該方法。

ClassLoader一般是系統提供的,不需要自己實現,不過,通過創建自定義的ClassLoader,可以實現一些強大靈活的功能,比如:

  • 熱部署,在不重啟Java程式的情況下,動態替換類的實現,比如Java Web開發中的JSP技術就利用自定義的ClassLoader實現修改JSP代碼即生效,OSGI (Open Service Gateway Initiative)框架使用自定義ClassLoader實現動態更新。
  • 應用的模塊化和相互隔離,不同的ClassLoader可以載入相同的類但互相隔離、互不影響。Web應用伺服器如Tomcat利用這一點在一個程式中管理多個Web應用程式,每個Web應用使用自己的ClassLoader,這些Web應用互不幹擾。OSGI利用這一點實現了一個動態模塊化架構,每個模塊有自己的ClassLoader,不同模塊可以互不幹擾。
  • 從不同地方靈活載入,系統預設的ClassLoader一般從本地的.class文件或jar文件中載入位元組碼文件,通過自定義的ClassLoader,我們可以從共用的Web伺服器、資料庫、緩存伺服器等其他地方載入位元組碼文件。

理解自定義ClassLoader有助於我們理解這些系統程式和框架,如Tomat, JSP, OSGI,在業務需要的時候,也可以藉助自定義ClassLoader實現動態靈活的功能。

下麵,我們首先來進一步理解Java載入類的過程,理解類ClassLoader和Class.forName,介紹一個簡單的應用,然後我們探討如何實現自定義ClassLoader,演示如何利用它實現熱部署。

類載入的基本機制和過程

運行Java程式,就是執行java這個命令,指定包含main方法的完整類名,以及一個classpath,即類路徑。類路徑可以有多個,對於直接的class文件,路徑是class文件的根目錄,對於jar包,路徑是jar包的完整名稱(包括路徑和jar包名)。

Java運行時,會根據類的完全限定名尋找並載入類,尋找的方式基本就是在系統類和指定的類路徑中尋找,如果是class文件的根目錄,則直接查看是否有對應的子目錄及文件,如果是jar文件,則首先在記憶體中解壓文件,然後再查看是否有對應的類。

負責載入類的類就是類載入器,它的輸入是完全限定的類名,輸出是Class對象。類載入器不是只有一個,一般程式運行時,都會有三個:

  1. 啟動類載入器(Bootstrap ClassLoader):這個載入器是Java虛擬機實現的一部分,不是Java語言實現的,一般是C++實現的,它負責載入Java的基礎類,主要是<JAVA_HOME>/lib/rt.jar,我們日常用的Java類庫比如String, ArrayList等都位於該包內。
  2. 擴展類載入器(Extension ClassLoader):這個載入器的實現類是sun.misc.Launcher$ExtClassLoader,它負責載入Java的一些擴展類,一般是<JAVA_HOME>/lib/ext目錄中的jar包。
  3. 應用程式類載入器(Application ClassLoader):這個載入器的實現類是sun.misc.Launcher$AppClassLoader,它負責載入應用程式的類,包括自己寫的和引入的第三方法類庫,即所有在類路徑中指定的類。

這三個類載入器有一定的關係,可以認為是父子關係,Application ClassLoader的父親是Extension ClassLoader,Extension的父親是Bootstrap ClassLoader,註意不是父子繼承關係,而是父子委派關係,子ClassLoader有一個變數parent指向父ClassLoader,在子ClassLoader載入類時,一般會首先通過父ClassLoader載入,具體來說,在載入一個類時,基本過程是:

  1. 判斷是否已經載入過了,載入過了,直接返回Class對象,一個類只會被一個ClassLoader載入一次。
  2. 如果沒有被載入,先讓父ClassLoader去載入,如果載入成功,返回得到的Class對象。
  3. 在父ClassLoader沒有載入成功的前提下,自己嘗試載入類。

這個過程一般被稱為"雙親委派"模型,即優先讓父ClassLoader去載入。為什麼要先讓父ClassLoader去載入呢?這樣,可以避免Java類庫被覆蓋的問題,比如用戶程式也定義了一個類java.lang.String,通過雙親委派,java.lang.String只會被Bootstrap ClassLoader載入,避免自定義的String覆蓋Java類庫的定義。

需要瞭解的是,"雙親委派"雖然是一般模型,但也有一些例外,比如:

  • 自定義的載入順序:儘管不被建議,自定義的ClassLoader可以不遵從"雙親委派"這個約定,不過,即使不遵從,以"java"開頭的類也不能被自定義類載入器載入,這是由Java的安全機制保證的,以避免混亂。
  • 網狀載入順序:在OSGI框架中,類載入器之間的關係是一個網,每個OSGI模塊有一個類載入器,不同模塊之間可能有依賴關係,在一個模塊載入一個類時,可能是從自己模塊載入,也可能是委派給其他模塊的類載入器載入。
  • 父載入器委派給子載入器載入:典型的例子有JNDI服務(Java Naming and Directory Interface),它是Java企業級應用中的一項服務,具體我們就不介紹了。

一個程式運行時,會創建一個Application ClassLoader,在程式中用到ClassLoader的地方,如果沒有指定,一般用的都是這個ClassLoader,所以,這個ClassLoader也被稱為系統類載入器(System ClassLoader)。

下麵,我們來具體看下表示類載入器的類 - ClassLoader。

理解ClassLoader

基本用法

類ClassLoader是一個抽象類,Application ClassLoader和Extension ClassLoader的具體實現類分別是sun.misc.Launcher$AppClassLoader和sun.misc.Launcher$ExtClassLoader,Bootstrap ClassLoader不是由Java實現的,沒有對應的類。

每個Class對象都有一個方法,可以獲取實際載入它的ClassLoader,方法是: 

public ClassLoader getClassLoader()

ClassLoader有一個方法,可以獲取它的父ClassLoader:

public final ClassLoader getParent()

如果ClassLoader是Bootstrap ClassLoader,返回值為null。

比如:

public class ClassLoaderDemo {
    public static void main(String[] args) {
        ClassLoader cl = ClassLoaderDemo.class.getClassLoader();
        while (cl != null) {
            System.out.println(cl.getClass().getName());
            cl = cl.getParent();
        }
        
        System.out.println(String.class.getClassLoader());
    }
}

輸出為:

sun.misc.Launcher$AppClassLoader
sun.misc.Launcher$ExtClassLoader
null

ClassLoader有一個靜態方法,可以獲取預設的系統類載入器:

public static ClassLoader getSystemClassLoader()

ClassLoader中有一個主要方法,用於載入類:

public Class<?> loadClass(String name) throws ClassNotFoundException

比如:

ClassLoader cl = ClassLoader.getSystemClassLoader();
try {
    Class<?> cls = cl.loadClass("java.util.ArrayList");
    ClassLoader actualLoader = cls.getClassLoader();
    System.out.println(actualLoader);
} catch (ClassNotFoundException e) {
    e.printStackTrace();
}    

需要說明的是,由於委派機制,Class的getClassLoader()方法返回的不一定是調用loadClass的ClassLoader,比如,上面代碼中,java.util.ArrayList實際由BootStrap ClassLoader載入,所以返回值就是null。

ClassLoader vs Class.forName

反射一節,我們介紹過Class的兩個靜態方法forName:

public static Class<?> forName(String className)
public static Class<?> forName(String name, boolean initialize, ClassLoader loader)

第一個方法使用系統類載入器載入,第二個指定ClassLoader,參數initialize表示,載入後,是否執行類的初始化代碼(如static語句塊),沒有指定預設為true。

ClassLoader的loadClass方法與forName方法都可以載入類,它們有什麼不同呢?基本是一樣的,不過,有一個不同,ClassLoader的loadClass不會執行類的初始化代碼,看個例子:

public class CLInitDemo {
    public static class Hello {
        static {
            System.out.println("hello");
        }
    };

    public static void main(String[] args) {
        ClassLoader cl = ClassLoader.getSystemClassLoader();
        String className = CLInitDemo.class.getName() + "$Hello";
        try {
            Class<?> cls = cl.loadClass(className);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

使用ClassLoader載入靜態內部類Hello,Hello有一個static語句塊,輸出"hello",運行該程式,類被載入了,但沒有任何輸出,即static語句塊沒有被執行。如果將loadClass的語句換為:

Class<?> cls = Class.forName(className);

則static語句塊會被執行,屏幕將輸出"hello"。

實現代碼

我們來看下ClassLoader的loadClass代碼,以進一步理解其行為:

public Class<?> loadClass(String name) throws ClassNotFoundException {
    return loadClass(name, false);
}

它調用了另一個loadClass方法,其主要代碼為(省略了一些代碼,加了註釋,以便於理解):

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
        // 首先,檢查類是否已經被載入了
        Class c = findLoadedClass(name);
        if (c == null) {
            //沒被載入,先委派父ClassLoader或BootStrap ClassLoader去載入
            try {
                if (parent != null) {
                    //委派父ClassLoader,resolve參數固定為false
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                //沒找到,捕獲異常,以便嘗試自己載入                
            }
            if (c == null) {
                // 自己去載入,findClass才是當前ClassLoader的真正載入方法
                c = findClass(name);
            }
        }
        if (resolve) {
            // 鏈接,執行static語句塊
            resolveClass(c);
        }
        return c;
    }
}

參數resolve類似Class.forName中的參數initialize,可以看出,其預設值為false,即使通過自定義ClassLoader重寫loadClass,設置resolve為true,它調用父ClassLoader的時候,傳遞的也是固定的false。

findClass是一個protected方法,類ClassLoader的預設實現就是拋出ClassNotFoundException,子類應該重寫該方法,實現自己的載入邏輯,後文我們會看個具體例子。

類載入應用 - 可配置的策略

可以通過ClassLoader的loadClass或Class.forName自己載入類,但什麼情況需要自己載入類呢?

很多應用使用面向介面的編程,介面具體的實現類可能有很多,適用於不同的場合,具體使用哪個實現類在配置文件中配置,通過更改配置,不用改變代碼,就可以改變程式的行為,在設計模式中,這是一種策略模式,我們看個簡單的示例。

定義一個服務介面IService:

public interface IService {
    public void action();
}

客戶端通過該介面訪問其方法,怎麼獲得IService實例呢?查看配置文件,根據配置的實現類,自己載入,使用反射創建實例對象,示例代碼為:

public class ConfigurableStrategyDemo {
    public static IService createService() {
        try {
            Properties prop = new Properties();
            String fileName = "data/c87/config.properties";
            prop.load(new FileInputStream(fileName));
            String className = prop.getProperty("service");
            Class<?> cls = Class.forName(className);
            return (IService) cls.newInstance();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) {
        IService service = createService();
        service.action();
    }
}

config.properties的內容示例為:

service=shuo.laoma.dynamic.c87.ServiceB

代碼比較簡單,就不贅述了。

自定義ClassLoader

基本用法

Java類載入機制的強大之處在於,我們可以創建自定義的ClassLoader,自定義ClassLoader是Tomcat實現應用隔離、支持JSP,OSGI實現動態模塊化的基礎。 

怎麼自定義呢?一般而言,繼承類ClassLoader,重寫findClass就可以了。怎麼實現findClass呢?使用自己的邏輯尋找class文件位元組碼的位元組形式,找到後,使用如下方法轉換為Class對象:

protected final Class<?> defineClass(String name, byte[] b, int off, int len)

name表示類名,b是存放位元組碼數據的位元組數組,有效數據從off開始,長度為len。

看個例子:

public class MyClassLoader extends ClassLoader {

    private static final String BASE_DIR = "data/c87/";

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        String fileName = name.replaceAll("\\.", "/");
        fileName = BASE_DIR + fileName + ".class";
        try {
            byte[] bytes = BinaryFileUtils.readFileToByteArray(fileName);
            return defineClass(name, bytes, 0, bytes.length);
        } catch (IOException ex) {
            throw new ClassNotFoundException("failed to load class " + name, ex);
        }
    }
}

MyClassLoader從BASE_DIR下的路徑中載入類,它使用了我們在57節介紹的BinaryFileUtils讀取文件,轉換為byte數組。MyClassLoader沒有指定父ClassLoader,預設是系統類載入器,即ClassLoader.getSystemClassLoader()的返回值,不過,ClassLoader有一個可重寫的構造方法,可以指定父ClassLoader:

protected ClassLoader(ClassLoader parent) 

用途

MyClassLoader有什麼用呢?將BASE_DIR加到classpath中不就行了,確實可以,這裡主要是演示基本用法,實際中,可以從Web伺服器、資料庫或緩存伺服器獲取bytes數組,這就不是系統類載入器能做到的了。

不過,不把BASE_DIR放到classpath中,而是使用MyClassLoader載入,確實有一個很大的好處,可以創建多個MyClassLoader,對同一個類,每個MyClassLoader都可以載入一次,得到同一個類的不同Class對象,比如:

MyClassLoader cl1 = new MyClassLoader();
String className = "shuo.laoma.dynamic.c87.HelloService";
Class<?> class1 = cl1.loadClass(className);

MyClassLoader cl2 = new MyClassLoader();
Class<?> class2 = cl2.loadClass(className);

if (class1 != class2) {
    System.out.println("different classes");
}

cl1和cl2是兩個不同的ClassLoader,class1和class2對應的類名一樣,但它們是不同的對象。

這到底有什麼用呢?

  • 可以實現隔離,一個複雜的程式,內部可能按模塊組織,不同模塊可能使用同一個類,但使用的是不同版本,如果使用同一個類載入器,它們是無法共存的,不同模塊使用不同的類載入器就可以實現隔離,Tomcat使用它隔離不同的Web應用,OSGI使用它隔離不同模塊。
  • 可以實現熱部署,使用同一個ClassLoader,類只會被載入一次,載入後,即使class文件已經變了,再次載入,得到的也還是原來的Class對象,而使用MyClassLoader,則可以先創建一個新的ClassLoader,再用它載入Class,得到的Class對象就是新的,從而實現動態更新。

下麵,我們來具體看熱部署的示例。

自定義ClassLoader的應用 - 熱部署

所謂熱部署,就是在不重啟應用的情況下,當類的定義,即位元組碼文件修改後,能夠替換該Class創建的對象,怎麼做到這一點呢?我們利用MyClassLoader,看個簡單的示例。

我們使用面向介面的編程,定義一個介面IHelloService:

public interface IHelloService {
    public void sayHello();
}

實現類是shuo.laoma.dynamic.c87.HelloImpl,class文件放到MyClassLoader的載入目錄中。

演示類是HotDeployDemo,它定義了以下靜態變數:

private static final String CLASS_NAME = "shuo.laoma.dynamic.c87.HelloImpl";
private static final String FILE_NAME = "data/c87/"
            +CLASS_NAME.replaceAll("\\.", "/")+".class";
private static volatile IHelloService helloService;

CLASS_NAME表示實現類名稱,FILE_NAME是具體的class文件路徑,helloService是IHelloService實例。

當CLASS_NAME代表的類位元組碼改變後,我們希望重新創建helloService,反映最新的代碼,怎麼做呢?先看用戶端獲取IHelloService的方法:

public static IHelloService getHelloService() {
    if (helloService != null) {
        return helloService;
    }
    synchronized (HotDeployDemo.class) {
        if (helloService == null) {
            helloService = createHelloService();
        }
        return helloService;
    }
}

這是一個單例模式,createHelloService()的代碼為:

private static IHelloService createHelloService() {
    try {
        MyClassLoader cl = new MyClassLoader();
        Class<?> cls = cl.loadClass(CLASS_NAME);
        if (cls != null) {
            return (IHelloService) cls.newInstance();
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    return null;
}

它使用MyClassLoader載入類,並利用反射創建實例,它假定實現類有一個public無參構造方法。

在調用IHelloService的方法時,客戶端總是先通過getHelloService獲取實例對象,我們模擬一個客戶端線程,它不停的獲取IHelloService對象,並調用其方法,然後睡眠1秒鐘,其代碼為:

public static void client() {
    Thread t = new Thread() {
        @Override
        public void run() {
            try {
                while (true) {
                    IHelloService helloService = getHelloService();
                    helloService.sayHello();
                    Thread.sleep(1000);
                }
            } catch (InterruptedException e) {
            }
        }
    };
    t.start();
}

怎麼知道類的class文件發生了變化,並重新創建helloService對象呢?我們使用一個單獨的線程模擬這一過程,代碼為:

public static void monitor() {
    Thread t = new Thread() {
        private long lastModified = new File(FILE_NAME).lastModified();

        @Override
        public void run() {
            try {
                while (true) {
                    Thread.sleep(100);
                    long now = new File(FILE_NAME).lastModified();
                    if (now != lastModified) {
                        lastModified = now;
                        reloadHelloService();
                    }
                }
            } catch (InterruptedException e) {
            }
        }
    };
    t.start();
}

我們使用文件的最後修改時間來跟蹤文件是否發生了變化,當文件修改後,調用reloadHelloService()來重新載入,其代碼為:

public static void reloadHelloService() {
    helloService = createHelloService();
}

就是利用MyClassLoader重新創建HelloService,創建後,賦值給helloService,這樣,下次getHelloService()獲取到的就是最新的了。

在主程式中啟動client和monitor線程,代碼為:

public static void main(String[] args) {
    monitor();
    client();
}

在運行過程中,替換HelloImpl.class,可以看到行為會變化,為便於演示,我們在data/c87/shuo/laoma/dynamic/c87/目錄下準備了兩個不同的實現類HelloImpl_origin.class和HelloImpl_revised.class,在運行過程中替換,會看到輸出不一樣,如下圖所示:

使用cp命令修改HelloImpl.class,如果其內容與HelloImpl_origin.class一樣,輸出為"hello",如果與HelloImpl_revised.class一樣,輸出為"hello revised"。

完整的代碼和數據在github上,文末有鏈接。

小結

本節探討了Java中的類載入機制,包括Java載入類的基本過程,類ClassLoader的用法,以及如何創建自定義的ClassLoader,探討了兩個簡單應用示例,一個通過動態載入實現可配置的策略,另一個通過自定義ClassLoader實現熱部署。

84節到本節,我們探討了Java中的多個動態特性,包括反射註解動態代理和類載入器,作為應用程式員,大部分用的都比較少,用的較多的就是使用框架和庫提供的各種註解了,但這些特性大量應用於各種系統程式、框架、和庫中,理解這些特性有助於我們更好的理解它們,也可以在需要的時候自己實現動態、通用、靈活的功能。

註解一節,我們提到,註解是一種聲明式編程風格,它提高了Java語言的表達能力,日常編程中一種常見的需求是文本處理,在電腦科學中,有一種技術大大提高了文本處理的表達能力,那就是正則表達式,大部分編程語言都有對它的支持,它有什麼強大功能呢?

(與其他章節一樣,本節所有代碼位於 https://github.com/swiftma/program-logic,位於包shuo.laoma.dynamic.c87下)

----------------

未完待續,查看最新文章,敬請關註微信公眾號“老馬說編程”(掃描下方二維碼),從入門到高級,深入淺出,老馬和你一起探索Java編程及電腦技術的本質。用心原創,保留所有版權。


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

-Advertisement-
Play Games
更多相關文章
  • 工作原理見書《51單片機應用及原理——基於KeilC和Proteus,陳海宴》P178-180 ...
  • 《鳥哥的Linux私房菜 (基礎學習篇 第三版)》是頗具知名度的Linux入門書《鳥哥的Linux私房菜:基礎學習篇》的全新版,全面而詳細地介紹了Linux操作系統。 全書分為5個部分:第一部分著重說明Linux的起源及功能,如何規劃和安裝Linux主機;第二部分介紹Linux的文件系統、文件、目錄 ...
  • 前言 ASP.NET Core 的變化和發展速度是飛快的,當你發現你還沒有掌握 ASP.NET Core 1.0 的時候, 2.0 已經快要發佈了,目前 2.0 處於 Preview 1 版本,意味著功能已經基本確定,還沒有學習過 ASP.NET Core 的同學可以直接從 2.0 開始學起,但是如 ...
  • Html 內容 Html就是超文本標記語言的簡寫,是最基礎的網頁語言。 Html是通過標簽來定義的語言,代碼都是由標簽所組成。 Html代碼不用區分大小寫。 Html代碼由<html>開始</html>結束。裡面由頭部分<head></head>和體部分<body></body>兩部分組成。 頭部分 ...
  • C#事件總結與應用 什麼是事件? 事件是特殊化的委托,委托是事件的基礎,所以在介紹事件之前先介紹一下委托 通俗的說就是: 事件就是消息驅動器通過委托類來調用感興趣的方法,事實上事件調用是間接的調用 就像是顯示中我的代理人一樣 發佈者與訂閱者 在學習事件的時候們首先要明白什麼是發佈者什麼是訂閱者: 通 ...
  • 一、constant 該函數可以將變數註冊在模塊中,並以服務的形式進行使用。 例如: var app = angular.module("MyModule",[]).constant("pageConfig",{pageSize:10}); 通過以上方式就定義了一個模塊中可用的pageConfig的 ...
  • CentOS上實現一鍵Maven打包並部署到Tomcat的Shell腳本 給這個Shell腳本取個名字,比如叫 deploylab, 將deploylab移到任何已經在系統環境變數的bin目錄下,如: 然後就可以在任意位置直接輸入命令 deploylab 一鍵部署最新代碼到Tomcat了。 ...
  • 在大型項目編碼推進中,涉及到 XML 解析問題時,大多數程式員都不太會選用底層的解析方式直接編碼。 主要存在編碼複雜性、難擴展、難復用....,但如果你是 super 程式員或是一個人的項目,也不妨一試。 Jdom/Dom4j/Xstream... 基於底層解析方式重新組織封裝的開源類庫,簡潔明瞭的 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...