About Java Agent
Java Agent的出現
在JDK1.5版本開始,Java增加了Instrumentation(Java Agent API)和JVMTI(JVM Tool Interface)功能,該功能可以實作JVM再加載某個class檔案對其位元組碼進行修改,也可以對已經加載的位元組碼進行一個重新的加載,而在1.6版本新增了attach(附加方式)方式,可以對運行中的Java行程插入Agent,Java Agent可以去實作位元組碼插樁、動態跟蹤分析等,比如RASP產品和Java Agent記憶體馬,
Java Agent運行模式
有兩種模式:
1、啟動Java程式時添加-javaagent(Instrumentation API實作方式)或-agentpath/-agentlib(JVMTI的實作方式)引數,如java -javaagent:/data/XXX.jar LingXeTest,
2、JDK1.6新增了attach(附加方式)方式,可以對運行中的Java行程附加Agent,
這兩種運行方式的最大區別在于第一種方式只能在程式啟動時指定Agent檔案,而attach方式可以在Java程式運行后根據行程ID動態注入Agent到JVM,
所以類似于想要注入Agent型記憶體馬,一般會用attach的方式,
Java Agent
Javaagent是java命令的一個引數,引數 javaagent 可以用于指定一個jar包
Java Agent和普通的Java類并沒有任何區別,普通的Java程式中規定了main方法為程式入口,而Java Agent則將premain(Agent模式)和agentmain(Attach模式)作為了Agent程式的入口,兩者所接受的引數是完全一致的,如下:
public static void premain(String args, Instrumentation inst) {} public static void agentmain(String args, Instrumentation inst) {}
而在Attach模式下的premain()方法有兩種寫法,如下:
public static void premain(String agentArgs, Instrumentation inst) public static void premain(String agentArgs)
JVM會去優先加載帶 Instrumentation 簽名的方法,加載成功忽略第二種,如果第一種沒有,則加載第二種方法,
Java Agent還限制了我們必須以jar包的形式運行或加載,我們必須將撰寫好的Agent程式打包成一個jar檔案,除此之外,Java Agent還強制要求了所有的jar檔案中必須包含/META-INF/MANIFEST.MF檔案,且該檔案中必須定義好Premain-Class(Agent模式)或Agent-Class:(Agent模式)配置,如:
Premain-Class: com.anbai.sec.agent.CrackLicenseAgent
Agent-Class: com.anbai.sec.agent.CrackLicenseAgent
如果我們需要修改已經被JVM加載過的類的位元組碼,那么還需要設定在MANIFEST.MF中添加
Can-Retransform-Classes: true或Can-Redefine-Classes: true,
javaagent引數相關:
-agentlib:<libname>[=<選項>] 加載本機代理庫 <libname>, 例如 -agentlib:hprof 另請參閱 -agentlib:jdwp=help 和 -agentlib:hprof=help -agentpath:<pathname>[=<選項>] 按完整路徑名加載本機代理庫 -javaagent:<jarpath>[=<選項>] 加載 Java 編程語言代理, 請參閱 java.lang.instrument jarpath 是指向代理程式 JAR 檔案的路徑,options 是代理選項,此開關可以在同一命令列上多次使用,從而創建多個代理程式,多個代理程式可以使用同一 jarpath,代理 JAR 檔案必須符合 JAR 檔案規范,下面的清單屬性是針對代理 JAR 檔案定義的: Premain-Class 代理類,即包含 premain 方法的類,此屬性是必需的,如果它不存在,JVM 將中止,注:這是類名,而不是檔案名或路徑, Boot-Class-Path 由引導類加載器搜索的路徑串列,路徑表示目錄或庫(在許多平臺上通常作為 jar 或 zip 庫被參考),查找類的特定于平臺的機制出現故障之后,引導類加載器會搜索這些路徑,按列出的順序搜索路徑,串列中的路徑由一個或多個空格分開,路徑使用分層 URI 的路徑組件的語法,如果該路徑以斜杠字符(“/”)開頭,則為絕對路徑,否則為相對路徑,相對路徑根據代理 JAR 檔案的絕對路徑決議,忽略格式不正確的路徑和不存在的路徑,此屬性是可選的, Can-Redefine-Classes 布林值(true 或 false,與大小寫無關),能夠重定義此代理所需的類,值如果不是 true,則被認為是 false,此屬性是可選的,默認值為 false, 代理 JAR 檔案附加到類路徑之后,
而關于java.lang.instrument包位于rt.jar,一共有5個檔案
原始碼簡介
其實這一部分把注釋翻譯過來,有些類和某些方法依舊不理解是什么意思,也有些看懂了但不知道怎么用,先鴿著,
ClassDefinition
public final class ClassDefinition { /** * 要重定義的類 */ private final Class<?> mClass; /** * 用于替換的本地 class ,為 byte 陣列 */ private final byte[] mClassFile; /** * 構造方法,使用提供的類和類檔案位元組創建一個新的 ClassDefinition 系結 */ public ClassDefinition( Class<?> theClass, byte[] theClassFile) { if (theClass == null || theClassFile == null) { throw new NullPointerException(); } mClass = theClass; mClassFile = theClassFile; } /** * 以下為 getter 方法 */ public Class<?> getDefinitionClass() { return mClass; } public byte[] getDefinitionClassFile() { return mClassFile; } }
ClassFileTransformer
ClassFileTransformer是一個轉換類檔案的代理介面,我們可以在獲取到Instrumentation物件后通過addTransformer方法添加自定義類檔案轉換器,
使用addTransformer方法可以注冊一個我們自定義的Transformer到Java Agent,當有新的類被JVM加載時JVM會自動回呼叫我們自定義的Transformer類的transform方法,傳入該類的transform資訊(類名、類加載器、類位元組碼等),我們可以根據傳入的類資訊決定是否需要修改類位元組碼,修改完位元組碼后我們將新的類位元組碼回傳給JVM,JVM會驗證類和相應的修改是否合法,如果符合類加載要求JVM會加載我們修改后的類位元組碼,
package java.lang.instrument; public interface ClassFileTransformer { /** * 類檔案轉換方法,重寫transform方法可獲取到待加載的類相關資訊 * * @param loader 定義要轉換的類加載器;如果是引導加載器,則為 null * @param className 類名,如:java/lang/Runtime * @param classBeingRedefined 如果是被重定義或重轉換觸發,則為重定義或重轉換的類;如果是類加載,則為 null * @param protectionDomain 要定義或重定義的類的保護域 * @param classfileBuffer 類檔案格式的輸入位元組緩沖區(不得修改) * @return 位元組碼byte陣列, */ byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer); }
重寫transform方法需要注意以下事項:
- ClassLoader如果是被Bootstrap ClassLoader(引導類加載器)所加載那么loader引數的值是空,
- 修改類位元組碼時需要特別注意插入的代碼在對應的ClassLoader中可以正確的獲取到,否則會報ClassNotFoundException,比如修改java.io.FileInputStream(該類由Bootstrap ClassLoader加載)時插入了我們檢測代碼,那么我們將必須保證FileInputStream能夠獲取到我們的檢測代碼類,
- JVM類名的書寫方式路徑方式:java/lang/String而不是我們常用的類名方式:java.lang.String,
- 類位元組必須符合JVM校驗要求,如果無法驗證類位元組碼會導致JVM崩潰或者VerifyError(類驗證錯誤),
- 如果修改的是retransform類(修改已被JVM加載的類),修改后的類位元組碼不得新增方法、修改方法引數、類成員變數,
- addTransformer時如果沒有傳入retransform引數(默認是false)就算MANIFEST.MF中配置了Can-Redefine-Classes: true而且手動呼叫了retransformClasses方法也一樣無法retransform,
- 卸載transform時需要使用創建時的Instrumentation實體,
Instrumentation
java.lang.instrument.Instrumentation是監測運行在JVM程式的Java API,利用Instrumentation我們可以實作如下功能:
- 動態添加或移除自定義的ClassFileTransformer(addTransformer/removeTransformer),JVM會在類加載時呼叫Agent中注冊的ClassFileTransformer;
- 動態修改classpath(appendToBootstrapClassLoaderSearch、appendToSystemClassLoaderSearch),將Agent程式添加到BootstrapClassLoader和SystemClassLoaderSearch(對應的是ClassLoader類的getSystemClassLoader方法,默認是sun.misc.Launcher$AppClassLoader)中搜索;
- 動態獲取所有JVM已加載的類(getAllLoadedClasses);
- 動態獲取某個類加載器已實體化的所有類(getInitiatedClasses),
- 重定義某個已加載的類的位元組碼(redefineClasses),
- 動態設定JNI前綴(setNativeMethodPrefix),可以實作Hook native方法,
- 重新加載某個已經被JVM加載過的類位元組碼retransformClasses),

原始碼如下:
public interface Instrumentation { //增加一個Class 檔案的轉換器,轉換器用于改變 Class 二進制流的資料,引數 canRetransform 設定是否允許重新轉換, void addTransformer(ClassFileTransformer transformer, boolean canRetransform); //在類加載之前,重新定義 Class 檔案,ClassDefinition 表示對一個類新的定義,如果在類加載之后,需要使用 retransformClasses 方法重新定義,addTransformer方法配置之后,后續的類加載都會被Transformer攔截,對于已經加載過的類,可以執行retransformClasses來重新觸發這個Transformer的攔截,類加載的位元組碼被修改后,除非再次被retransform,否則不會恢復, void addTransformer(ClassFileTransformer transformer); //洗掉一個類轉換器 boolean removeTransformer(ClassFileTransformer transformer); boolean isRetransformClassesSupported(); //在類加載之后,重新定義 Class,這個很重要,該方法是1.6 之后加入的,事實上,該方法是 update 了一個類, void retransformClasses(Class<?>... classes) throws UnmodifiableClassException; boolean isRedefineClassesSupported(); void redefineClasses(ClassDefinition... definitions) throws ClassNotFoundException, UnmodifiableClassException; boolean isModifiableClass(Class<?> theClass); @SuppressWarnings("rawtypes") Class[] getAllLoadedClasses(); @SuppressWarnings("rawtypes") Class[] getInitiatedClasses(ClassLoader loader); //獲取一個物件的大小 long getObjectSize(Object objectToSize); void appendToBootstrapClassLoaderSearch(JarFile jarfile); void appendToSystemClassLoaderSearch(JarFile jarfile); boolean isNativeMethodPrefixSupported(); void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix); }
Java Agent使用
前面都是理論,我們來簡單寫一個小Demo感受一下如何使用Java Agent技術,
Agent模式
大致分為以下流程(以-javaagent模式為例):
- 撰寫一個Agent類,其中定義
premain方法并呼叫Instrumentation#addTransformer方法添加一個自定義的Transformer - 自定義一個
Transformer類,實作Instrumentation介面,在transform方法中寫入自己想要的AOP邏輯 - 創建MANIFEST.MF檔案,可以手動寫也可以通過Maven的插件(pom.xml)
- 打包Agent的jar包
- 在需要使用JavaAgent的專案添加JVM啟動引數
-javaagent并指定我們打包好的jar
這里需要2個專案,1個為javaagent的jar包,另1個為被javaagent代理的類,最終在被代理類的main方法執行前先執行我們Agent中的premain方法
0x01 撰寫javaagent相關代碼
先創建一個Maven專案,其中創建一個Agent類,里面需要包含premain方法
package com.zh1z3ven; import java.lang.instrument.Instrumentation; public class Agent { public static void premain(String agentArgs, Instrumentation inst){ System.out.println("agentArgs"+agentArgs); inst.addTransformer(new DefineTransformer(),true);//呼叫addTransformer添加一個Transformer } }
創建DefineTransformer類,實作ClassFileTransformer介面
package com.zh1z3ven; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.security.ProtectionDomain; public class DefineTransformer implements ClassFileTransformer { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { System.out.println("premain load class"+className); //列印加載的類 return new byte[0]; } }
0x02 創建MANIFEST.MF檔案
手動創建的話需要在resources/META-INF目錄下創建MANIFEST.MF檔案,內容如下:注意多留一行空行
Manifest-Version: 1.0 Can-Redefine-Classes: true Can-Retransform-Classes: true Premain-Class: com.zh1z3ven.Agent
通過pom.xml中呼叫Maven的插件去創建該檔案
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<archive>
<!--自動添加META-INF/MANIFEST.MF -->
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Premain-Class>com.zh1z3ven.Agent</Premain-Class>
<Agent-Class>com.zh1z3ven.Agent</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>6</source>
<target>6</target>
</configuration>
</plugin>
</plugins>
</build>
打包好jar后,檔案會在jar包中
一些可能會用到的引數說明:
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(可選)
0x03 撰寫測驗類
隨意寫一個
public class a { public static void main(String[] args) { System.out.println("main Method"); } }
0x04 -javaagent模式啟動
JVM啟動引數添加
-javaagent:target/JavaAgent-1.0-SNAPSHOT.jar


執行main方法之前會加載所有的類,包括系統類和自定義類,而在ClassFileTransformer中會去攔截系統類和自己實作的類物件,邏輯則是在ClassFileTransformer實作類的transform方法中定義,
而在這里transform給我的感覺是類似于一個filter會去攔截/遍歷一些要在JVM中加載的類,而在transform方法中我們可以定義一些邏輯,比如if className== xxx時走入一個邏輯去實作AOP,而其中就可以利用如javassist技術修改位元組碼并作為transform方法的回傳值,這樣就在該類在JVM中加載前(-javaagent模式)修改了位元組碼
使用javassist修改位元組碼
這里在之前a類中新添加一個方法,并在Agent里我們自定義的Transformert中transform添加一個邏輯,使用javassist去修改我們a類中新添加的方法,
a類中新加一個call方法
package MemoryShell.JavaAgent; public class a { public static void main(String[] args) { System.out.println("main Method"); call(); } public static void call(){ System.out.println("say hello ..."); } }
DefineTransformer
package com.zh1z3ven; import javassist.*; import java.io.IOException; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.security.ProtectionDomain; public class DefineTransformer implements ClassFileTransformer { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { // System.out.println("premain load class"+className); //列印加載的類 if ("MemoryShell/JavaAgent/a".equals(className)){ try { ClassPool classPool = ClassPool.getDefault(); CtClass ctClass = classPool.get("MemoryShell.JavaAgent.a"); CtMethod call = ctClass.getDeclaredMethod("call"); // 列印后加了一個彈計算器的操作 String MethodBody = "{System.out.println(\"say hello ...\");" + "java.lang.Runtime.getRuntime().exec(\"open -a Calculator\");}"; call.setBody(MethodBody); byte[] bytes = ctClass.toBytecode(); //detach的意思是將記憶體中曾經被javassist加載過的a物件移除,如果下次有需要在記憶體中找不到會重新走javassist加載 ctClass.detach(); return bytes; } catch (NotFoundException e) { e.printStackTrace(); } catch (CannotCompileException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } return new byte[0]; } }
打成jar包,指定JVM引數后運行a類
-javaagent:target/JavaAgent-1.1-SNAPSHOT.jar

Attach api
在Java SE 6 以后在Instrumentation介面中提供了新的方法agentmain可以在 main 函式開始運行之后再運行,
//采用attach機制,被代理的目標程式VM有可能很早之前已經啟動,當然其所有類已經被加載完成,這個時候需要借助Instrumentation#retransformClasses(Class<?>... classes)讓對應的類可以重新轉換,從而激活重新轉換的類執行ClassFileTransformer串列中的回呼 public static void agentmain (String agentArgs, Instrumentation inst) public static void agentmain (String agentArgs)
同樣,agentmain 方法中帶Instrumentation引數的方法也比不帶優先級更高,開發者必須在MANIFEST.MF檔案里面設定“Agent-Class”來指定包含 agentmain 函式的類,
在Java6 以后實作啟動后加載的新實作是Attach api,Attach API 很簡單,只有 2 個主要的類,都在 com.sun.tools.attach 包里面:
- VirtualMachine 字面意義表示一個Java 虛擬機,也就是程式需要監控的目標虛擬機,提供了獲取系統資訊(比如獲取記憶體dump、執行緒dump,類資訊統計(比如已加載的類以及實體個數等), loadAgent,Attach 和 Detach (Attach 動作的相反行為,從 JVM 上面解除一個代理)等方法,可以實作的功能可以說非常之強大 ,該類允許我們通過給attach方法傳入一個jvm的pid(行程id),遠程連接到jvm上 ,代理類注入操作只是它眾多功能中的一個,通過loadAgent方法向jvm注冊一個代理程式agent,在該agent的代理程式中會得到一個Instrumentation實體,該實體可以 在class加載前改變class的位元組碼,也可以在class加載后重新加載,在呼叫Instrumentation實體的方法時,這些方法會使用ClassFileTransformer介面中提供的方法進行處理,
- VirtualMachineDescriptor 則是一個描述虛擬機的容器類,配合 VirtualMachine 類完成各種功能,
attach實作動態注入的原理如下:
通過VirtualMachine類的attach(pid)方法,便可以attach到一個運行中的java行程上,之后便可以通過loadAgent(agentJarPath)來將agent的jar包注入到對應的行程,然后對應的行程會呼叫agentmain方法,
Attach模式使用
0x01 在JavaAgent專案中新撰寫一個AgentMain類
package com.zh1z3ven; import java.lang.instrument.Instrumentation; public class AgentMain { public static void agentmain(String agentArgs, Instrumentation instrumentation) { instrumentation.addTransformer(new AgentMainTransformer(), true); } }
0x02 新建一個自定義的Transformer
transform方法中邏輯依舊是修改a類的call方法位元組碼去彈calc
package com.zh1z3ven; import javassist.*; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.security.ProtectionDomain; public class AgentMainTransformer implements ClassFileTransformer { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { if ("MemoryShell.JavaAgent.a".equals(className)) { try { ClassPool classPool = ClassPool.getDefault(); CtClass ctClass = classPool.get("MemoryShell.JavaAgent.a"); CtMethod call = ctClass.getDeclaredMethod("call"); // 列印后加了一個彈計算器的操作 String MethodBody = "{java.lang.Runtime.getRuntime().exec(\"open -a Calculator\");" + "System.out.println(\"say hello ...\");}"; call.setBody(MethodBody); byte[] bytes = ctClass.toBytecode(); return bytes; //detach的意思是將記憶體中曾經被javassist加載過的a物件移除,如果下次有需要在記憶體中找不到會重新走javassist加載 // ctClass.detach(); } catch (Exception e) { e.printStackTrace(); return classfileBuffer; } }else { return classfileBuffer; } } }
0x03 測驗AgentMainTest類
將jar通過jvm pid注入進來,使其修改a類中call方法的位元組碼
package MemoryShell.JavaAgent; import com.sun.tools.attach.*; import java.io.IOException; import java.util.List; public class AgentMainTest { public static void main(String[] args) { System.out.println("running JVM start "); List<VirtualMachineDescriptor> list = VirtualMachine.list(); // 尋找當前系統中所有運行著的JVM行程 for (VirtualMachineDescriptor vmd : list) { //如果虛擬機的名稱為 xxx 則 該虛擬機為目標虛擬機,獲取該虛擬機的 pid //然后加載 agent.jar 發送給該虛擬機 System.out.println(vmd.displayName()); //vmd.displayName()看到當前系統都有哪些JVM行程在運行 if (vmd.displayName().endsWith("MemoryShell.JavaAgent.AgentMainTest")) { VirtualMachine virtualMachine = null; try { virtualMachine = VirtualMachine.attach(vmd.id()); virtualMachine.loadAgent("/Users/xxxx/JavaSourceCode/JavaCode/JavaAgent/target/JavaAgent-1.0-SNAPSHOT.jar"); virtualMachine.detach(); } catch (AttachNotSupportedException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (AgentLoadException e) { e.printStackTrace(); } catch (AgentInitializationException e) { e.printStackTrace(); } } } } }
0x04 記得修改MANIFEST.MF或直接改pom.xml
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<archive>
<!--自動添加META-INF/MANIFEST.MF -->
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Agent-Class>com.zh1z3ven.AgentMain</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
0x05 打包,先運行測驗AgentMainTest類將jar注入進來使其修改a類的位元組碼,之后運行a的main方法,呼叫到call方法時是我們修改過后的位元組碼了,所以會彈calc

Agent模式與Attach模式小結:
- 上面Attach這種情況是修改的還沒被JVM加載的類,已加載的Java類是不會再被Agent處理的,這時候我們需要在Attach到目標行程后呼叫instrumentation.redefineClasses,讓JVM重新該Java類,這樣我們就可以使用Agent機制修改該類的位元組碼了,
public static void agentmain(String agentArgs, Instrumentation inst) throws UnmodifiableClassException, ClassNotFoundException { inst.addTransformer(new Transformer(), true); inst.retransformClasses(Class.forName("com.example.xxxclass")); }
- premain和agentmain兩種方式修改位元組碼的時機都是類檔案加載之后,也就是說必須要帶有Class型別的引數,不能通過位元組碼檔案和自定義的類名重新定義一個本來不存在的類,
- 類的位元組碼修改稱為類轉換(Class Transform),類轉換其實最終都回歸到類重定義Instrumentation#redefineClasses()方法,此方法有以下限制:
- 新類和老類的父類必須相同;
- 新類和老類實作的介面數也要相同,并且是相同的介面;
- 新類和老類訪問符必須一致, 新類和老類欄位數和欄位名要一致;
- 新類和老類新增或洗掉的方法必須是private static/final修飾的;
- 可以修改方法體,
- java agent 中的所有依賴,在原行程中的 classpath 中都要能找到,否則在注入時原行程會報錯NoClassDefFoundError,
- agent 行程的 classpath 中必須有 tools.jar(提供 VirtualMachine attach api ),jdk 默認有 tools.jar,jre 默認沒有,并且Linux和Windows之間是存在一個tools.jar適配問題,
Reference
https://www.cnblogs.com/nice0e3/p/14086165.html
https://www.cnblogs.com/rickiyang/p/11368932.html
https://su18.org/post/irP0RsYK1/
javasec.org
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/538027.html
標籤:其他
下一篇:第一次打靶
