重新對(duì)Java的類(lèi)加載器的學(xué)習(xí)方式
Java 類(lèi)加載器和JVM 類(lèi)加載器是同一體系中的不同概念。
Java 類(lèi)加載器是一個(gè)更高抽象層面的概念,可以實(shí)現(xiàn)自定義加載器;而 JVM 類(lèi)加載器是 JVM 的實(shí)現(xiàn)部分,負(fù)責(zé)實(shí)際的字節(jié)碼加載。
類(lèi)加載器:負(fù)責(zé)將.class文件加載到內(nèi)存中,并為之生成對(duì)應(yīng)的Class對(duì)象(是JVM執(zhí)行類(lèi)加載機(jī)制的前提)
1、介紹
1.1、簡(jiǎn)介
在Java中,類(lèi)加載器(ClassLoader)是Java虛擬機(jī)(JVM)用來(lái)加載類(lèi)的核心組件。
它負(fù)責(zé)將Java字節(jié)碼文件(.class文件)動(dòng)態(tài)加載到內(nèi)存中,并將其轉(zhuǎn)化為JVM可以執(zhí)行的類(lèi)對(duì)象。類(lèi)加載器是Java運(yùn)行時(shí)系統(tǒng)的一部分,它支持Java的動(dòng)態(tài)特性,使得Java程序可以在運(yùn)行時(shí)加載類(lèi)和接口。
如下圖所示:
1.2、符號(hào)引用和直接引用
關(guān)于符號(hào)引用和直接引用的介紹這里先進(jìn)行一個(gè)理解,下面在類(lèi)加載器執(zhí)行過(guò)程的連接階段,有個(gè)解析的過(guò)程需要聯(lián)系到這里的知識(shí)。
- 符號(hào)引用:指的是一個(gè)字符串,該字符串表示一個(gè)類(lèi)、字段或方法的名稱(chēng),這個(gè)名稱(chēng)由 JVM 在運(yùn)行時(shí)解析。
- 直接引用:指的是內(nèi)存中對(duì)象的具體地址或偏移量,它用于直接訪問(wèn)該字段或調(diào)用該方法。
1、符號(hào)引用
符號(hào)引用通常在 Java 字節(jié)碼(class
文件)中以字符串的形式出現(xiàn)。它是類(lèi)與類(lèi)之間一種相對(duì)的、靈活的引用方式。這種引用方式在字節(jié)碼編譯時(shí)就已經(jīng)確定,但實(shí)際內(nèi)存地址在運(yùn)行時(shí)才會(huì)分配。
示例:符號(hào)引用的特點(diǎn)
考慮一下這段代碼:
class Example { void hello() { System.out.println("Hello, World!"); } }
在編譯成字節(jié)碼后,hello
方法的符號(hào)引用會(huì)包含:
- 方法名:
hello
- 方法描述符:
()V
(意味著無(wú)參數(shù)且沒(méi)有返回值)
在字節(jié)碼中的常量池部分,可以看到以下條目(這里是簡(jiǎn)化的表示):
1. Class: 'Example' 2. Method: 'hello' with descriptor '()V'
這個(gè)符號(hào)引用不會(huì)包含任何內(nèi)存地址或具體實(shí)現(xiàn)細(xì)節(jié)。
2、直接引用
當(dāng) JVM 運(yùn)行時(shí)解析符號(hào)引用時(shí),它會(huì)將符號(hào)引用轉(zhuǎn)換為直接引用。直接引用是指向內(nèi)存中對(duì)象或方法入口的確切地址,這樣 JVM 就可以直接訪問(wèn)它們。
示例:直接引用的獲取過(guò)程
下面的例子展示了符號(hào)引用到直接引用的過(guò)程。
public class Main { public static void main(String[] args) { Example example = new Example(); // 創(chuàng)建 Example 對(duì)象 example.hello(); // 調(diào)用 hello 方法 } } class Example { void hello() { System.out.println("Hello, World!"); } }
在這個(gè)代碼中:
example.hello()
是調(diào)用hello
方法。在編譯時(shí),hello
方法的引用是符號(hào)引用。- 當(dāng) JVM 到達(dá)這行代碼時(shí),它首先會(huì)解析
hello
的符號(hào)引用。
3、符號(hào)轉(zhuǎn)直接的過(guò)程
1.指向類(lèi)定義:
在符號(hào)引用查找過(guò)程中,JVM 會(huì)查找并確認(rèn) Example
類(lèi)的符號(hào)引用,即它的名稱(chēng) Example
。
2.處理方法:
一旦 Example
類(lèi)被確認(rèn)可用,JVM 將查找 hello
方法的符號(hào)引用。當(dāng)它找到這個(gè)符號(hào)引用時(shí),它會(huì)定位到方法在內(nèi)存中的地址(也就是直接引用)。
這個(gè)直接引用通常是方法在內(nèi)存中相對(duì)于類(lèi)對(duì)象的偏移。
3.執(zhí)行調(diào)用:
最后,JVM 使用這個(gè)直接引用來(lái)調(diào)用 hello
方法,從而直接在內(nèi)存中找到并執(zhí)行方法的字節(jié)碼。
2、加載流程
Java是一個(gè)動(dòng)態(tài)語(yǔ)言,這意味著類(lèi)在程序運(yùn)行時(shí)被加載,而不是在編譯時(shí)完成加載。類(lèi)加載器的主要任務(wù)就是將類(lèi)的字節(jié)碼文件從文件系統(tǒng)或網(wǎng)絡(luò)等資源加載到內(nèi)存中。
具體而言,類(lèi)加載器的職責(zé)包括:
- 加載類(lèi):將Java字節(jié)碼文件讀取到內(nèi)存,并轉(zhuǎn)換為
Class
對(duì)象。 - 鏈接類(lèi):將類(lèi)的二進(jìn)制數(shù)據(jù)合并到JVM運(yùn)行時(shí)環(huán)境中。這一步包括驗(yàn)證、準(zhǔn)備和解析。
- 初始化類(lèi):執(zhí)行類(lèi)的靜態(tài)初始化塊和靜態(tài)變量的初始化。
關(guān)于上述各個(gè)階段的主要流程下面進(jìn)行了詳細(xì)的介紹。
由上圖可知:
ClassLoader在整個(gè)裝載階段,只能影響到類(lèi)的加載,而無(wú)法通過(guò)ClassLoader去改變類(lèi)的鏈接和初始化行為。
整個(gè)執(zhí)行過(guò)程可以分為三大步:加載、連接、初始化。
1.加載:將字節(jié)碼文件通過(guò)IO流讀取到JVM的方法區(qū),并同時(shí)在堆中生成Class對(duì)像。
2.鏈接:
- 驗(yàn)證:校驗(yàn)字節(jié)碼文件的正確性。
- 準(zhǔn)備:為類(lèi)的靜態(tài)變量分配內(nèi)存,并初始化為默認(rèn)值;對(duì)于final static修飾的變量,在編譯時(shí)就已經(jīng)分配好內(nèi)存了。
- 解析:將類(lèi)中的符號(hào)引用轉(zhuǎn)換為直接引用。
注意:如果類(lèi)加載后,未通過(guò)驗(yàn)證,則不能被使用。
3.初始化:對(duì)類(lèi)的靜態(tài)變量和靜態(tài)代碼塊初始化為指定的值,執(zhí)行靜態(tài)代碼。
- ClassLoader是Java的核心組件,所有的Class都是由ClassLoader進(jìn)行加載的。
- ClassLoader是否可以運(yùn)行,則由Execution Engine決定。
- ClassLoader負(fù)責(zé)通過(guò)各種方式將Class信息的二進(jìn)制數(shù)據(jù)流讀入JVM內(nèi)部,轉(zhuǎn)換為一個(gè)與目標(biāo)類(lèi)對(duì)應(yīng)的java.lang.Class對(duì)象實(shí)例,然后交給Java虛擬機(jī)進(jìn)行鏈接、初始化等操作。
3、類(lèi)加載的分類(lèi)
分為顯式加載 vs 隱式加載(即JVM加載class文件到內(nèi)存的方式)
3.1、顯示加載:
在代碼中顯示調(diào)用ClassLoader加載class對(duì)象。
實(shí)現(xiàn)方式
- 1.Class.forName(name)
- 2.this.getClass().
- 3.getClassLoader().loadClass() 。
加載class對(duì)象。
3.2、隱式加載:
通過(guò)虛擬機(jī)自動(dòng)加載到內(nèi)存中,是不直接在代碼中調(diào)用ClassLoader的方法加載class對(duì)象,類(lèi)在被引用(如調(diào)用靜態(tài)方法或訪問(wèn)靜態(tài)字段)時(shí)自動(dòng)加載。
如在加載某個(gè)類(lèi)的class文件時(shí),該類(lèi)的class文件中引用了另外一個(gè)類(lèi)的對(duì)象,此時(shí)額外引用的類(lèi)將通過(guò)JVM自動(dòng)加載到內(nèi)存中。
代碼示例:
// 隱式加載 User user = new User(); // 顯式加載,并初始化 Class clazz = Class.forName("com.test.java.User"); // 顯式加載,但不初始化 ClassLoader.getSystemClassLoader().loadClass("com.test.java.Parent");
心得:
- 隱式加載:為開(kāi)發(fā)者簡(jiǎn)化了加載過(guò)程,不需要顯式調(diào)用,通常在程序中不易察覺(jué)。
- 顯式加載:可用于動(dòng)態(tài)加載類(lèi),靈活控制加載時(shí)機(jī)。
4、命名空間
命名空間指的是在一定范圍內(nèi),標(biāo)識(shí)符(如類(lèi)名、變量名等)被唯一綁定到一個(gè)特定的實(shí)體。對(duì)于類(lèi)加載器而言,命名空間是指每個(gè)類(lèi)加載器有其各自的發(fā)現(xiàn)和加載類(lèi)的范圍,它負(fù)責(zé)自己加載的類(lèi)及其依賴(lài)關(guān)系。
4.1、類(lèi)加載器和命名空間的關(guān)系
1.隔離性
每個(gè)類(lèi)加載器都有一個(gè)獨(dú)立的命名空間。相同類(lèi)名的類(lèi)可以在不同的類(lèi)加載器中存在,而彼此不會(huì)干擾。
例如,一個(gè) JAR 中的 com.example.MyClass
可以通過(guò)不同的類(lèi)加載器各自加載,而不會(huì)發(fā)生沖突。
2.父類(lèi)優(yōu)先原則
當(dāng)一個(gè)類(lèi)加載器加載類(lèi)時(shí),它會(huì)首先將加載請(qǐng)求委托給它的父類(lèi)加載器。這種機(jī)制確保了核心類(lèi)庫(kù)得以?xún)?yōu)先加載,從而避免了相同名稱(chēng)的類(lèi)在不同上下文中出現(xiàn)。
4.2、示例
以下是一個(gè)簡(jiǎn)單的示例,展示不同類(lèi)加載器之間的命名空間如何影響類(lèi)的加載。
MyClass.java:
package com.example; public class MyClass { static { System.out.println("MyClass loaded!"); } }
CustomClassLoader.java:
import java.io.File; import java.io.FileInputStream; import java.io.IOException; public class CustomClassLoader extends ClassLoader { @Override protected Class<?> findClass(String name) throws ClassNotFoundException { String filePath = name.replace('.', File.separatorChar) + ".class"; try (FileInputStream fis = new FileInputStream(new File(filePath))) { byte[] b = new byte[fis.available()]; fis.read(b); return defineClass(name, b, 0, b.length); } catch (IOException e) { throw new ClassNotFoundException("Class not found: " + name, e); } } }
Main.java:
public class Main { public static void main(String[] args) { try { CustomClassLoader loader1 = new CustomClassLoader(); CustomClassLoader loader2 = new CustomClassLoader(); // 使用兩個(gè)自定義類(lèi)加載器加載同一類(lèi) Class<?> class1 = loader1.loadClass("com.example.MyClass"); Class<?> class2 = loader2.loadClass("com.example.MyClass"); // 檢查兩個(gè)類(lèi)加載器加載的類(lèi)是否相同 System.out.println("Are class1 and class2 the same? " + (class1 == class2)); // 創(chuàng)建實(shí)例 Object instance1 = class1.getDeclaredConstructor().newInstance(); Object instance2 = class2.getDeclaredConstructor().newInstance(); } catch (Exception e) { e.printStackTrace(); } } }
當(dāng)運(yùn)行 Main.java
時(shí),輸出可能會(huì)是:由于加載器不同。
MyClass loaded! MyClass loaded! Are class1 and class2 the same? false
解釋
- 雙重加載:由于我們使用了兩個(gè)不同的自定義類(lèi)加載器 (
loader1
和loader2
) 來(lái)加載同一個(gè)類(lèi)com.example.MyClass
,因此它們?cè)趦?nèi)存中生成了兩個(gè)不同的Class
實(shí)例。 - 命名空間隔離:
class1
和class2
雖然指向同一個(gè)類(lèi)的符號(hào)引用,但由于在不同的類(lèi)加載器中加載,它們擁有獨(dú)立的命名空間,因此class1 == class2
結(jié)果為false
。
總結(jié)
- 命名空間:為每個(gè)類(lèi)加載器創(chuàng)建了一個(gè)獨(dú)立的命名空間,使得相同名稱(chēng)的類(lèi)可以共存于不同的上下文中。
- 父類(lèi)優(yōu)先原則:用于提升類(lèi)加載的安全性,優(yōu)先通過(guò)父類(lèi)加載器查找類(lèi)。
- 自定義類(lèi)加載器:可以實(shí)現(xiàn)輕松管理類(lèi)加載過(guò)程,提供更多靈活性和控制權(quán)。
5、類(lèi)加載器的分類(lèi)
JVM支持兩種類(lèi)型的類(lèi)加載器,分別為引導(dǎo)類(lèi)加載器(Bootstrap ClassLoader)和自定義類(lèi)加載器(C z ClassLoader)。
- Bootstrap ClassLoader(啟動(dòng)類(lèi)加載器):
- Extension ClassLoader(擴(kuò)展類(lèi)加載器):
- Application ClassLoader(系統(tǒng)類(lèi)加載器):
自定義類(lèi)加載器:擴(kuò)展 java.lang.ClassLoader
代碼示例:
public class ClassTestLoader extends ClassLoader{ public static void main(String[] args) throws ClassNotFoundException { ClassLoader classLoader = new ClassTestLoader(); Class<?> clazz = classLoader.loadClass("com.ali.sls.test.Counter"); System.out.println("class loader:=="+ clazz.getClassLoader()); System.out.println("class loader:=="+ clazz.getClassLoader().getParent()); System.out.println("class loader:=="+ clazz.getClassLoader().getParent().getParent()); } } class loader:==sun.misc.Launcher$AppClassLoader@18b4aac2 class loader:==sun.misc.Launcher$ExtClassLoader@6433a2 class loader:==null
6、雙親委派
Java的類(lèi)加載機(jī)制采用了雙親委派模型(Parent Delegation Model)。該模型的核心思想是:當(dāng)一個(gè)類(lèi)加載器試圖加載某個(gè)類(lèi)時(shí),它會(huì)先將這個(gè)請(qǐng)求委托給父類(lèi)加載器,而不是自己直接加載。只有當(dāng)父類(lèi)加載器無(wú)法找到該類(lèi)時(shí),才由當(dāng)前類(lèi)加載器嘗試加載。
6.1、緩存機(jī)制
每個(gè)類(lèi)加載器(包括父類(lèi)加載器)都會(huì)維護(hù)一個(gè)緩存,用于存儲(chǔ)已經(jīng)加載過(guò)的類(lèi)。當(dāng)類(lèi)加載器收到加載請(qǐng)求時(shí),會(huì)首先檢查緩存中是否已經(jīng)加載過(guò)該類(lèi)。如果已經(jīng)加載過(guò),則直接返回緩存的類(lèi),而不會(huì)重新加載。
- 子類(lèi)加載器加載的類(lèi):
子類(lèi)加載器加載的類(lèi)會(huì)存儲(chǔ)在子類(lèi)加載器的緩存中,父類(lèi)加載器無(wú)法訪問(wèn)子類(lèi)加載器的緩存。
- 父類(lèi)加載器加載的類(lèi):
父類(lèi)加載器加載的類(lèi)會(huì)存儲(chǔ)在父類(lèi)加載器的緩存中,子類(lèi)加載器可以通過(guò)雙親委派機(jī)制訪問(wèn)父類(lèi)加載器的緩存。
因此,如果子類(lèi)加載器已經(jīng)加載了某個(gè)類(lèi),父類(lèi)加載器不會(huì)再次加載該類(lèi),因?yàn)楦割?lèi)加載器無(wú)法感知子類(lèi)加載器的緩存。
6.2、類(lèi)的唯一性
在JVM中,類(lèi)的唯一性是由 類(lèi)的全限定名 + 類(lèi)加載器 共同決定的。即使兩個(gè)類(lèi)加載器加載了同一個(gè)類(lèi)的字節(jié)碼,JVM也會(huì)將它們視為不同的類(lèi)。
如果子類(lèi)加載器加載了一個(gè)類(lèi),父類(lèi)加載器再次嘗試加載同一個(gè)類(lèi),JVM會(huì)認(rèn)為這是兩個(gè)不同的類(lèi)(因?yàn)轭?lèi)加載器不同)。這可能導(dǎo)致 類(lèi)沖突 或 類(lèi)型轉(zhuǎn)換異常,因?yàn)镴VM認(rèn)為這兩個(gè)類(lèi)是獨(dú)立的。
6.3、工作流程:
類(lèi)加載器接收到加載請(qǐng)求時(shí),首先將請(qǐng)求委派給父類(lèi)加載器。
如果父類(lèi)加載器能找到該類(lèi),則加載成功;如果父類(lèi)加載器無(wú)法加載該類(lèi),則由當(dāng)前類(lèi)加載器加載。這種機(jī)制確保了Java核心類(lèi)庫(kù)不會(huì)被用戶(hù)自定義的類(lèi)加載器替代或覆蓋。
6.4、如何打破
1:自定義類(lèi)加載器
我們將創(chuàng)建一個(gè)自定義的類(lèi)加載器,它直接加載某個(gè)特定包中的類(lèi),而不經(jīng)過(guò)父加載器。
import java.io.File; import java.io.FileInputStream; import java.io.IOException; public class MyClassLoader extends ClassLoader { private String classPath; public MyClassLoader(String classPath) { this.classPath = classPath; } @Override protected Class<?> loadClass(String name) throws ClassNotFoundException { // 檢查是否需要執(zhí)行自定義加載 if (name.startsWith("com.example")) { // 這里實(shí)際上不調(diào)用父類(lèi)加載器 return findClass(name); } // 否則,使用默認(rèn)的父類(lèi)加載器 return super.loadClass(name); } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { String filePath = classPath + File.separator + name.replace('.', File.separatorChar) + ".class"; try (FileInputStream fis = new FileInputStream(filePath)) { byte[] b = new byte[fis.available()]; fis.read(b); return defineClass(name, b, 0, b.length); } catch (IOException e) { throw new ClassNotFoundException("Class not found: " + name, e); } } }
解釋:
- 在
loadClass
方法中,我們直接處理以com.example
開(kāi)頭的類(lèi),調(diào)用findClass
來(lái)加載。而對(duì)于其他類(lèi),則調(diào)用super.loadClass(name)
,這表明默認(rèn)的父類(lèi)加載器將處理它。
注意:確保 com/example/MyClass.class
文件在 "path/to/classes"
目錄中。
2.使用 Thread
的上下文類(lèi)加載器
在某些情況下,也可以通過(guò)設(shè)置當(dāng)前線程的上下文類(lèi)加載器(context class loader)來(lái)打破雙親委派。
public class ContextClassLoaderExample { public static void main(String[] args) { // 設(shè)置自定義類(lèi)加載器為當(dāng)前線程的上下文類(lèi)加載器 Thread.currentThread().setContextClassLoader(new MyClassLoader("path/to/classes")); // 然后通過(guò)上下文類(lèi)加載器加載類(lèi) ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); try { Class<?> myClass = contextClassLoader.loadClass("com.example.MyClass"); // 直接調(diào)用上下文類(lèi)加載器 System.out.println("Loaded class: " + myClass.getName()); } catch (ClassNotFoundException e) { e.printStackTrace(); } } }
7、類(lèi)的卸載
Java 的 GC(垃圾回收)會(huì)在類(lèi)引用不存在時(shí)進(jìn)行類(lèi)的卸載。只有使用 ClassLoader
加載的類(lèi),如果其 ClassLoader 被卸載,該類(lèi)才會(huì)被卸載。
類(lèi)的卸載并不是一個(gè)強(qiáng)制性的操作,只有在特定條件下才會(huì)發(fā)生。
7.1、卸載條件
類(lèi)的卸載發(fā)生在以下情況下:
1.類(lèi)加載器被垃圾回收
當(dāng)沒(méi)有任何引用指向某個(gè)類(lèi)加載器時(shí),該類(lèi)加載器及其所加載的類(lèi)有可能被卸載。JVM 可以回收類(lèi)加載器,并同時(shí)卸載它加載的所有類(lèi)。
2.類(lèi)及其類(lèi)加載器均無(wú)法到達(dá)
類(lèi)和類(lèi)加載器不僅沒(méi)有引用,而且它們?cè)诔A砍?、棧幀等處也不再被引用時(shí),可以進(jìn)行卸載。
7.2、觸發(fā)條件
雖然部分類(lèi)可以在 Java 程序運(yùn)行時(shí)被卸載,但是的確沒(méi)有顯式的方法去卸載類(lèi),整個(gè)卸載過(guò)程是由 JVM 的垃圾回收器自動(dòng)處理。
以下是一些觸發(fā)類(lèi)卸載的條件。
1.動(dòng)態(tài)類(lèi)加載:
當(dāng)使用自定義類(lèi)加載器動(dòng)態(tài)加載類(lèi)時(shí),如果不再有引用指向這個(gè)類(lèi)和類(lèi)加載器,它們會(huì)被視為垃圾對(duì)象,從而可能被回收。
2.Classpath 變化:
如果應(yīng)用程序在運(yùn)行時(shí)改變了類(lèi)路徑(比如加載新版本的同名類(lèi)),舊的類(lèi)及其加載器可能被卸載。
以下是一個(gè)類(lèi)卸載的簡(jiǎn)單示例:
public class ClassUnloadingExample { public static void main(String[] args) { CustomClassLoader classLoader = new CustomClassLoader(); try { Class<?> clazz1 = classLoader.loadClass("com.example.MyClass"); // 使用 clazz1 Object instance = clazz1.getDeclaredConstructor().newInstance(); System.out.println("Class Loaded: " + clazz1.getName()); // 設(shè)置 classLoader 為 null,解除引用 classLoader = null; // 觸發(fā)垃圾回收 System.gc(); Thread.sleep(1000); // 確保 GC 有足夠時(shí)間運(yùn)行 // 這里將不會(huì)再有引用指向這個(gè)類(lèi),可能被卸載 System.out.println("Unloading classes..."); } catch (Exception e) { e.printStackTrace(); } } } // 自定義類(lèi)加載器 class CustomClassLoader extends ClassLoader { @Override protected Class<?> findClass(String name) throws ClassNotFoundException { // 加載類(lèi)的邏輯 // 假設(shè)類(lèi)文件框架完整 return super.findClass(name); } }
8、類(lèi)加載異常處理
在類(lèi)加載過(guò)程中可能會(huì)遇到的異常主要有:
ClassNotFoundException
: 當(dāng)請(qǐng)求的類(lèi)不存在時(shí)拋出。NoClassDefFoundError
: 類(lèi)存在但不再可用,通常是因?yàn)?class 文件被刪除或 JVM 啟動(dòng)時(shí)未找到。UnsupportedClassVersionError
: 由于 Java 版本不兼容導(dǎo)致的錯(cuò)誤。
總結(jié)
通過(guò)上述文章的介紹,希望可以幫助開(kāi)發(fā)者在項(xiàng)目日常中更加清晰了解java類(lèi)的加載機(jī)制原理。
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
SpringBoot實(shí)現(xiàn)定時(shí)任務(wù)的三種方式小結(jié)
這篇文章主要介紹了SpringBoot實(shí)現(xiàn)定時(shí)任務(wù)的三種方式小結(jié),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11Spring Boot 使用 Swagger 構(gòu)建 RestAPI 接口文檔
這篇文章主要介紹了Spring Boot 使用 Swagger 構(gòu)建 RestAPI 接口文檔,幫助大家更好的理解和使用Spring Boot框架,感興趣的朋友可以了解下2020-10-10Java中綴表達(dá)式轉(zhuǎn)后綴表達(dá)式實(shí)現(xiàn)方法詳解
這篇文章主要介紹了Java中綴表達(dá)式轉(zhuǎn)后綴表達(dá)式實(shí)現(xiàn)方法,結(jié)合實(shí)例形式分析了Java中綴表達(dá)式轉(zhuǎn)換成后綴表達(dá)式的相關(guān)算法原理與具體實(shí)現(xiàn)技巧,需要的朋友可以參考下2019-03-03SpringBoot+Quartz實(shí)現(xiàn)動(dòng)態(tài)定時(shí)任務(wù)
這篇文章主要為大家詳細(xì)介紹了springBoot+Quartz實(shí)現(xiàn)動(dòng)態(tài)定時(shí)任務(wù),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-09-09java Servlet 實(shí)現(xiàn)動(dòng)態(tài)驗(yàn)證碼圖片示例
這篇文章主要介紹了java Servlet 實(shí)現(xiàn)動(dòng)態(tài)驗(yàn)證碼圖片示例的資料,這里整理了詳細(xì)的代碼,有需要的小伙伴可以參考下。2017-02-02SpringBoot 整合Tess4J庫(kù)實(shí)現(xiàn)圖片文字識(shí)別案例詳解
Tess4J是一個(gè)基于Tesseract OCR引擎的Java接口,可以用來(lái)識(shí)別圖像中的文本,說(shuō)白了,就是封裝了它的API,讓Java可以直接調(diào)用,今天給大家分享一個(gè)SpringBoot整合Tess4j庫(kù)實(shí)現(xiàn)圖片文字識(shí)別的小案例2023-10-10Java實(shí)現(xiàn)文件和base64流的相互轉(zhuǎn)換功能示例
這篇文章主要介紹了Java實(shí)現(xiàn)文件和base64流的相互轉(zhuǎn)換功能,涉及Java文件讀取及base64 轉(zhuǎn)換相關(guān)操作技巧,需要的朋友可以參考下2018-05-05如何用java實(shí)現(xiàn)分頁(yè)查詢(xún)
這篇文章主要介紹了如何用java實(shí)現(xiàn)分頁(yè)查詢(xún),文中講解非常細(xì)致,代碼幫助大家更好的理解和學(xué)習(xí),感興趣的朋友可以了解下2020-06-06