為了簡寫這行代碼,我竟使用靜態和動態編譯技術

来源:https://www.cnblogs.com/jtea/archive/2023/10/31/17799649.html
-Advertisement-
Play Games

背景 在我們系統中有這麼一個需求,業務方會通過mq將一些用戶信息傳給我們,我們的服務處理完後,再將信息轉發給子系統。mq的內容如下: @Data public class Person { //第一部分 private Integer countryId; private Integer compa ...


背景

在我們系統中有這麼一個需求,業務方會通過mq將一些用戶信息傳給我們,我們的服務處理完後,再將信息轉發給子系統。mq的內容如下:

@Data
public class Person {
    
    //第一部分
    private Integer countryId;    
    private Integer companyId;
    private String uid;

    //第二部分
    private User userBaseInfo;
    private List<UserContact> contactList;
    private List<UserAddress> addressList;
    private UserEducation educationInfo;
    private UserProfession professionInfo;
    private List<Order> orderList;
    private List<Bill> billList;
    private List<UserMerchant> merchantList;
    private List<UserOperate> operateList;
    private BeneficialOwner beneficialOwnerInfo;
}    

主要分為兩部分,第一部分是用戶id,這部分用於唯一標識一個用戶,不會改變。第二部分是一些基礎信息,賬單、訂單、聯繫方式、地址等等,這部分信息內容經常增加。
後面業務新增了一個邏輯,會對第二部某些信息進行剔除,最後這部分信息如果還有,才轉發到子系統。所以開發同學新增這麼一個很長的條件判斷:

public static boolean isNull(BizData bizData) {
    return CollectionUtils.isEmpty(bizData.getBillList()) && CollectionUtils.isEmpty(bizData.getOrderList()) && CollectionUtils.isEmpty(bizData.getAddressList()) && CollectionUtils.isEmpty(bizData.getContactList()) && bizData.getEducationInfo() == null && bizData.getProfessionInfo() == null && bizData.getUserBaseInfo() == null && CollectionUtils.isEmpty(bizData.getMerchantList()) && CollectionUtils.isEmpty(bizData.getOperateList()) && bizData.getBeneficialOwnerInfo() == null;
}

在review代碼的時候,發現這裡是一個“坑”,是一個會變化的點,以後新增信息,很可能會漏了來改這裡,在我的開發過程中,最擔心的就是遇到這些會變化點又寫死邏輯的,過段時間我就會忘記,如果換個人接手,那更難以發現,容易出現bug。因為這個條件判斷並不會自動隨著我們新增欄位而自動修改,完全靠人記憶,容易遺漏。

思考

那有沒有辦法做到新增信息不需要修改這裡嗎,也就是isNull方法可以自動動態判斷屬性是否為空呢?
首先我們都會想到反射,通過反射可以讀取class所有欄位,每次處理都反射判斷一下欄位值是否為空即可做到動態判斷。但反射的性能太低了,對於我們來說這是個調用量非常大的方法,儘量做到不損失性能,所以反射不在本次考慮範圍內。

既然有不變和變化的兩部分,那麼我們可以先將其分離,將不變的抽取到一個基類去。為了簡化代碼,第二部分我們只保留兩個屬性。

@Data
public class PersonBase {
    
    //第一部分
    private Integer countryId;    
    private Integer companyId;
    private String uid;
}

@Data
public class Person extend PersonBase {
    
    //第二部分...
    private User userBaseInfo;
    private List<UserContact> contactList;
}

要動態生成isNull方法,可以先從結果反推是怎麼樣的。可以有如下兩種方式:
1、在原Person類新增一個isNull方法,這種方式的特點是我們可以直接通過對象直接調用方法,如:

@Data
public class Person extend PersonBase {
        
    private User userBaseInfo;
    private List<UserContact> contactList;

    public boolean isNull() {
        return this.userBaseInfo != null && this.contactList != null;
    }
}

2、動態新增一個類,動態新增一個isNull方法,參數是BizData。這種方式無法通過Preson對象調用方法,甚至無法直接通過生成類調用方法,因為動態類的名稱我們都無法預知。如:

public class Person$Generated {
    
    public boolean isNull(BizData bizData) {
        return bizData.getUserBaseInfo() != null && bizData.getContactList() != null;
    }
}

這就是我們本篇要解決的問題,通過靜態/動態編譯技術生成代碼。這裡靜態是指“編譯期”,也就是類和方法在編譯期間就存在了,動態是指“運行時”,意思編譯期間類還不存在,等程式運行時才被載入,鏈接,初始化。
這兩種方式大家實際都經常接觸到,lombok可以幫我們生成getter/setter,本質就是在編譯期為類新增方法,spring無處不在的動態代理就是運行時生成的類。

動態編譯

我們先來看動態編譯,因為動態編譯我們都比較熟,也比較簡單,在spring中隨處可見,例如我們熟悉的動態代理類就是動態生成的。
我們編寫的java代碼會先經過編譯稱為位元組碼,位元組碼再由jvm載入運行,所以動態生成類就是要編寫相應的位元組碼。
但由於java位元組碼太複雜了,需要熟悉各種位元組碼指令,一般我們不會直接編寫位元組碼代碼,會藉助位元組碼框架或工具來生成。例如查看簡單的hello world類的位元組碼,idea -> view -> show bytecode。

public class HelloWorld {

	public static void main(String[] args) {
		System.out.println("hello world");
	}
}

ASM 介紹

ASM是一個通用的 Java 位元組碼操作和分析框架。它可用於直接以二進位形式修改現有類或動態生成類。ASM 提供了一些常見的位元組碼轉換和分析演算法,可以從中構建自定義的複雜轉換和代碼分析工具。ASM 提供與其他 Java 位元組碼框架類似的功能,但重點關註性能。因為它的設計和實現儘可能小且儘可能快,所以它非常適合在動態系統中使用(但當然也可以以靜態方式使用,例如在編譯器中)。

接下來我們用asm來生成hello world,如下:

public class HelloWorldGenerator {
    public static void main(String[] args) throws Exception {
        // 創建一個ClassWriter,用於生成位元組碼
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
        
        // 定義類的頭部信息
        cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "HelloWorld", null, "java/lang/Object", null);

        // 生成預設構造函數
        MethodVisitor constructor = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
        constructor.visitCode();
        constructor.visitVarInsn(Opcodes.ALOAD, 0);
        constructor.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
        constructor.visitInsn(Opcodes.RETURN);
        constructor.visitMaxs(1, 1);
        constructor.visitEnd();

        // 生成main方法
        MethodVisitor mainMethod = cw.visitMethod(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);
        mainMethod.visitCode();

        // 列印"Hello, World!"到控制台
        mainMethod.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
        mainMethod.visitLdcInsn("Hello, World!");
        mainMethod.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);

        mainMethod.visitInsn(Opcodes.RETURN);
        mainMethod.visitMaxs(2, 2);
        mainMethod.visitEnd();

        // 完成類的生成
        cw.visitEnd();

        // 將生成的位元組碼寫入一個類文件
        byte[] code = cw.toByteArray();
        MyClassLoader classLoader = new MyClassLoader();
        Class<?> helloWorldClass = classLoader.defineClass("HelloWorld", code);
        
        // 創建一個實例並運行main方法
        helloWorldClass.getDeclaredMethod("main", String[].class).invoke(null, (Object) new String[0]);
    }

    // 自定義ClassLoader用於載入生成的類
    private static class MyClassLoader extends ClassLoader {
        public Class<?> defineClass(String name, byte[] b) {
            return defineClass(name, b, 0, b.length);
        }
    }
}

上面的代碼我是用chatgpt生成的,只需要輸入:“幫我用java asm位元組碼框架生成一個hello world,並註釋每行代碼寫明它的作用。”
相比直接編寫位元組碼指令,asm將其封裝成各種類和方法,方便我們理解和編寫,實際上asm還是比較底層的框架,所以許多框架會再它的基礎上繼續封裝,如cglib,byte buddy等。
可以看到生成的結果和我們自己編寫的是一樣的。

實現

接下來我們就用asm來動態生成上面的isNull方法,由於目標類是動態生成的,類名我們都不知道,但我們最終是要調用它的isNull方法,這怎麼辦呢?
我們可以定義一個介面,然後動態生成的類實現它,最終通過介面來調用它,這就是介面的好處之一,我們可以不關註具體類是誰,內部怎麼實現。

如定義介面如下:

public interface NullChecker<T> {

	/**
	 * 參數固定為origin
	 *
	 * @param origin 名稱必須為origin
	 */
	Boolean isNull(T origin);
}

這是個泛型介面,也就是所有類型都可以這麼用。isNull方法參數名稱必須為origin,因為在生成位元組碼時寫死了這個名稱。
接下來編寫核心的生成方法,如下:

public class ClassByteGenerator implements Opcodes {

	public static byte[] generate(Class originClass) {

		ClassWriter classWriter = new ClassWriter(0);
		MethodVisitor methodVisitor;

		//將.路徑替換為/
		String originClassPath = originClass.getPackage().getName().replace(".", "/") + "/" + originClass.getSimpleName();
		//動態生成類的名稱:原類$ASMGenerated
		String generateClassName = originClass.getSimpleName() + "$ASMGenerated";
		String generateClassPatch = ClassByteGenerator.class.getPackage().getName().replace(".", "/") + "/" + generateClassName;
		String nullCheckerClassPath = NullChecker.class.getPackage().getName().replace(".", "/") + "/" + NullChecker.class.getSimpleName();
		classWriter.visit(V1_8, ACC_PUBLIC | ACC_SUPER, generateClassPatch, null, "java/lang/Object", new String[]{nullCheckerClassPath});

		classWriter.visitSource(generateClassName + ".java", null);

		{
			methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
			methodVisitor.visitCode();
			Label label0 = new Label();
			methodVisitor.visitLabel(label0);
			methodVisitor.visitLineNumber(7, label0);
			methodVisitor.visitVarInsn(ALOAD, 0);
			methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
			methodVisitor.visitInsn(RETURN);
			Label label1 = new Label();
			methodVisitor.visitLabel(label1);
			methodVisitor.visitLocalVariable("this", "L" + generateClassPatch + ";", null, label0, label1, 0);
			methodVisitor.visitMaxs(1, 1);
			methodVisitor.visitEnd();
		}
		{
			methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "isNull", "(L" + originClassPath + ";)Ljava/lang/Boolean;", null, null);
			methodVisitor.visitParameter("origin", 0);
			methodVisitor.visitCode();

			Label label0 = new Label();
			methodVisitor.visitLabel(label0);
			methodVisitor.visitVarInsn(ALOAD, 1);
			Label label1 = new Label();

			int index = 0;
			//過濾掉基類的
			PropertyDescriptor[] propertyDescriptors = Arrays.stream(BeanUtils.getPropertyDescriptors(originClass))
					.filter(p -> p.getReadMethod().getDeclaringClass() == originClass)
					.toArray(PropertyDescriptor[]::new);
			for (PropertyDescriptor pd : propertyDescriptors) {
				String descriptor = "()" + Type.getDescriptor(pd.getPropertyType());
				if (index == 0) {
					methodVisitor.visitMethodInsn(INVOKEVIRTUAL, originClassPath, pd.getReadMethod().getName(), descriptor, false);
				} else if (index > 0 && index < propertyDescriptors.length - 1) {
					methodVisitor.visitJumpInsn(IFNULL, label1);
					methodVisitor.visitVarInsn(ALOAD, 1);
					methodVisitor.visitMethodInsn(INVOKEVIRTUAL, originClassPath, pd.getReadMethod().getName(), descriptor, false);
				} else {
					methodVisitor.visitJumpInsn(IFNULL, label1);
					methodVisitor.visitVarInsn(ALOAD, 1);
					methodVisitor.visitMethodInsn(INVOKEVIRTUAL, originClassPath, pd.getReadMethod().getName(), descriptor, false);
					methodVisitor.visitJumpInsn(IFNULL, label1);
					methodVisitor.visitInsn(ICONST_1);
				}
				index++;
			}

			Label label2 = new Label();
			methodVisitor.visitJumpInsn(GOTO, label2);
			methodVisitor.visitLabel(label1);
			methodVisitor.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
			methodVisitor.visitInsn(ICONST_0);
			methodVisitor.visitLabel(label2);
			methodVisitor.visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[]{Opcodes.INTEGER});
			methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/Boolean", "valueOf", "(Z)Ljava/lang/Boolean;", false);
			methodVisitor.visitInsn(ARETURN);
			Label label3 = new Label();
			methodVisitor.visitLabel(label3);
			methodVisitor.visitLocalVariable("this", "L" + generateClassPatch + ";", null, label0, label3, 0);
			methodVisitor.visitLocalVariable("origin", "L" + originClassPath + ";", null, label0, label3, 1);
			methodVisitor.visitMaxs(1, 2);
			methodVisitor.visitEnd();
		}

		{
			methodVisitor = classWriter.visitMethod(ACC_PUBLIC | ACC_BRIDGE | ACC_SYNTHETIC, "isNull", "(Ljava/lang/Object;)Ljava/lang/Boolean;", null, null);
			methodVisitor.visitParameter("origin", ACC_SYNTHETIC);
			methodVisitor.visitCode();
			Label label0 = new Label();
			methodVisitor.visitLabel(label0);
			methodVisitor.visitLineNumber(7, label0);
			methodVisitor.visitVarInsn(ALOAD, 0);
			methodVisitor.visitVarInsn(ALOAD, 1);
			methodVisitor.visitTypeInsn(CHECKCAST, originClassPath);
			methodVisitor.visitMethodInsn(INVOKEVIRTUAL, generateClassPatch, "isNull", "(L" + originClassPath + ";)Ljava/lang/Boolean;", false);
			methodVisitor.visitInsn(ARETURN);
			Label label1 = new Label();
			methodVisitor.visitLabel(label1);
			methodVisitor.visitLocalVariable("this", "L" + generateClassPatch + ";", null, label0, label1, 0);
			methodVisitor.visitMaxs(2, 2);
			methodVisitor.visitEnd();
		}
		classWriter.visitEnd();

		return classWriter.toByteArray();
	}
}

代碼有點長,可能你會想還是用chatgpt生成,但這種邏輯性比較強的它就無能為力了。不過我們還有工具可以生成它,我使用的是ASM Bytecode Viewer,idea中安裝插件即可。
首先將要實現的結果用代碼寫出來,然後右鍵使用ASM Bytecode Viewer,就可以看到對應的asm代碼。當然實際我們是要遍歷類的所有欄位,就是for迴圈遍歷屬性的那一部分,這需要自己寫,也不難,在插件生成代碼後稍微調整下即可。

public class MyPersonGenerated implements NullChecker<Person> {

	@Override
	public Boolean isNull(Person person) {
		return person.getUserBaseInfo() != null && person.getContactList() != null;
	}
}

八股文背多了就知道類生命周期是:載入 -> 鏈接(驗證,準備,解析) -> 初始化 -> 使用 -> 卸載。所以首先要使用ClassLoader將動態類載入到jvm,我們可以定義一個類繼承抽象類ClassLoader,調用它的defineClass。

public class MyClassLoader extends ClassLoader {

	public Class<?> defineClass(byte[] b) {
		return super.defineClass(null, b, 0, b.length);
	}
}

使用如下,當然實際情況中我們會將生成的NullChecker賦值給一個全局變數緩存,不用每次都newInstance創建。

MyClassLoader myClassLoader = new MyClassLoader();
byte[] bytes = ClassByteGenerator.generate(Person.class);
Class<?> personNullCheckerCls = myClassLoader.defineClass(bytes);
NullChecker personNullChecker = (NullChecker) personNullCheckerCls.newInstance();
boolean result = o.isNull(person);

也可以將生成類的位元組保存到文件,然後拖到idea觀察結果,如下:

try (FileOutputStream fos = new FileOutputStream("./Person$ASMGenerated.class")) {
	fos.write(bytes); // 將位元組數組寫入.class文件
} catch (IOException e) {
	throw e;
}

靜態編譯

看完動態編譯我們再看靜態編譯。java代碼編譯和執行的整個過程包含三個主要機制:1.java源碼編譯機制 2.類載入機制 3.類執行機制。其中java源碼編譯由3個過程組成:1.分析和輸入到符號表 2.註解處理 3.語義分析和生成class文件。如下:

在介紹mapstruct這篇時我們也有提到,其中主要就是在源碼編譯的註解處理階段,可以插入我們的自定義代碼。

例如我們新建工程,定義如下註解,它標識的類就會對應生成一個含isNull方法的類。其中RetentionPolicy.SOURCE表示在源碼階段生效,在運行時是讀不到這個註解的,lombok的註解也是這個道理。

@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface GenerateIsNullMethod {
	String value() default "";
}

接著編寫註解處理器,在發現GenerateIsNullMethod註解時,進入處理邏輯。

@SupportedAnnotationTypes("com.example.mapstruct.processor.GenerateIsNullMethod")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@AutoService(Processor.class)
public class IsNullAnnotationProcessor extends AbstractProcessor {

	private ProcessingEnvironment processingEnv;

	@Override
	public synchronized void init(ProcessingEnvironment processingEnv) {
		super.init(processingEnv);
		this.processingEnv = processingEnv;
	}

	@Override
	public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
		processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, "GenerateIsNullMethod start===");
		Set<? extends Element> set = roundEnv.getElementsAnnotatedWith(GenerateIsNullMethod.class);
		for (Element classElement : set) {
			generateIsNullMethod(classElement);
		}
		return true;
	}

	private void generateIsNullMethod(Element classElement) {
		//javapoet只能創建新的文件,不能修改https://github.com/square/javapoet/issues/505
		String packageName = processingEnv.getElementUtils().getPackageOf(classElement).toString();
		String className = classElement.getSimpleName().toString();
		String newClassName = className + "Ext";

		MethodSpec.Builder p = MethodSpec.methodBuilder("isNull")
				.addModifiers(Modifier.PUBLIC)
				.addModifiers(Modifier.STATIC)
				.addParameter(ClassName.bestGuess(packageName + "." + className), "p")
				.returns(Boolean.class);
		String statement = "return ";
		for (Element ee : classElement.getEnclosedElements()) {
			if (ee.getKind().isField()) {
				String eeName = ee.getSimpleName().toString();
				statement += "p.get" + eeName.substring(0, 1).toUpperCase() + eeName.substring(1, ee.getSimpleName().length()) + "()" + " != null && ";
			}
		}
		statement = statement.substring(0, statement.length() - 4);
		processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, statement + "===");
		MethodSpec isNullMethod = p.addStatement(statement).build();

		TypeSpec updatedClass = TypeSpec.classBuilder(newClassName)
				.addModifiers(Modifier.PUBLIC)
				.addMethod(isNullMethod)
				.build();

		JavaFile javaFile = JavaFile.builder(packageName, updatedClass)
				.build();
		try {
			javaFile.writeTo(processingEnv.getFiler());
		} catch (IOException e) {
			processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Failed to generate isNull method: " + e.getMessage());
		}
	}
}

@AutoService是google一個工具包,幫我們在META-INF/services路徑下生成配置,註解處理器才會生效。
這裡生成代碼使用到了javapoet工具,用於生成.java源文件。
其它的就都是在生成代碼了,需要註意的是,既然是在編譯期,那就不要想用運行時的東西,例如反射,都還沒到那個階段。
導入這個工程使用GenerateIsNullMethod標記Person類,編譯後就可以觀察到生成一個PersonExt的類,它的isNull方法會判斷Person參數每個屬性是否為空。
這裡我並沒有像lombok一樣在原類上新增方法,而是新增一個Ext類,因為那樣做要解析語法樹,比較複雜,我沒有實現,有興趣的可以參考lombok自己實現一下。

總結

本篇介紹瞭如何使用靜態/動態編譯生成代碼,這種方式在許多框架、工具都非常常見,只是我們平時比較少接觸到。
通過學習我們可以更好瞭解平時使用的技術的原理,知其然知其所以然,以後遇到類似的場景也能想到用這類解決方案來實現。

更多分享,歡迎關註我的github:https://github.com/jmilktea/jtea


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

-Advertisement-
Play Games
更多相關文章
  • 用python添加參數都是用的input函數,不能添加預設值也不能輸入help提示。 最近發現了2個更好用的庫分享給大家。 一、使用input庫。 這個使用很簡單,就不過多描述了。 def test(a,b): print(f"{a}+{b}=" + str(int(a)+int(b)) ) if ...
  • super相關的介紹文章看了無數遍,每次看得都雲里霧裡的,沒過多久就忘了,只模糊知道跟MRO有關,但是稍微一複雜就不知道怎麼回事了,本篇文章主要記錄我對super的理解 1.粗暴簡單的理解 super的作用就是執父類的方法,雖然這句話不完全對,但是也差不多是那麼個意思了。 比如以單繼承為例 clas ...
  • 需求:有一個vo類,該類繼承了一個實體類,獲取到vo對象後,需要將其中的null值轉為空字元串; 思路:傳入參數,用Object接收,利用反射獲取到該對象的所有欄位,並判斷置空; 由於一開始沒有考慮到父類的欄位獲取,導致時不時出現錯誤,因此這裡簡單記錄一下。 // 無需返回object,set後對象 ...
  • File --JAVA 構造方法 方法說明 public File (String pathname) 根據文件路徑創建對象 public File (String parent, String child) 根據父路徑名字字元串和子路徑名字元串創建文件對象 public File (String ...
  • 作者今天在開發一個後臺發送消息的功能時,由於需要給多個用戶發送消息,於是使用了 mybatis plus 提供的 saveBatch() 方法,在測試環境測試通過上預發佈後,測試反應發送消息介面很慢得等 5、6 秒,於是我就登錄預發佈環境查看執行日誌,發現是 mybatis plus 提供的 sav ...
  • 各種閑著沒事的 scanf 奇葩用法 然而這些卻很好用誒。 同理,scanf 可以拓展到 sscanf、fscanf~ 例題:P1580 yyy loves Easter_Egg I、P7911 網路連接 未計入更加奇葩的 C 語言用法,比如 %i %a 這種明顯等價的轉換字元。 基礎1:整數輸入 ...
  • 今天在對接支付寶 APP 支付的時候遇到了一個報錯,記錄下問題的排查過程~ 報錯過程 APP 中彈窗提示的報錯“商家訂單參數異常,請重新發起付款”,檢查了下參數感覺沒啥問題,不知道是啥問題導致的。 去官網搜了下,折騰排查了一遍,發現是環境問題,沒有切到沙箱環境導致的 (*/ω\*) 。 先放個官網提 ...
  • Gradle8.4構建SpringBoot多模塊項目 一、基本 1、版本 這個版本是Jdk8最後一個SpringBoot版本 軟體 版本 Gradle 8.4 SpringBoot 2.7.15 JDK 8 2、Gradle基本介紹 2.1、使用Wrapper方式構建 好處:統一gradle的版本 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...