方法引用(Method reference)和invokedynamic指令詳細分析

来源:https://www.cnblogs.com/racaljk/archive/2019/01/04/10221349.html
-Advertisement-
Play Games

方法引用(Method reference)和invokedynamic指令詳細分析 是jvm指令集裡面最複雜的一條。本文將詳細分析 指令是如何實現方法引用(Method reference)的。 具體言之,有這樣一個方法引用: 使用 查看對應位元組碼: 使用 指令生成encode對象,然後存入 局部 ...


方法引用(Method reference)和invokedynamic指令詳細分析

invokedynamic是jvm指令集裡面最複雜的一條。本文將詳細分析invokedynamic指令是如何實現方法引用(Method reference)的。

具體言之,有這樣一個方法引用:

interface Encode {
    void encode(Derive person);
}
class Base {
    public void encrypt() {
        System.out.println("Base::speak");
    }
}
class Derive extends Base {
    @Override
    public void encrypt() {
        System.out.println("Derive::speak");
    }
}
public class MethodReference {
    public static void main(String[] args) {
        Encode encode = Base::encrypt;
        System.out.println(encode);
    }
}

使用javap -verbose MethodReference.class查看對應位元組碼:

// 常量池
Constant pool:
   #1 = Methodref          #6.#22         // java/lang/Object."<init>":()V
   #2 = InvokeDynamic      #0:#27         // #0:encode:()LEncode;
   #3 = Fieldref           #28.#29        // java/lang/System.out:Ljava/io/PrintStream;
   #4 = Methodref          #30.#31        // java/io/PrintStream.println:(Ljava/lang/Object;)V
   #5 = Class              #32            // MethodReference
   #6 = Class              #33            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               LMethodReference;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               args
  #17 = Utf8               [Ljava/lang/String;
  #18 = Utf8               encode
  #19 = Utf8               LEncode;
  #20 = Utf8               SourceFile
  #21 = Utf8               MethodReference.java
  #22 = NameAndType        #7:#8          // "<init>":()V
  #23 = Utf8               BootstrapMethods
  #24 = MethodHandle       #6:#34         // invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;L
java/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang
/invoke/CallSite;
  #25 = MethodType         #35            //  (LDerive;)V
  #26 = MethodHandle       #5:#36         // invokevirtual Base.encrypt:()V
  #27 = NameAndType        #18:#37        // encode:()LEncode;
  #28 = Class              #38            // java/lang/System
  #29 = NameAndType        #39:#40        // out:Ljava/io/PrintStream;
  #30 = Class              #41            // java/io/PrintStream
  #31 = NameAndType        #42:#43        // println:(Ljava/lang/Object;)V
  #32 = Utf8               MethodReference
  #33 = Utf8               java/lang/Object
  #34 = Methodref          #44.#45        // java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/Str
ing;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallS
ite;
  #35 = Utf8               (LDerive;)V
  #36 = Methodref          #46.#47        // Base.encrypt:()V
  #37 = Utf8               ()LEncode;
  #38 = Utf8               java/lang/System
  #39 = Utf8               out
  #40 = Utf8               Ljava/io/PrintStream;
  #41 = Utf8               java/io/PrintStream
  #42 = Utf8               println
  #43 = Utf8               (Ljava/lang/Object;)V
  #44 = Class              #48            // java/lang/invoke/LambdaMetafactory
  #45 = NameAndType        #49:#53        // metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Lj
ava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
  #46 = Class              #54            // Base
  #47 = NameAndType        #55:#8         // encrypt:()V
  #48 = Utf8               java/lang/invoke/LambdaMetafactory
  #49 = Utf8               metafactory

// 位元組碼指令
 public static void main(java.lang.String[]);
     0: invokedynamic #2,  0              // InvokeDynamic #0:encode:()LEncode;
     5: astore_1
     6: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
     9: aload_1
    10: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
    13: return

// 屬性
SourceFile: "MethodReference.java"
InnerClasses:
     public static final #51= #50 of #56; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
BootstrapMethods:
  0: #24 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/Method
Type;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #25 (LDerive;)V
      #26 invokevirtual Base.encrypt:()V
      #25 (LDerive;)V

使用invokedynamic指令生成encode對象,然後存入局部變數槽#1。接著獲取getstatic獲取java/lang/System類的out欄位,最後局部變數槽#1作為參數壓棧,invokevirtual虛函數調用System.outprintln方法。

那麼invokedynamic到底是怎麼生成encode對象的呢?

1.虛擬機解析

hotspotinvokedynamic指令的解釋如下:

      CASE(_invokedynamic): {

        u4 index = Bytes::get_native_u4(pc+1);
        ConstantPoolCacheEntry* cache = cp->constant_pool()->invokedynamic_cp_cache_entry_at(index);

        // We are resolved if the resolved_references field contains a non-null object (CallSite, etc.)
        // This kind of CP cache entry does not need to match the flags byte, because
        // there is a 1-1 relation between bytecode type and CP entry type.
        if (! cache->is_resolved((Bytecodes::Code) opcode)) {
          CALL_VM(InterpreterRuntime::resolve_from_cache(THREAD, (Bytecodes::Code)opcode),
                  handle_exception);
          cache = cp->constant_pool()->invokedynamic_cp_cache_entry_at(index);
        }

        Method* method = cache->f1_as_method();
        if (VerifyOops) method->verify();

        if (cache->has_appendix()) {
          ConstantPool* constants = METHOD->constants();
          SET_STACK_OBJECT(cache->appendix_if_resolved(constants), 0);
          MORE_STACK(1);
        }

        istate->set_msg(call_method);
        istate->set_callee(method);
        istate->set_callee_entry_point(method->from_interpreted_entry());
        istate->set_bcp_advance(5);

        // Invokedynamic has got a call counter, just like an invokestatic -> increment!
        BI_PROFILE_UPDATE_CALL();

        UPDATE_PC_AND_RETURN(0); // I'll be back...
      }

使用invokedynamic_cp_cache_entry_at獲取常量池對象,然後檢查是否已經解析過,如果沒有就解析反之復用,然後設置方法位元組碼,留待後面解釋執行。那麼,重點是這個解析。我們對照著jvm spec來看。

根據jvm文檔的描述,invokedynamic的操作數(operand)指向常量池一個動態調用點描述符(dynamic call site specifier)。
動態調用點描述符是一個CONSTANT_InvokeDynamic_info結構體:

CONSTANT_InvokeDynamic_info {
 u1 tag;
 u2 bootstrap_method_attr_index;
 u2 name_and_type_index;
}
  • tag 表示這個結構體的常量,不用管
  • bootstrap_method_attr_index 啟動方法數組
  • name_and_type_index 一個名字+類型的描述欄位,就像這樣Object p放到虛擬機裡面表示是Ljava/lang/Object; p

然後啟動方法數組結構是這樣:

BootstrapMethods_attribute {
 ...
 u2 num_bootstrap_methods;
 { 
    u2 bootstrap_method_ref;
    u2 num_bootstrap_arguments;
    u2 bootstrap_arguments[num_boot]
    } bootstrap_methods[num_bootstrap_methods];
}

就是一個數組,每個元素是{指向MethodHandle的索引,啟動方法參數個數,啟動方法參數}

MethodlHandle是個非常重要的結構,指導了虛擬機對於這個啟動方法的解析,先關註一下這個結構:

CONSTANT_MethodHandle_info {
 u1 tag;//表示該結構體的常量tag,可以忽略
 u1 reference_kind;
 u2 reference_index;
}
  • reference_kind是[1,9]的數,它表示這個method handle的類型,這個欄位和位元組碼的行為有關。
  • reference_index 根據reference_kind會指向常量池的不同類型,具體來說
    • reference_kind==1,3,4 指向CONSTANT_Fieldref_info結構,表示一個類的欄位
    • reference_kind==5,8,指向CONSTANT_Methodref_info,表示一個類的方法
    • reference_kind==6,7, 同上,只是兼具介面的方法或者類的方法的可能。
    • reference_kind==9,指向CONSTATN_InterfaceMethodref_info,表示一個介面方法

通過invokedynamic,我們可以得

  1. 名字+描述符的表示(由name_and_type_index給出)
  2. 一個啟動方法數組(由bootstrap_method_attr_index給出)

2.手動解析

可以手動模擬一下解析,看看最後得到的數據是什麼樣的。在這個例子中:

  0: invokedynamic #2,  0   //第二個operand總是0

查看常量池#2項:

#2 = InvokeDynamic      #0:#27         // #0:encode:()LEncode;
#27 = NameAndType        #18:#37        // encode:()LEncode;

BootstrapMethods:
  0: #24 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/Method
Type;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #25 (LDerive;)V
      #26 invokevirtual Base.encrypt:()V
      #25 (LDerive;)V

得到的名字+描述符是:Encode.encode(),啟動方法數組有一個元素,回憶下之前說的,這個元素構成如下:

{指向MethodHandle的索引,啟動方法參數個數,啟動方法參數}

這裡得到的MethodHandle表示的是LambdaMetafactory.metafactory:

#24 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/Method
Type;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;`

啟動方法參數有:

  • #25 (LDerive;)V
  • #26 invokevirtual Base.encrypt:()V
  • #25 (LDerive;)V

3. java.lang.invoke.LambdaMetafactory

先說說LambdaMetafactory有什麼用。javadoc給出的解釋是:

Facilitates the creation of simple "function objects" that implement one or more interfaces by delegation to a provided MethodHandle, after appropriate type adaptation and partial evaluation of arguments. Typically used as a bootstrap method for invokedynamic call sites, to support the lambda expression and method reference expression features of the Java Programming Language.
When the target of the CallSite returned from this method is invoked, the resulting function objects are instances of a class which implements the interface named by the return type of invokedType, declares a method with the name given by invokedName and the signature given by samMethodType. It may also override additional methods from Object.

LambdaMetafactory方便我們創建簡單的"函數對象",這些函數對象通過代理MethodHandle實現了一些介面。
當這個函數返回的CallSite被調用的時候,會產生一個類的實例,該類還實現了一些方法,具體由參數給出

將上面得到的MethodHandle寫得更可讀就是調用的這個方法:

   public static CallSite LambdaMetafactory.metafactory(MethodHandles.Lookup caller,
                                       String invokedName,
                                       MethodType invokedType,
                                       MethodType samMethodType,
                                       MethodHandle implMethod,
                                       MethodType instantiatedMethodType);

六個參數,慢慢來。

3.1 LambdaMetafactory.metafactory()調用前

要知道參數是什麼意思,可以從它的調用者來管中窺豹:

 static CallSite makeSite(MethodHandle bootstrapMethod,
                             // Callee information:
                             String name, MethodType type,
                             // Extra arguments for BSM, if any:
                             Object info,
                             // Caller information:
                             Class<?> callerClass) {
        MethodHandles.Lookup caller = IMPL_LOOKUP.in(callerClass);
        CallSite site;
        try {
            Object binding;
            info = maybeReBox(info);
            if (info == null) {
                binding = bootstrapMethod.invoke(caller, name, type);
            } else if (!info.getClass().isArray()) {
                binding = bootstrapMethod.invoke(caller, name, type, info);
            } else {
                Object[] argv = (Object[]) info;
                maybeReBoxElements(argv);
                switch (argv.length) {
                ...
                case 3:
                    binding = bootstrapMethod.invoke(caller, name, type,
                                                     argv[0], argv[1], argv[2]);
                    break;
                ...
                }
            }
            //System.out.println("BSM for "+name+type+" => "+binding);
            if (binding instanceof CallSite) {
                site = (CallSite) binding;
            }  else {
                throw new ClassCastException("bootstrap method failed to produce a CallSite");
            }
            ...
        } catch (Throwable ex) {
            ...
        }
        return site;
    }

java.lang.invoke.LambdaMetafactory的調用是通過MethodHandle引發的,所以可能還需要補一下MethodHandle的用法,百度一搜一大堆,javadoc也給出了使用示例:

String s;
MethodType mt; MethodHandle mh;
MethodHandles.Lookup lookup = MethodHandles.lookup();
// mt is (char,char)String
mt = MethodType.methodType(String.class, char.class, char.class);
mh = lookup.findVirtual(String.class, "replace", mt);
s = (String) mh.invoke("daddy",'d','n');
// invokeExact(Ljava/lang/String;CC)Ljava/lang/String;
assertEquals(s, "nanny");

回到源碼,關鍵是這句:

binding = bootstrapMethod.invoke(caller, name, type,
                               argv[0], argv[1], argv[2]);

argv[0],argv[1],argv[2]分別表示之前啟動方法的三個參數
caller即調用者,這裡是MethodReference這個類,然後name和type參見下麵的詳細解釋:

  • MethodHandles.Lookup caller 表示哪個類引發了調動
  • String invokedName 表示生成的類的方法名,對應例子的encode
  • MethodType invokedType 表示CallSite的函數簽名,其中參數類型表示捕獲變數的類型,返回類型是類要實現的介面的名字,對應例子的()Encode,即要生成一個類,這個類沒有捕獲自由變數(所以參數類為空),然後這個類要實現Encode介面(返回類型為生成的類要實現的介面)
    接下來
  • MethodType samMethodType 表示要實現的方法的函數簽名和返回值,對於例子的#25 (LDerive;)V,即實現方法帶有一個形參,返回void
  • MethodHandle implMethod 表示實現的方法裡面應該調用的函數,對於例子的#26 invokevirtual Base.encrypt:()V,表示調用Base的虛函數encrypt,返回void
  • MethodType instantiatedMethodType 表示調用方法的運行時描述符,如果不是泛型就和samMethodType一樣

3.2 LambdaMetafactory.metafactory()調用

源碼面前,不是了無秘密嗎hhh,點進源碼看看這個LambdaMetafactory到底做了什麼:

     */
    public static CallSite metafactory(MethodHandles.Lookup caller,
                                       String invokedName,
                                       MethodType invokedType,
                                       MethodType samMethodType,
                                       MethodHandle implMethod,
                                       MethodType instantiatedMethodType)
            throws LambdaConversionException {
        AbstractValidatingLambdaMetafactory mf;
        mf = new InnerClassLambdaMetafactory(caller, invokedType,
                                             invokedName, samMethodType,
                                             implMethod, instantiatedMethodType,
                                             false, EMPTY_CLASS_ARRAY, EMPTY_MT_ARRAY);
        mf.validateMetafactoryArgs();
        return mf.buildCallSite();
    }

它什麼也沒做,做事的是InnerClassLambdaMetafactory.buildCallSite()創建的最後CallSite,那就進一步看看InnerClassLambdaMetafactory.buildCallSite()

    @Override
    CallSite buildCallSite() throws LambdaConversionException {
        // 1. 創建生成的類對象
        final Class<?> innerClass = spinInnerClass();
        if (invokedType.parameterCount() == 0) {
            // 2. 用反射獲取構造函數
            final Constructor<?>[] ctrs = AccessController.doPrivileged(
                    new PrivilegedAction<Constructor<?>[]>() {
                @Override
                public Constructor<?>[] run() {
                    Constructor<?>[] ctrs = innerClass.getDeclaredConstructors();
                    if (ctrs.length == 1) {
                        // The lambda implementing inner class constructor is private, set
                        // it accessible (by us) before creating the constant sole instance
                        ctrs[0].setAccessible(true);
                    }
                    return ctrs;
                }
                    });
            if (ctrs.length != 1) {
                throw new LambdaConversionException("Expected one lambda constructor for "
                        + innerClass.getCanonicalName() + ", got " + ctrs.length);
            }

            try {
                // 3. 創建實例 
                Object inst = ctrs[0].newInstance();
                // 4. 根據實例和samBase(介面類型)生成MethodHandle
                // 5. 生成ConstantCallSite
                return new ConstantCallSite(MethodHandles.constant(samBase, inst));
            }
            catch (ReflectiveOperationException e) {
                throw new LambdaConversionException("Exception instantiating lambda object", e);
            }
        } else {
            try {
                UNSAFE.ensureClassInitialized(innerClass);
                return new ConstantCallSite(
                        MethodHandles.Lookup.IMPL_LOOKUP
                             .findStatic(innerClass, NAME_FACTORY, invokedType));
            }
            catch (ReflectiveOperationException e) {
                throw new LambdaConversionException("Exception finding constructor", e);
            }
        }
    }

首先它生成一個.class文件,虛擬機預設不會輸出,需要下麵設置VM option-Djdk.internal.lambda.dumpProxyClasses=.,Dump出虛擬機生成的類我得到的是:

import java.lang.invoke.LambdaForm.Hidden;

// $FF: synthetic class
final class MethodReference$$Lambda$1 implements Encode {
    private MethodReference$$Lambda$1() {
    }

    @Hidden
    public void encode(Derive var1) {
        ((Base)var1).encrypt();
    }
}

該類實現了傳來的介面函數(動態類生成,熟悉spring的朋友應該很熟悉)。

回到buildCallSite()源碼,它使用MethodHandles.constant(samBase, inst)創建MethdHandle,放到CallSite裡面,完成整個LambdaMetafactory的工作。
MethodHandles.constant(samBase, inst)相當於一個總是返回inst的方法。

總結

到這裡就結束了整個流程,文章有點長,總結一下:

  1. 虛擬機遇到invokedynamic,開始解析操作數
  2. 根據invokedynamic #0:#27獲取到啟動方法(#0)和一個名字+描述符(#27)
    其中啟動方法是
BootstrapMethods:
  0: #24 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/Method
Type;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #25 (LDerive;)V
      #26 invokevirtual Base.encrypt:()V
      #25 (LDerive;)V

名字+描述符

 #27 = NameAndType        #18:#37        // encode:()LEncode;
  1. 啟動方法指向LambdaMetafactory.metafactory,但是不會直接調用而是通過MethdHandle間接調用。調用位置位於CallSite.makeCallSite()
  2. LambdaMetafactory.metafactory()其實使用InnerClassLambdaMetafactory.buildCallSite()創建了最後的CallSite
  3. buildCallSite()會創建一個.class,
  4. buildCallSite()會向最後的CallSite裡面放入一個可調用的MethdHandle
  5. 這個MethodHandle指向的是一個總是返回剛剛創建的.class類的實例的方法,由MethodHandles.constant(samBase, inst)完成
  6. 最後,用invokevirtual調用CallSite裡面的MethdHandle,返回.class類的示例,即inst,即new MethodReference$$Lambda$1

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

-Advertisement-
Play Games
更多相關文章
  • 計數器 Counter 計數元素迭代器 elements() 計數對象拷貝 copy() 計數對象清空 clear() 有序字典 OrderedDict (對字典的補充,可以記住字典元素添加的順序) 預設字典 defaultdict,(指定字典值的類型) 可命名元組 namedtuple (給元組對 ...
  • 網路編程協議 1.osi七層模型 應用層 表示層 會話層 傳輸層 網路層 數據鏈路層 物理層 2.套接字 socket 有兩類,一種基於文件類型,一種基於網路類型 3.Tcp和udp協議 Tcp協議:面向連接,數據可靠,傳輸效率低,面向位元組流 建立連接與斷開連接的過程(三次握手,四次揮手) 建立連接 ...
  • 1.學習爬蟲,為什麼必須會正則表達式? 我們爬取一些網頁具體內容時,只需要這個網頁某個標簽的一部分內容就足夠,或者是這個標簽的某個屬性的值時,用普通的 xpath 或者css.selector是不能完成的,此時我們就需用到正則表達式去匹配獲取。2.正則表達式官方簡介? 正則表達式,又稱規則表達式。( ...
  • 前言 函數指針是什麼?如何使用函數指針?函數指針到底有什麼大用?本文將一一介紹。 如何理解函數指針 如果有int *類型變數,它存儲的是int類型變數的地址;那麼對於函數指針來說,它存儲的就是函數的地址。函數也是有地址的,函數實際上由載入記憶體的一些指令組成,而指向函數的指針存儲了函數指令的起始地址。 ...
  • 《從零開始PYTHON3》學習資源包下載 課程連載已經完全結束。 經過整理校對,這裡把在課程中出現過的源碼和練習答案示例源碼全部打包提供下載: 提取碼:f3r6 壓縮包解壓密碼:https://formoon.github.io 資源包中還包含了64位Windows版本的Python3安裝包,是驗證 ...
  • 周末小實踐,vue+樹莓派+一言API 一直有個想法,讓樹莓派做後端,實現一個有趣的網路服務。可是,苦於不會前端,遲遲無法動手。最近由於工作任務需要研究了一下前端。 問過前端大佬們,個個都說你得用vue.js,當前最流行、最熱門的前端框架,就用它!聽我的,沒錯! 在大佬的指導下,花了半天時間領略到了 ...
  • 1.簡介 HBase是一個基於HDFS的、分散式的、面向列的非關係型資料庫。 HBase的特點 1.海量數據存儲,HBase表中的數據能夠容納上百億行*上百萬列。 2.面向列的存儲,數據在表中是按照列進行存儲的,能夠動態的增加列並對列進行各種操作。 3.準實時查詢,HBase在海量的數據量下能夠接近 ...
  • 題意 "題目鏈接" Sol 一道咕咕咕了好長時間的題 題解可以看 "這裡" cpp include define LL long long using namespace std; const int MAXN = 1e7 + 5e6 + 10, mod = 1e9 + 7, mod2 = 1e9 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...