編寫高質量代碼:改善Java程式的151個建議(第8章:異常___建議118~122)

来源:http://www.cnblogs.com/selene/archive/2016/10/18/5960252.html
-Advertisement-
Play Games

多線程技術可以更好地利用系統資源,減少用戶的響應時間,提高系統的性能和效率,但同時也增加了系統的複雜性和運維難度,特別是在高併發、大壓力、高可靠性的項目中。線程資源的同步、搶占、互斥都需要慎重考慮,以避免產生性能損耗和線程死鎖。 建議118:不推薦覆寫start方法 多線程比較簡單的實現方式是繼承T ...


  多線程技術可以更好地利用系統資源,減少用戶的響應時間,提高系統的性能和效率,但同時也增加了系統的複雜性和運維難度,特別是在高併發、大壓力、高可靠性的項目中。線程資源的同步、搶占、互斥都需要慎重考慮,以避免產生性能損耗和線程死鎖。

建議118:不推薦覆寫start方法

  多線程比較簡單的實現方式是繼承Thread類,然後覆寫run方法,在客戶端程式中通過調用對象的start方法即可啟動一個線程,這是多線程程式的標準寫法。不知道大家能夠還能回想起自己寫的第一個多線程的demo呢?估計一般是這樣寫的:

class MultiThread extends Thread{
    @Override
    public synchronized void start() {
        //調用線程體
run();
} @Override public void run() { //MultiThread do someThing } }

覆寫run方法,這好辦,寫上自己的業務邏輯即可,但為什麼要覆寫start方法呢?最常見的理由是:要在客戶端調用start方法啟動線程,不覆寫start方法怎麼啟動run方法呢?於是乎就覆寫了start方法,在方法內調用run方法。客戶端代碼是一個標準程式,代碼如下 

public static void main(String[] args) {
        //多線程對象
        MultiThread m = new MultiThread();
        //啟動多線程
        m.start();
    }

  相信大家都能看出,這是一個錯誤的多線程應用,main方法根本就沒有啟動一個子線程,整個應用程式中只有一個主線程在運行,並不會創建任何其它的線程。對此,有很簡單的解決辦法。只要刪除MultiThread類的start方法即可。

  然後呢?就結束了嗎?是的,很多時候確實到此結束了。那為什麼不必而且不能覆寫start方法,僅僅就是因為" 多線程應用就是這樣寫的 " 這個原因嗎?

  要說明這個問題,就需要看一下Thread類的源碼了。Thread類的start方法的代碼(這個是JDK7版本的)如下: 

public synchronized void start() {
        // 判斷線程狀態,必須是為啟動狀態
        /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();
        // 加入線程組中
        /*
         * Notify the group that this thread is about to be started so that it
         * can be added to the group's list of threads and the group's unstarted
         * count can be decremented.
         */
        group.add(this);
        boolean started = false;
        try {
            // 分配棧記憶體,啟動線程,運行run方法
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /*
                 * do nothing. If start0 threw a Throwable then it will be
                 * passed up the call stack
                 */
            }
        }
    }
   // 本地方法
private native void start0();

  這裡的關鍵是本地方法start0,它實現了啟動線程、申請棧記憶體、運行run方法、修改線程狀態等職責,線程管理和棧記憶體管理都是由JVM負責的,如果覆蓋了start方法,也就是撤銷了線程管理和棧記憶體管理的能力,這樣如何啟動一個線程呢?事實上,不需要關註線程和棧記憶體的管理,主需要編碼者實現多線程的邏輯即可(即run方法體),這也是JVM比較聰明的地方,簡化多線程應用。

  那可能有人要問了:如果確實有必要覆寫start方法,那該如何處理呢?這確實是一個罕見的要求,不過覆寫也容易,只要在start方法中加上super.start()即可,代碼如下:

class MultiThread extends Thread {
    @Override
    public synchronized void start() {
        /* 線程啟動前的業務處理 */
        super.start();
        /* 線程啟動後的業務處理 */
    }

    @Override
    public void run() {
        // MultiThread do someThing
    }

}

  註意看start方法,調用了父類的start方法,沒有主動調用run方法,這是由JVM自行調用的,不用我們顯示實現,而且是一定不能實現。此方式雖然解決了" 覆寫start方法 "的問題,但是基本上無用武之地,到目前為止還沒有發現一定要覆寫start方法的多線程應用,所以要求覆寫start的場景。都可以使用其他的方式實現,例如類變數、事件機制、監聽等方式。

註意:繼承自Thread類的多線程類不必覆寫start方法。

建議119:啟動線程前stop方法是不可靠的

  有這樣一個案例,我們需要一個高效率的垃圾郵件製造機,也就是有儘可能多的線程來儘可能多的製造垃圾郵件,垃圾郵件重要的信息保存在資料庫中,如收件地址、混淆後的標題、反應垃圾處理後的內容等,垃圾製造機的作用就是從資料庫中讀取這些信息,判斷是否符合條件(如收件地址必須包含@符號、標題不能為空等),然後轉換成一份真實的郵件發出去。

  整個應用邏輯很簡單,這必然是一個多線程應用,垃圾郵件製造機需要繼承Thread類,代碼如下:

//垃圾郵件製造機
class SpamMachine extends Thread{
    @Override
    public void run() {
        //製造垃圾郵件
        System.out.println("製造大量垃圾郵件......");
    }
}

  在客戶端代碼中需要發揮電腦的最大潛能來製造郵件,也就是說開儘可能多的線程,這裡我們使用一個while迴圈來處理,代碼如下:

public static void main(String[] args) {
        //不分晝夜的製造垃圾郵件
        while(true){
            //多線程多個垃圾郵件製造機
            SpamMachine sm = new SpamMachine();
            //xx條件判斷,不符合提交就設置該線程不可執行
            if(!false){
                sm.stop();
            }
            //如果線程是stop狀態,則不會啟動
            sm.start();
        }
    }

  在此段代碼中,設置了一個極端條件:所有的線程在啟動前都執行stop方法,雖然它是一個過時的方法,但它的運行邏輯還是正常的,況且stop方法在此處的目的並不是停止一個線程,而是設置線程為不可啟用狀態。想來這應該是沒有問題的,但是運行結果卻出現了奇怪的現象:部分線程還是啟動了,也就是在某些線程(沒有規律)中的start方法正常執行了。在不符合判斷規則的情況下,不可啟用狀態的線程還是啟用了。這是為什麼呢?

  這是線程啟動start方法的一個缺陷。Thread類的stop方法會根據線程狀態來判斷是終結線程還是設置線程為不可運行狀態,對於未啟動的線程(線程狀態為NEW)來說,會設置其標誌位為不可啟動,而其他的狀態則是直接停止。stop方法的JDK1.6源代碼(JDk1.6以上源碼於此可能有變化,需要重新觀察源碼)如下:  

   @Deprecated
    public final void stop() {
        // If the thread is already dead, return.
    // A zero status value corresponds to "NEW".
    if ((threadStatus != 0) && !isAlive()) {
        return;
    }
    stop1(new ThreadDeath());
    }
 private final synchronized void stop1(Throwable th) {
    SecurityManager security = System.getSecurityManager();
    if (security != null) {
        checkAccess();
        if ((this != Thread.currentThread()) ||
        (!(th instanceof ThreadDeath))) {
        security.checkPermission(SecurityConstants.STOP_THREAD_PERMISSION);
        }
    }
        // A zero status value corresponds to "NEW"
    if (threadStatus != 0) {
        resume(); // Wake up thread if it was suspended; no-op otherwise
        stop0(th);
    } else {

            // Must do the null arg check that the VM would do with stop0
        if (th == null) {
         throw new NullPointerException();
        }

            // Remember this stop attempt for if/when start is used
        stopBeforeStart = true;
        throwableFromStop = th;
        }
    }

  這裡設置了stopBeforeStart變數,標志著是在啟動前設置了停止標誌,在start方法中(JDK6源碼)是這樣校驗的:  

public synchronized void start() {
        /**
     * This method is not invoked for the main method thread or "system"
     * group threads created/set up by the VM. Any new functionality added 
     * to this method in the future may have to also be added to the VM.
     *
     * A zero status value corresponds to state "NEW".
         */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();
        group.add(this);
        start0();
// 在啟動前設置了停止狀態
if (stopBeforeStart) { stop0(throwableFromStop); } } private native void start0();

  註意看start0方法和stop0方法的順序,start0方法在前,也就說既是stopBeforeStart為true(不可啟動),也會啟動一個線程,然後再stop0結束這個線程,而罪魁禍首就在這裡!

  明白了原因,我們的情景代碼就很容易修改了,代碼如下:

public static void main(String[] args) {
        // 不分晝夜的製造垃圾郵件
        while (true) {
            // 多線程多個垃圾郵件製造機
            SpamMachine sm = new SpamMachine();
            // xx條件判斷,不符合提交就設置該線程不可執行
            if (!false) {
                new SpamMachine().start();
            }
        }
    }

  不再使用stop方法進行狀態的設置,直接通過判斷條件來決定線程是否可啟用。對於start方法的缺陷,一般不會引起太大的問題,只是增加了線程啟動和停止的精度而已。

建議120:不使用stop方法停止線程

  線程啟動完畢後,在運行時可能需要中止,Java提供的終止方法只有一個stop,但是我不建議使用這個方法,因為它有以下三個問題:

(1)、stop方法是過時的:從Java編碼規則來說,已經過時的方法不建議採用。

(2)、stop方法會導致代碼邏輯不完整:stop方法是一種" 惡意 " 的中斷,一旦執行stop方法,即終止當前正在運行的線程,不管線程邏輯是否完整,這是非常危險的。看如下的代碼:

public static void main(String[] args) {
        Thread thread = new Thread() {
            @Override
            public void run() {
                try {
                    // 子線程休眠1秒
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    // 異常處理
                }
                System.out.println("此處是業務邏輯,永遠不會執行");
            }
        };
        // 啟動線程
        thread.start();
        // 主線程休眠0.1秒
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 子線程停止
        thread.stop();
    }

  這段代碼的邏輯是這樣的:子線程是一個匿名內部類,它的run方法在執行時會休眠一秒,然後執行後續的邏輯,而主線程則是休眠0.1秒後終止子線程的運行,也就說JVM在執行tread.stop()時,子線程還在執行sleep(1000),此時stop方法會清除棧內信息,結束該線程,這也就導致了run方法的邏輯不完整,輸出語句println代表的是一段邏輯,可能非常重要,比如子線程的主邏輯、資源回收、情景初始化等,但是因為stop線程了,這些都不再執行,於是就產生了業務邏輯不完整的情況。

  這是極度危險的,因為我們不知道子線程會在什麼時候被終止,stop連基本的邏輯完整性都無法保證。而且此種操作也是非常隱蔽的,子線程執行到何處會被關閉很難定位,這位以後的維護帶來了很多麻煩。

(3)、stop方法會破壞原子邏輯

  多線程為瞭解決共用資源搶占的問題,使用了鎖概念,避免資源不同步,但是正因為此,stop方法卻會帶來更大的麻煩,它會丟棄所有的鎖,導致原子邏輯受損。例如有這樣一段程式:

class MultiThread implements Runnable {
    int a = 0;
    @Override
    public void run() {
        // 同步代碼塊,保證原子操作
        synchronized ("") {
            // 自增
            a++;
            try {
                //線程休眠0.1秒
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 自減
            a--;
            String tn = Thread.currentThread().getName();
            System.out.println(tn + ":a = " + a);
        }
    }

}

  MultiThread實現了Runnable介面,具備多線程能力,其中run方法中加上了synchronized代碼塊,表示內部是原子邏輯,它會先自增然後自減,按照synchronized同步代碼塊的規則來處理,此時無論啟動多少線程,列印出來的結果應該是a=0,但是如果有一個正在執行的線程被stop,就會破壞這種原子邏輯,代碼如下:  

    public static void main(String[] args) {
        MultiThread t = new MultiThread();
        Thread t1 = new Thread(t);
        // 啟動t1線程
        t1.start();
        for (int i = 0; i < 5; i++) {
            new Thread(t).start();
        }
        //停止t1線程
        t1.stop();
    }

  首先說明的是所有線程共用了一個MultiThread的實例變數t,其次由於在run方法中加入了同步代碼塊,所以只能有一個線程進入到synchronized塊中。這段代碼的執行順序如下:

  1. 線程t1啟動,並執行run方法,由於沒有其它線程同步代碼塊的鎖,所以t1線程執行後自加後執行到sleep方法即開始休眠,此時a=1
  2. JVM又啟動了5個線程,也同時運行run方法,由於synchronized關鍵字的阻塞作用,這5個線程不能執行自增和自減操作,等待t1線程鎖釋放。
  3. 主線程執行了t1.stop方法,終止了t1線程,註意,由於a變數是所有線程共用的,所以其它5個線程獲得的a變數也是1
  4. 其它5個線程依次獲得CPU執行機會,列印出a值

  分析了這麼多,相信大家也明白了輸出結果,結果如下:

    Thread-5:a = 1
    Thread-4:a = 1
    Thread-3:a = 1
    Thread-2:a = 1
    Thread-1:a = 1

  原本期望synchronized同步代碼塊中的邏輯都是原子邏輯,不受外界線程的干擾,但是結果卻出現原子邏輯被破壞的情況,這也是stop方法被廢棄的一個重要原因:破壞了原子邏輯。

  既然終止一個線程不能使用stop方法,那怎樣才能終止一個正在運行的線程呢?答案也簡單,使用自定義的標誌位決定線程的執行情況,代碼如下:

class SafeStopThread extends Thread {
    // 此變數必須加上volatile
    /*
     * volatile: 1.作為指令關鍵字,確保本條指令不會因編譯器的優化而省略,且要求每次直接讀值.
     * 2.被設計用來修飾被不同線程訪問和修改的變數。如果不加入volatile
     * ,基本上會導致這樣的結果:要麼無法編寫多線程程式,要麼編譯器失去大量優化的機會。
     */
    private volatile boolean stop = false;

    @Override
    public void run() {
        // 判斷線程體是否運行
        while (stop) {
            // doSomething
        }
    }

    public void terminate() {
        stop = true;
    }
}

  這是很簡單的辦法,線上程體中判斷是否需要停止運行,即可保證線程體的邏輯完整性,而且也不會破壞原子邏輯。可能大家對JavaAPI比較熟悉,於是提出疑問:Thread不是還提供了interrupt中斷線程的方法嗎?這個方法可不是過時方法,那可以使用嗎?它可以終止一個線程嗎?

  interrupt,名字看上去很像是終止一個線程的方法,但它不能終止一個正在執行著的線程,它只是修改中斷標誌而已,例如下麵一段代碼:

    public static void main(String[] args) {
        Thread thread = new Thread() {
            @Override
            public void run() {
                // 線程一直運行
                while (true) {
                    System.out.println("Running......");
                }
            }
        };
        // 啟動線程
        thread.start();
        // 中斷線程
        thread.interrupt();
    }

  執行這段代碼,你會發現一直有Running在輸出,永遠不會停止,似乎執行了interrupt沒有任何變化,那是因為interrupt方法不能終止一個線程狀態,它只會改變中斷標誌位(如果在thread.interrupt()前後輸出thread.isInterrupted()則會發現分別輸出了false和true),如果需要終止該線程,還需要自己進行判斷,例如我們可以使用interrupt編寫出更簡潔、安全的終止線程代碼:

class SafeStopThread extends Thread {
    @Override
    public void run() {
        //判斷線程體是否運行
        while (!isInterrupted()) {
            // do SomeThing
        }
    }
}

   總之,如果期望終止一個正在運行的線程,則不能使用已過時的stop方法。需要自行編碼實現,如此即可保證原子邏輯不被破壞,代碼邏輯不會出現異常。當然,如果我們使用的是線程池(比如ThreadPoolExecutor類),那麼可以通過shutdown方法逐步關閉池中的線程,它採用的是比較溫和、安全的關閉線程方法,完全不會產生類似stop方法的弊端。

建議121:線程優先順序只使用三個等級

  線程的優先順序(Priority)決定了線程獲得CPU運行的機會,優先順序越高獲得的運行機會越大,優先順序越低獲得的機會越小。Java的線程有10個級別(準確的說是11個級別,級別為0的線程是JVM的,應用程式不能設置該級別),那是不是說級別是10的線程肯定比級別是9的線程先運行呢?我們來看如下一個多線程類:

class TestThread implements Runnable {
    public void start(int _priority) {
        Thread t = new Thread(this);
        // 設置優先順序別
        t.setPriority(_priority);
        t.start();
    }
    @Override
    public void run() {
        // 消耗CPU的計算
        for (int i = 0; i < 100000; i++) {
            Math.hypot(924526789, Math.cos(i));
        }
        // 輸出線程優先順序
        System.out.println("Priority:" + Thread.currentThread().getPriority());
    }
}

  該多線程實現了Runnable介面,實現了run方法,註意在run方法中有一個比較占用CPU的計算,該計算毫無意義,

public static void main(String[] args) {
        //啟動20個不同優先順序的線程
        for (int i = 0; i < 20; i++) {
            new TestThread().start(i % 10 + 1);
        }
    }

 這裡創建了20個線程,每個線程在運行時都耗盡了CPU的資源,因為優先順序不同,線程調度應該是先處理優先順序高的,然後處理優先順序低的,也就是先執行2個優先順序為10的線程,然後執行2個優先順序為9的線程,2個優先順序為8的線程......但是結果卻並不是這樣的。

  Priority:5
  Priority:7
  Priority:10
  Priority:6
  Priority:9
  Priority:6
  Priority:5
  Priority:7
  Priority:10
  Priority:3
  Priority:4
  Priority:8
  Priority:8
  Priority:9
  Priority:4
  Priority:1
  Priority:3
  Priority:1
  Priority:2
  Priority:2

  println方法雖然有輸出損耗,可能會影響到輸出結果,但是不管運行多少次,你都會發現兩個不爭的事實:

(1)、並不是嚴格按照線程優先順序來執行的

  比如線程優先順序為5的線程比優先順序為7的線程先執行,優先順序為1的線程比優先順序為2的線程先執行,很少出現優先順序為2的線程比優先順序為10的線程先執行(註意,這裡是" 很少 ",是說確實有可能出現,只是幾率低,因為優先順序只是表示線程獲得CPU運行的機會,並不代表強制的排序號)。

(2)、優先順序差別越大,運行機會差別越明顯

  比如優先順序為10的線程通常會比優先順序為2的線程先執行,但是優先順序為6的線程和優先順序為5的線程差別就不太明顯了,執行多次,你會發現有不同的順序。

  這兩個現象是線程優先順序的一個重要表現,之所以會出現這種情況,是因為線程運行是需要獲得CPU資源的,那誰能決定哪個線程先獲得哪個線程後獲得呢?這是依照操作系統設置的線程優先順序來分配的,也就是說,每個線程要運行,需要操作系統分配優先順序和CPU資源,對於JAVA來說,JVM調用操作系統的介面設置優先順序,比如windows操作系統優先順序都相同嗎?

  事實上,不同的操作系統線程優先順序是不同的,Windows有7個優先順序,Linux有140個優先順序,Freebsd則由255個(此處指的優先順序個數,不同操作系統有不同的分類,如中斷級線程,操作系統級等,各個操作系統具體用戶可用的線程數量也不相同)。Java是跨平臺的系統,需要把這10個優先順序映射成不同的操作系統的優先順序,於是界定了Java的優先順序只是代表搶占CPU的機會大小,優先順序越高,搶占CPU的機會越大,被優先執行的可能性越高,優先順序相差不大,則搶占CPU的機會差別也不大,這就是導致了優先順序為9的線程可能比優先順序為10的線程先運行。

  Java的締造者們也覺察到了線程優先問題,於是Thread類中設置了三個優先順序,此意就是告訴開發者,建議使用優先順序常量,而不是1到10的隨機數字。常量代碼如下: 

public class Thread implements Runnable {
    /**
     * The minimum priority that a thread can have. 
     */
    public final static int MIN_PRIORITY = 1;
    /**
     * The default priority that is assigned to a thread. 
     */
    public final static int NORM_PRIORITY = 5;
    /**
     * The maximum priority that a thread can have. 
     */
    public final static int MAX_PRIORITY = 10;


}

  在編碼時直接使用這些優先順序常量,可以說在大部分情況下MAX_PRIORITY的線程回比MIN_PRIORITY的線程優先運行,但是不能認為是必然會先運行,不能把這個優先順序做為核心業務的必然條件,Java無法保證優先順序高肯定會先執行,只能保證高優先順序有更多的執行機會。因此,建議在開發時只使用此三類優先順序,沒有必要使用其他7個數字,這樣也可以保證在不同的操作系統上優先順序的表現基本相同。

  大家也許會問,如果優先順序相同呢?這很好辦,也是由操作系統決定的。基本上是按照FIFO原則(先入先出,First Input First Output),但也是不能完全保證。


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

-Advertisement-
Play Games
更多相關文章
  • [ASP.NET Core] Getting Started 前言 本篇文章介紹如何快速建立一個ASP.NET Core應用程式,為自己留個紀錄也希望能幫助到有需要的開發人員。 ASP.NET Core官網 環境 建立一個ASP.NET Core應用程式,首先要從官網下載SDK來建置.NET Cor ...
  • 枚舉類型是定義了一組“符號名稱/值”配對。枚舉類型是強類型的。每個枚舉類型都是從system.Enum派生,又從system.ValueType派生,而system.ValueType又從system.Object派生,所以枚舉類型是指類型。 編譯枚舉類型時,C#編譯器會把每個符號轉換成類型的一個常 ...
  • 上一章我們講到關於C#線程方向的應用。但是筆者並沒有講到多線程中的另一個知識點——同步。多線程的應用開發都有可能發生臟數據。同步的功能或多或少都會用到。本章就要來講一下關於線程同步的問題。根據筆者這幾年來的.NET開發可以瞭解到的同步方式至少有四種以上。如。lock、volatile、Monitor ...
  • VS中如何快捷地給自己的代碼添加創建信息註釋 Intro 以下討論的都是沒有使用 GIT 來管理源代碼的情況,如果使用 GIT 管理源代碼可直接使用VS的Git擴展就不需要考慮以下問題。 什麼是創建信息註釋? 創建信息註釋,類似於文件的創建信息,一般來說,至少要有創建人和創建時間。如果說有人修改了方 ...
  • spring的事務處理分為兩種: 1、編程式事務:在程式中控制事務開始,執行和提交;(不建議使用,所以這裡我就不說明太多) 2、聲明式事務:在Spring配置文件中對事務進行配置,無須在程式中寫代碼;(建議使用) 我對”聲明式“的理解是這樣的:Spring配置文件中定義好了這樣一個規則, 這個規則可 ...
  • 回到目錄 挺有意思的一件事 對於MVC視圖渲染來說,大家應該不會陌生,但對於模型的渲染,不知道是否聽說過,主要是說Model通過它屬性的相關特性(DataType,UIHint)來將它們自動渲染到View上,這是一個比較不錯的技術,因為使用傳統的Html.EditorForModel去渲染是不能滿足 ...
  • 需求分析 ”null exception“很見的一種異常,但在某些情況下卻會引起嚴重的bug! 本文目的就是對代碼進行null 的檢查,避免不應該出現的Error。 本文藉助reshaper,全局檢測項目中所有可能出現的null exception 關於resharper的知識:http://zzk ...
  • 一、動態語言 Objective-C語言是一門動態語言,它將很多靜態語言在編譯和鏈接時期做的事放到了運行時來處理。這種動態語言的優勢在於:具有靈活性,比如:消息轉發,方法交換等。它有一個運行時系統Objc Runtime,其實是一個Runtime庫,基本上是用C和彙編寫的,這個庫使得C語言有了面向對 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...