JavaAgent學習小結

来源:https://www.cnblogs.com/jycboy/archive/2020/02/01/12249472.html
-Advertisement-
Play Games

前言 最近因為公司需要,需要瞭解下java探針,在網上找資料,發現資料還是有很多的,但是例子太少,有的直接把公司代碼粘貼出來,太複雜了,有的又特別簡單不是我想要的例子, 我想要這樣的一個例子: jvm在運行,我想動態修改一個類,jvm在不用重啟的情況下, 自動載入新的類定義. 動態修改類定義,聽著感 ...


前言

最近因為公司需要,需要瞭解下java探針,在網上找資料,發現資料還是有很多的,但是例子太少,有的直接把公司代碼粘貼出來,太複雜了,有的又特別簡單不是我想要的例子, 我想要這樣的一個例子:

jvm在運行,我想動態修改一個類,jvm在不用重啟的情況下, 自動載入新的類定義. 動態修改類定義,聽著感覺就很酷. 本文將實現一個方法監控的例子, 開始方法是沒有監控的, 動態修改後, 方法執行結束會列印方法耗時.

Instrumentation介紹

使用 Instrumentation,開發者可以構建一個獨立於應用程式的代理程式(Agent),用來監測和協助運行在 JVM 上的程式,甚至能夠替換和修改某些類的定義。有了這樣的功能,開發者就可以實現更為靈活的運行時虛擬機監控和 Java 類操作了,這樣的特性實際上提供了一種虛擬機級別支持的 AOP 實現方式,使得開發者無需對 JDK 做任何升級和改動,就可以實現某些 AOP 的功能了。

在 Java SE 5 中,Instrument 要求在運行前利用命令行參數或者系統參數來設置代理類,在實際的運行之中,虛擬機在初始化之時(在絕大多數的 Java 類庫被載入之前),啟動instrumentation 的設置,從而可以在載入位元組碼之前,修改類的定義。

在 Java SE6 裡面,則更進一步,可以在jvm運行時,動態修改類定義,使用就更方便了,本文也主要是講著一種方式.

Instrumentation 類 定義如下:

 1 /*有兩種獲取Instrumentation介面實例的方法:
 2 1.以指示代理類的方式啟動JVM時。 在這種情況下,將Instrumentation實例傳遞給代理類的premain方法。
 3 2. JVM提供了一種在JVM啟動後的某個時間啟動代理的機制。 在這種情況下,將Instrumentation實例傳遞給代理代碼的agentmain方法。
 4 這些機制在包裝規範中進行了描述。
 5 代理獲取某個Instrumentation實例後,該代理可以隨時在該實例上調用方法。
 6 */
 7 public interface Instrumentation {
 8     //增加一個Class 文件的轉換器,轉換器用於改變 Class 二進位流的數據,參數 canRetransform 設置是否允許重新轉換。
 9     void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
10     //註冊一個轉換器
11     void addTransformer(ClassFileTransformer transformer);
12 
13     //刪除一個類轉換器
14     boolean removeTransformer(ClassFileTransformer transformer);
15 
16     boolean isRetransformClassesSupported();
17 
18     //在類載入之後,重新定義 Class。這個很重要,該方法是1.6 之後加入的,事實上,該方法是 update 了一個類。
19     void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
20 
21     boolean isRedefineClassesSupported();
22     /*此方法用於替換類的定義,而無需引用現有的類文件位元組,除了在常規JVM語義下會發生的初始化之外,此方法不會引起任何初始化。換句話說,重新定義類不會導致其初始化程式運行。靜態變數的值將保持調用前的狀態。
23 重新定義的類的實例不受影響。*/
24     void redefineClasses(ClassDefinition... definitions)
25         throws  ClassNotFoundException, UnmodifiableClassException;
26 
27     boolean isModifiableClass(Class<?> theClass);
28     //獲取所有已經載入的類
29     @SuppressWarnings("rawtypes")
30     Class[] getAllLoadedClasses();
31 
32     @SuppressWarnings("rawtypes")
33     Class[] getInitiatedClasses(ClassLoader loader);
34     //獲取一個對象的大小
35     long getObjectSize(Object objectToSize);
36    
37     void appendToBootstrapClassLoaderSearch(JarFile jarfile);
38     
39     void appendToSystemClassLoaderSearch(JarFile jarfile);
40     boolean isNativeMethodPrefixSupported();
41     void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix);
42 }
  • 其中addTransformer 和 retransformClasses 是有關聯的, addTransformer 註冊轉換器,retransformClasses 觸發轉換器.
  • redefineClass是除了Transformer 之外另外一中轉變類定義的方式.

Instrument的兩種方式

第一種: JVM啟動前靜態Instrument

使用Javaagent命令啟動代理程式。參數 javaagent 可以用於指定一個 jar 包,並且對該 java 包有2個要求:

  1. 這個 jar 包的 MANIFEST.MF 文件必須指定 Premain-Class 項。
  2. Premain-Class 指定的那個類必須實現 premain() 方法。

premain 方法,從字面上理解,就是運行在 main 函數之前的的類。當Java 虛擬機啟動時,在執行 main 函數之前,JVM 會先運行-javaagent所指定 jar 包內 Premain-Class 這個類的 premain 方法 。

在命令行輸入 java可以看到相應的參數,其中有 和 java agent相關的:

-agentlib:<libname>[=<選項>] 載入本機代理庫 <libname>, 例如 -agentlib:hprof
    另請參閱 -agentlib:jdwp=help 和 -agentlib:hprof=help
-agentpath:<pathname>[=<選項>]
    按完整路徑名載入本機代理庫
-javaagent:<jarpath>[=<選項>]
    載入 Java 編程語言代理, 請參閱 java.lang.instrument

從本質上講,Java Agent 是一個遵循一組嚴格約定的常規 Java 類。 上面說到 javaagent命令要求指定的類中必須要有premain()方法,並且對premain方法的簽名也有要求,簽名必須滿足以下兩種格式:

public static void premain(String agentArgs, Instrumentation inst)
    
public static void premain(String agentArgs)

JVM 會優先載入 帶 Instrumentation 簽名的方法,載入成功忽略第二種,如果第一種沒有,則載入第二種方法。這個邏輯在sun.instrument.InstrumentationImpl 類中.

如何使用javaagent?

使用 javaagent 需要幾個步驟:

  1. 定義一個 MANIFEST.MF 文件,必須包含 Premain-Class 選項,通常也會加入Can-Redefine-Classes 和 Can-Retransform-Classes 選項。
  2. 創建一個Premain-Class 指定的類,類中包含 premain 方法,方法邏輯由用戶自己確定。
  3. 將 premain 的類和 MANIFEST.MF 文件打成 jar 包。
  4. 使用參數 -javaagent: jar包路徑 啟動要代理的方法。

在執行以上步驟後,JVM 會先執行 premain 方法,大部分類載入都會通過該方法,註意:是大部分,不是所有。當然,遺漏的主要是系統類,因為很多系統類先於 agent 執行,而用戶類的載入肯定是會被攔截的。也就是說,這個方法是在 main 方法啟動前攔截大部分類的載入活動,既然可以攔截類的載入,那麼就可以去做重寫類這樣的操作,結合第三方的位元組碼編譯工具,比如ASM,javassist,cglib等等來改寫實現類。

MANIFREST.MF文件的常用配置:

Premain-Class :包含 premain 方法的類(類的全路徑名)

Agent-Class :包含 agentmain 方法的類(類的全路徑名)

Boot-Class-Path :設置引導類載入器搜索的路徑列表。查找類的特定於平臺的機制失敗後,引導類載入器會搜索這些路徑。按列出的順序搜索路徑。列表中的路徑由一個或多個空格分開。路徑使用分層 URI 的路徑組件語法。如果該路徑以斜杠字元(“/”)開頭,則為絕對路徑,否則為相對路徑。相對路徑根據代理 JAR 文件的絕對路徑解析。忽略格式不正確的路徑和不存在的路徑。如果代理是在 VM 啟動之後某一時刻啟動的,則忽略不表示 JAR 文件的路徑。(可選)

Can-Redefine-Classes :true表示能重定義此代理所需的類,預設值為 false(可選)

Can-Retransform-Classes :true 表示能重轉換此代理所需的類,預設值為 false (可選)

Can-Set-Native-Method-Prefix: true表示能設置此代理所需的本機方法首碼,預設值為 false(可選)

列舉一個premain 的例子:

 1 public class PreMainTraceAgent {
 2     public static void premain(String agentArgs, Instrumentation inst) {
 3         System.out.println("agentArgs : " + agentArgs);
 4         inst.addTransformer(new DefineTransformer(), true);
 5     }
 6 
 7     static class DefineTransformer implements ClassFileTransformer{
 8         @Override
 9         public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
10             System.out.println("premain load Class:" + className);
11             return classfileBuffer;
12         }
13     }
14 }

由於本文不關註這種靜態Instrumentation的方式,這裡只是做簡介,感興趣的可以去搜索下.

第二種動態Instrumentation的方式

在 Java SE 6 的 Instrumentation 當中,有一個跟 premain“並駕齊驅”的“agentmain”方法,可以在 main 函數開始運行之後再運行。

跟 premain 函數一樣, 開發者可以編寫一個含有“agentmain”函數的 Java 類:

由於本文不關註這種靜態Instrumentation的方式,這裡只是做簡介,感興趣的可以去搜索下.
第二種動態Instrumentation的方式

在 Java SE 6 的 Instrumentation 當中,有一個跟 premain“並駕齊驅”的“agentmain”方法,可以在 main 函數開始運行之後再運行。
跟 premain 函數一樣, 開發者可以編寫一個含有“agentmain”函數的 Java 類:

跟 premain 函數一樣,開發者可以在 agentmain 中進行對類的各種操作。其中的 agentArgs 和 Inst 的用法跟 premain 相同。

與“Premain-Class”類似,開發者必須在 manifest 文件裡面設置“Agent-Class”來指定包含 agentmain 函數的類。

可是,跟 premain 不同的是,agentmain 需要在 main 函數開始運行後才啟動,至於該方法如何運行,怎麼跟正在運行的jvm 關聯上, 就需要介紹下Attach API.

Attach API 不是 Java 的標準 API,而是 Sun 公司提供的一套擴展 API,用來向目標 JVM ”附著”(Attach)代理工具程式的。有了它,開發者可以方便的監控一個 JVM,運行一個外加的代理程式。

Attach API 很簡單,只有 2 個主要的類,都在 com.sun.tools.attach 包裡面: VirtualMachine 代表一個 Java 虛擬機,也就是程式需要監控的目標虛擬機,提供了 JVM 枚舉,Attach 動作和 Detach 動作(Attach 動作的相反行為,從 JVM 上面解除一個代理)等等 ; VirtualMachineDescriptor 則是一個描述虛擬機的容器類,配合 VirtualMachine 類完成各種功能。

下邊我們利用上邊說的實現一個監控方法執行耗時的例子: 定時執行一個方法,開始方法是沒有監控的, 方法重定義加上監控。

一個簡單的方法監控例子 

那麼我們想一下需要實現這個例子,需要幾個模塊.

  • 一個代理模塊(監控邏輯);
  • 一個main函數(運行的jvm);
  • 一個把上邊兩個模塊關聯在一起的程式.

從代理模塊開始:

1. 需要監控的TimeTest類:

/**
 * @ClassName TimeTest
 * @Author jiangyuechao
 * @Date 2020/1/20-10:36
 * @Version 1.0
 */
public class TimeTest {

    public static void sayHello( ){
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("sayhHello..........");
    }

    public static void sayHello2(String word){
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("sayhHello2.........."+word);
    }
}

2. 編寫agent 代碼

位元組碼轉換類:

 1 public class MyTransformer implements ClassFileTransformer {
 2 
 3     // 被處理的方法列表
 4     final static Map<String, List<String>> methodMap = new HashMap<String, List<String>>();
 5 
 6     public MyTransformer() {
 7         add("com.chaochao.java.agent.TimeTest.sayHello");
 8         add("com.chaochao.java.agent.TimeTest.sayHello2");
 9     }
10 
11     private void add(String methodString) {
12         String className = methodString.substring(0, methodString.lastIndexOf("."));
13         String methodName = methodString.substring(methodString.lastIndexOf(".") + 1);
14         List<String> list = methodMap.get(className);
15         if (list == null) {
16             list = new ArrayList<String>();
17             methodMap.put(className, list);
18         }
19         list.add(methodName);
20     }
21 
22     @Override
23     public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
24                             ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
25         System.out.println("className:"+className);
26         if (methodMap.containsKey(className)) {// 判斷載入的class的包路徑是不是需要監控的類
27             try {
28                 ClassPool classPool=new ClassPool();
29                 classPool.insertClassPath(new LoaderClassPath(loader));
30                 CtClass ctClass= classPool.get(className.replace("/","."));
31 //                CtMethod ctMethod= ctClass.getDeclaredMethod("run");
32                 CtMethod[] declaredMethods = ctClass.getDeclaredMethods();
33                 for (CtMethod ctMethod : declaredMethods) {
34                        //插入本地變數
35                     ctMethod.addLocalVariable("begin",CtClass.longType);
36                     ctMethod.addLocalVariable("end",CtClass.longType);
37 
38                     ctMethod.insertBefore("begin=System.currentTimeMillis();System.out.println(\"begin=\"+begin);");
39                     //前面插入:最後插入的放最上面
40                     ctMethod.insertBefore("System.out.println( \"埋點開始-1\" );");
41 
42                     ctMethod.insertAfter("end=System.currentTimeMillis();System.out.println(\"end=\"+end);");
43                     ctMethod.insertAfter("System.out.println(\"性能:\"+(end-begin)+\"毫秒\");");
44 
45                     //後面插入:最後插入的放最下麵
46                     ctMethod.insertAfter("System.out.println( \"埋點結束-1\" );");
47                 }
48                 return ctClass.toBytecode();
49             }  catch (NotFoundException | CannotCompileException|IOException e) {
50                 e.printStackTrace();
51             } 
52             return new byte[0];
53         }
54         else
55              System.out.println("沒找到.");
56         return null;
57     }
58     
59 }

上邊的類就是在方法前後加上耗時列印.

下邊是定義的AgentMainTest: 

import java.lang.instrument.Instrumentation;

public class AgentMainTest {
   //關聯後執行的方法
    public static void agentmain(String args, Instrumentation inst) throws Exception {
        System.out.println("Args:" + args);
        Class[] classes = inst.getAllLoadedClasses();
        for (Class clazz : classes) 
        {
           System.out.println(clazz.getName());
        }
        System.out.println("開始執行自定義MyTransformer");
        // 添加Transformer
        inst.addTransformer(new MyTransformer(),true);
        
        inst.retransformClasses(TimeTest.class);
    }
    
    public static void premain(String args, Instrumentation inst) throws Exception 
    {
        System.out.println("Pre Args:" + args);
        Class[] classes = inst.getAllLoadedClasses();
        for (Class clazz : classes) 
        {
           System.out.println(clazz.getName());
        }
    } 
}

MANIFREST.MF文件定義,註意最後一行是空格:

Manifest-Version: 1.0
Premain-Class: com.chaochao.java.agent.AgentMainTest
Agent-Class: com.chaochao.java.agent.AgentMainTest
Can-Redefine-Classes: true
Can-Retransform-Classes: true

 

代理模塊介紹完畢, 下邊是一個main函數程式.這個就很簡單了.

 1 public class TestMan {
 2 
 3     public static void main(String[] args) throws InterruptedException 
 4     {
 5         TimeTest tt = new TimeTest();
 6         tt.sayHello();
 7         tt.sayHello2("one");
 8         while(true)
 9         {
10             Thread.sleep(60000);
11             new Thread(new WaitThread()).start();  
12             tt.sayHello();
13             tt.sayHello2("two");
14         }
15     }
16      
17    static class WaitThread implements Runnable 
18    {
19         @Override  
20         public void run()
21         {
22             System.out.println("Hello"); 
23         }
24    }
25 }

最後一個關聯模塊:

/**
 * 
 * @author jiangyuechao
 *
 */
public class AttachMain {

    public static void main(String[] args) throws Exception{
        VirtualMachine vm = null;  
        String pid = null;
        List<VirtualMachineDescriptor> list = VirtualMachine.list();  
        for (VirtualMachineDescriptor vmd : list)  
        {
            System.out.println("pid:" + vmd.id() + ":" + vmd.displayName());
            if(vmd.displayName().contains("TestMan")) {
                pid = vmd.id();
            }
        }
        //E:\eclipse-workspace\JavaStudyAll\JVMStudy\target
       // String agentjarpath = "E:/jee-workspace/javaAgent/TestAgent.jar"; //agentjar路徑  
        String agentjarpath = "E:/jee-workspace/javaAgent/AgentMainTest.jar"; //agentjar路徑  
        vm = VirtualMachine.attach(pid);//目標JVM的進程ID(PID)  
        vm.loadAgent(agentjarpath, "This is Args to the Agent.");  
        vm.detach();  
      }

}

也很簡單, 第一步獲取pid ,第二步使用attach 方法關聯jvm.

上便代碼準備好了,那麼怎麼把他們運行起來呢, 需要幾步:

  1. 先把agent 代碼打包為jar 包
  2. 運行main 函數,執行agent

agent 打包

把agent代碼打包為普通的jar 包即可, 使用eclipse或intellij 都可以. 以eclipse 為例,只需要註意一步使用你寫好的MANIFREST文件 

但是我推薦使用另外一種方式,命令行的方式, 使用java 命令行直接來的, 既方便又快捷.

首先把需要的類放在一個文件夾下, javac編譯:

javac -encoding UTF-8 -classpath .;E:\tools\jdk1.8.0_65\lib\tools.jar;E:\eclipse-workspace\JavaStudyAll\JVMStudy\lib\javassist.jar; AgentMainTest.java MyTransformer.java

其中需要依賴tools.jar和 javassist jar包.

編譯後的class文件打包為jar包:

jar cvmf MANIFEST.MF AgentMainTest.jar AgentMainTest.class MyTransformer.class 

如下所示:

agent包准備好之後, 就簡單了,先運行main函數,啟動一個虛擬機. 運行入下:

sayhHello..........
sayhHello2..........one

運行AttachMain 類,關聯agent程式,就會看到如下的輸出:

可以看到 在方法執行結束後, 已經有了耗時的列印. 測試成功.

Instrumentation的局限性

大多數情況下,我們使用Instrumentation都是使用其位元組碼插樁的功能,或者籠統說就是類重定義(Class Redefine)的功能,但是有以下的局限性:

  1. premain和agentmain兩種方式修改位元組碼的時機都是類文件載入之後,也就是說必須要帶有Class類型的參數,不能通過位元組碼文件和自定義的類名重新定義一個本來不存在的類。
  2. 類的位元組碼修改稱為類轉換(Class Transform),類轉換其實最終都回歸到類重定義Instrumentation#redefineClasses()方法,此方法有以下限制:
    1. 新類和老類的父類必須相同;
    2. 新類和老類實現的介面數也要相同,並且是相同的介面;
    3. 新類和老類訪問符必須一致。 新類和老類欄位數和欄位名要一致;
    4. 新類和老類新增或刪除的方法必須是private static/final修飾的;
    5. 可以修改方法體。

除了上面的方式,如果想要重新定義一個類,可以考慮基於類載入器隔離的方式:創建一個新的自定義類載入器去通過新的位元組碼去定義一個全新的類,不過也存在只能通過反射調用該全新類的局限性。

參考:

https://www.cnblogs.com/rickiyang/p/11368932.html

https://www.ibm.com/developerworks/cn/java/j-lo-jse61/index.html 

 

轉發請註明出處: https://www.cnblogs.com/jycboy/p/12249472.html 

 


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

-Advertisement-
Play Games
更多相關文章
  • Java基礎系列1:深入理解Java數據類型 當初學習電腦的時候,教科書中對程式的定義是:程式=數據結構+演算法,Java基礎系列第一篇就聊聊Java中的數據類型。 本篇聊Java數據類型主要包括四個內容: Java基本類型 Java封裝類型 自動裝箱和拆箱 封裝類型緩存機制 Java基本類型 Ja ...
  • 最近在比賽一個項目 , 是給Dubbo寫一個負載均衡介面 , 其實dubbo已經實現了下麵四種, 所以他做的不是這個單面負載均衡, 需要做雙向負載均衡 , 負載均衡的權重取決於服務端,所以有些時候我們不知道如何計算權重, 權重受到很多因素影響 ,所以就需要動態考慮了. ...
  • 二叉查找樹(二叉搜索樹、二叉排序樹)的創建、增、刪、查、改。 main.cpp: #include <iostream> #include "BinarySearchTree.h" using namespace std; int main() { BinarySearchTree<int> bst ...
  • Redis詳解(二)——AOF 前言 RDB 持久化存在一個缺點是一定時間內做一次備份,如果redis意外down掉的話,就會丟失最後一次快照後的所有修改(數據有丟失)。對於數據完整性要求很嚴格的需求,怎麼解決呢? 本篇博客接著來介紹Redis的另一種持久化方式——AOF。 1、AOF簡介 Redi ...
  • Java基礎系列1:Java基本類型與封裝類型 當初學習電腦的時候,教科書中對程式的定義是:程式=數據結構+演算法,Java基礎系列第一篇就聊聊Java中的數據類型。 本篇聊Java數據類型主要包括兩個內容: Java基本類型 Java封裝類型 Java基本類型 Java基本類型分類、大小及表示範圍 ...
  • @RequestParam 和 @PathVariable 註解是用於從request中接收請求的,兩個都可以接收參數,關鍵點不同的是@RequestParam 是從request裡面拿取值,而 @PathVariable 是從一個URI模板裡面來填充 @RequestParam看下麵一段代碼: h ...
  • 本文將介紹以下內容:Windows下安裝scala運行環境,安裝編譯工具並簡單配置,實現著名的“Hello,World"。 一,Windows下安裝scala運行環境 1.配置jdk,因為scala的運行需要依靠jvm虛擬機,所以在使用scala時需要有java環境 2.官網下載scala包,點擊這 ...
  • 2020年,給自己定一個新目標————開始寫技術博客,將之前所學的內容重新複習並整理成一系列的文章,一來可以讓自己對這些基礎知識更加熟悉,二來方便於以後的複習查閱。 以前自己都是以筆記的形式將知識點記錄在有道雲筆記中,這樣可能造成由於時間緊或者懶,只是記錄了筆記,沒有去深刻的理解。所以乘著這次全面復 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...