1.字節(jié)碼
Java剛誕生的時(shí)候有一句非常著名的宣傳口號(hào):“一次編寫讥裤,到處運(yùn)行”。為了實(shí)現(xiàn)這個(gè)目的姻报,Sun公司以及其他虛擬機(jī)提供商發(fā)布了很多可以運(yùn)行在不同平臺(tái)上的jvm虛擬機(jī)己英,虛擬機(jī)的作用就是載入和執(zhí)行一種與平臺(tái)無關(guān)的字節(jié)碼。簡(jiǎn)單來說吴旋,java程序從編寫完成到運(yùn)行损肛,大致會(huì)有兩個(gè)階段,第一個(gè)階段是從.java文件編譯成.class文件荣瑟;第二階段是jvm載入.class文件治拿,進(jìn)行解釋和執(zhí)行。
為什么稱之為字節(jié)碼笆焰,而不叫比特碼呢劫谅?是因?yàn)樽止?jié)碼文件是采用十六進(jìn)制組成,jvm讀取的時(shí)候是以兩個(gè)十六進(jìn)制數(shù)為一組讀取嚷掠,我們知道一個(gè)十六進(jìn)制是4bit捏检,所以兩個(gè)十六進(jìn)制就是一個(gè)字節(jié),jvm便是按字節(jié)讀取不皆。
2.字節(jié)碼增強(qiáng)
我們修改字節(jié)碼有兩個(gè)過程:
1.修改已生成的字節(jié)碼(即.class文件)
2.重新加載更改后的字節(jié)碼贯城,使之生效
2.1 字節(jié)碼修改技術(shù)
字節(jié)碼修改技術(shù)通常包括以下幾類:
- ASM :一個(gè)輕量級(jí)的字節(jié)碼操作框架,直接涉及到j(luò)vm底層操作和指令霹娄,使用難度較大能犯。
- CGLIB:屬于動(dòng)態(tài)織入(字節(jié)碼加載之后)技術(shù),基于ASM實(shí)現(xiàn)项棠,性能高悲雳。同時(shí),CGLIB突破了Java動(dòng)態(tài)代理基于接口的限制香追,采用子類繼承的方式合瓢。
- JAVAssist:屬于動(dòng)態(tài)織入技術(shù),操作簡(jiǎn)單透典,接口強(qiáng)大晴楔,性能較ASM差顿苇。
- ASPECTJ:靜態(tài)織入(字節(jié)碼加載之前)框架,常用于AOP編程框架税弃。
2.2 使修改后的字節(jié)碼生效
我們這里只關(guān)注通過動(dòng)態(tài)織入框架定義的字節(jié)碼纪岁。可以通過JVMTI(JVM提供的一套對(duì)JVM操作的接口工具则果,通過接口注冊(cè)事件hook幔翰,在jvm事件觸發(fā)時(shí),同時(shí)觸發(fā)我們定義好的鉤子)西壮,將字節(jié)碼文件寫成一個(gè)agent遗增,并在java程序啟動(dòng)之后,通過Attach API(提供的jvm進(jìn)程之間通信的能力)的方式款青,動(dòng)態(tài)加載進(jìn)入虛擬機(jī)做修。
Talk is cheap.Show me the code.
下面我們采用最簡(jiǎn)單的JAVAssit+AttachAPI的方式編寫一套demo。
1.首先抡草,我們先模擬一個(gè)java進(jìn)程:
package demo;
import java.lang.management.ManagementFactory;
import java.util.concurrent.TimeUnit;
public class Application {
public static void main(String[] args) {
String name = ManagementFactory.getRuntimeMXBean().getName();
String s = name.split("@")[0];
System.out.println("pid:" + s);
while (true) {
boolean logined = login("admin", "111");
System.out.println((logined ? "成功" : "失敗") + " pid:" + s);
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static boolean login(String user, String passwd) {
System.out.println("login...");
if ("admin".equals(user) && "123".equals(passwd)) {
return true;
}
return false;
}
}
此程序會(huì)一直返回失敗饰及,并且打印出程序的進(jìn)程id。
2.接下來康震,我們用JVMTI接口編寫一個(gè)agent:
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
public class MyAgent {
public static void agentmain(String args, Instrumentation inst) throws UnmodifiableClassException {
inst.addTransformer(new MyTransformer(),true);
System.out.println("agent加載完畢");
for (Class aClass : inst.getAllLoadedClasses()) {
if(aClass.getName().contains("Application")){
System.out.println(aClass.getName());
inst.retransformClasses(aClass);
System.out.println("重新加載class完畢");
}
}
}
}
import javassist.*;
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import java.util.Objects;
public class MyTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println("我進(jìn)到transformer了:"+className);
if (!className.contains("Application")) {
return classfileBuffer;
}
ClassPool cp = ClassPool.getDefault();
try {
CtClass ctClass1 = cp.get("demo.Application");
CtClass ctClass2 = cp.get(className);
CtClass ctClass = Objects.isNull(ctClass1) ? ctClass2 : ctClass1;
CtMethod ctMethod = ctClass.getDeclaredMethod("login");
ctMethod.setBody("{return true;}");
System.out.println("修改class完畢");
return ctClass.toBytecode();
} catch (NotFoundException e) {
e.printStackTrace();
} catch (CannotCompileException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return classfileBuffer;
}
}
完成之后燎含,我們用編輯器或者jar命令將以上兩個(gè)類打成一個(gè)jar包,命名為javabyte.jar签杈,不管用什么方法瘫镇,最終保持jar包結(jié)構(gòu)如下:
然后下一步鼎兽,需要解壓jar答姥,修改里面的MANIFEST.MF文件,保持文件內(nèi)容與以下內(nèi)容一致:
Manifest-Version: 1.0
Agent-Class: MyAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Class-Path: javassist-3.24.1-GA.jar
Main-Class:
3.通過Attach API谚咬,動(dòng)態(tài)加載改過的字節(jié)碼
import com.sun.tools.attach.AgentInitializationException;
import com.sun.tools.attach.AgentLoadException;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;
import java.io.IOException;
public class Demo {
public static void main(String[] args) {
try {
VirtualMachine virtualMachine = VirtualMachine.attach("30421");
virtualMachine.loadAgent("javabyte.jar");
} catch (AttachNotSupportedException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (AgentLoadException e) {
e.printStackTrace();
} catch (AgentInitializationException e) {
e.printStackTrace();
}
}
}
注意:以上代碼中的路徑一定要跟自己工程路徑一致鹦付,比如:demo.Application,demo是我的包名择卦;javabyte.jar這個(gè)可以直接替換為jar的絕對(duì)路徑敲长。
操作步驟:
1.運(yùn)行1程序,會(huì)打印出進(jìn)程id
2.打包2程序
3.根據(jù)pid修改3程序秉继,運(yùn)行
結(jié)果如下:
運(yùn)行1程序:
運(yùn)行3程序:
如果程序運(yùn)行報(bào)錯(cuò)和tools有關(guān)祈噪,直接在項(xiàng)目里面添加依賴即可:
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>1.8.0</version>
<scope>system</scope>
<systemPath>/Library/Java/JavaVirtualMachines/jdk1.8.0_281.jdk/Contents/Home/lib/tools.jar</systemPath>
</dependency>
遇到問題也不用著急,可以打印各種日志來跟蹤你的程序運(yùn)行尚辑,并找到問題辑鲤。