在定位公司問題的時候,需要瞭解一下skywalking的相關知識,而agent就提上了日程。 官網文檔 Agent技術是Jdk在1.5版本之後,所提供的一個在jvm啟動前後對部分java類代理加強的機制。由於是直接修改位元組碼,並不會對業務代碼有註入,所以可以很好的應用於監控或者熱部署等場景。 正常所 ...
在定位公司問題的時候,需要瞭解一下skywalking的相關知識,而agent就提上了日程。
官網文檔
Agent技術是Jdk在1.5版本之後,所提供的一個在jvm啟動前後對部分java類代理加強的機制。由於是直接修改位元組碼,並不會對業務代碼有註入,所以可以很好的應用於監控或者熱部署等場景。
正常所提到的Agent一般都是部署成jar包的樣子,比如agent-1.0-SNAPSHOT.jar。
在這個jar包中,要添加一個MANIFEST.MF文件,在文件中指定jar包的代理類,比如下麵代碼中的Premain-Class。
在對應的代理類,要實現一個permain方法或者agentmain方法,這樣jvm可以通過MANIFEST找到類,通過類再找到對應的方法,從而進行加強,所以加強邏輯是在permain方法或者agentmain方法內部實現的。
Manifest-Version: 1.0
Built-By: qisi
Premain-Class: com.qisi.agent.InterviewAgent
Agent-Class: com.qisi.agent.InterviewAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Class-Path: byte-buddy-1.10.22.jar
Created-By: Apache Maven 3.8.1
Build-Jdk: 1.8.0_332
public class InterviewAgent {
public static void premain(String agentArgs, Instrumentation instrumentation) {
}
public static void agentmain(String agentArgs, Instrumentation instrumentation) {
}
}
而如果在permain或者agentmain方法打上debug可以發現,執行時是通過sun.instrument.InstrumentationImpl#loadClassAndCallPremain和sun.instrument.InstrumentationImpl#loadClassAndCallAgentmain兩個方法通過反射來執行到我們指定的類的。
Agent技術有兩種場景,一種是在jvm啟動之前,通過-javaagent:path來指定jar包,像是skywalking就是採用的這種方式;另一種則是在jvm啟動之後,通過attach指定的進程,對jvm中的類進行加強,arthas就是採用的這種方式。
在具體介紹這兩種方式之前,需要先講一下Instrumentation相關類和介面
java.lang.Instrumentation
Instrumentation
Instrumentation相關的類都在java.lang.Instrumentation包下,兩個異常,兩個介面,一個類。
兩個異常在這裡不做介紹,功能就像類名一樣。核心的其實是Instrumentation介面,本文僅關註紅框內的幾個方法。這幾個方法都是通過permain和agentmain獲取到的instrumentation實例進行的操作。
從時間發展來看,其中jdk1.5開始支持的是下麵幾個方法,也就是說在jdk5的時候,僅支持添加和移除類轉換器,且添加的類轉換器只能在載入和重定義的時候使用。就是說如果類沒有載入,那麼通過addTransformer方法註冊的ClassFileTransformer就可以對這個類進行增強,否則一旦類已經載入完畢,則只能通過redefineClasses,完全替換類定義再次觸發loadClass來增強
addTransformer(ClassFileTransformer transformer)
removeTransformer(ClassFileTransformer transformer)
isRedefineClassesSupported();//依賴於MANIFEST中的Can-Redefine-Classes值
redefineClasses(ClassDefinition... definitions)
而從jdk1.6開始,增加了一個retransformClasses的概念。retransform和redefine的區別,前者是在原有類的基礎上進行修改,後者則是完全重定義,不使用原有類做任何參考。
需要註意的事,只有在首次調用addTransformer時,將canRetransform設置為true的類,才可以被重新轉換。
addTransformer(ClassFileTransformer transformer, boolean canRetransform);
isRetransformClassesSupported();
retransformClasses(Class<?>... classes)//依賴於MANIFEST中的Can-Retransform-Classes值
isModifiableClass(Class<?> theClass);
ClassFileTransformer、ClassDefinition
這兩個類其實都是Instrumentation介面方法的入參,其中用的比較多的應該是ClassFileTransformer。這個類只有一個transform,jvm類載入的時候都會調用一遍這個方法。如果需要加強,那麼就利用給定的參數,進行位元組碼的改動,將改動後的位元組碼作為返回值返回;如果無需增強,則直接返回null即可。
byte[]
transform( ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer)
ClassDefinition也類似,不過是在對象里重新綁定class和byte的關係
public final class ClassDefinition {
/**
* The class to redefine
*/
private final Class<?> mClass;
/**
* The replacement class file bytes
*/
private final byte[] mClassFile;
實踐
MANIFEST.MF配置
在pom文件中添加下麵的代碼,根據需要修改參數值
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<addMavenDescriptor>false</addMavenDescriptor>
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Premain-Class>
com.qisi.agent.InterviewByteButtyAgent
</Premain-Class>
<Agent-Class>
com.qisi.agent.InterviewByteButtyAgent
</Agent-Class>
<Can-Redefine-Classes>
true
</Can-Redefine-Classes>
<Can-Retransform-Classes>
true
</Can-Retransform-Classes>
<Built-By>
qisi
</Built-By>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
-javaagent:
在這種方式下,起作用的是permain,也就是說-javaagent和permain方法是配套使用的。
核心就是添加一個自定義的ClassFileTransformer,可以另起一個類,也可以這樣匿名類。
如果只是熟悉流程可以像下麵一樣,直接列印一些日誌,不去修改類;
public static void premain(String agentArgs, Instrumentation instrumentation) {
System.out.println("enhance by premain,params:"+agentArgs);
instrumentation.addTransformer(new 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 classfileBuffer;
}
}, true);
}
如果要真實修改,需要引入asm javassist bytebuddy等修改位元組碼的框架。下麵這部分就是使用了bytebuddy,作用是讓任何類的testAgent方法,都返回固定值transformed
public static void premain(String agentArgs, Instrumentation instrumentation) throws ClassNotFoundException {
System.out.println("enhance by permain InterviewByteButtyAgent,params:"+agentArgs);
new AgentBuilder.Default().type(any()).transform(new AgentBuilder.Transformer() {
@Override
public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder, TypeDescription typeDescription, ClassLoader classLoader, JavaModule module) {
return builder.method(named("testAgent"))
.intercept(FixedValue.value("transformed"));
}
}).installOn(instrumentation);
}
編寫完之後,就可以在任意項目添加一個存在testAgent方法的進行嘗試了,比如
java -javaagent:/xxxx/path/agent-1.0-SNAPSHOT.jar=key1:value1,key2:value2 -jar AppDemo.jar
attach
agentmain
這種方式需要實現agentmain方法,和permian不太一樣的地方是需要在addTransformer之後觸發需要retransformClasses想要加強的類。
public static void agentmain(String agentArgs, Instrumentation instrumentation) {
System.out.println("enhance by agentmain,params:"+agentArgs);
instrumentation.addTransformer(new ClassFileTransformer() {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer)
throws IllegalClassFormatException {
System.out.println("agentmain load Class :" + className);
return classfileBuffer;
}
}, true);
try {
instrumentation.retransformClasses(Class.forName("com.qisi.mybatis.app.controller.FirstRequestController"));
} catch (UnmodifiableClassException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
同樣,提供一個bytebuddy的例子,下麵這個則是指定修改FirstRequestController的testAgent方法的返回值為transformed
public static void agentmain(String agentArgs, Instrumentation instrumentation) throws ClassNotFoundException {
System.out.println("enhance by agentmain InterviewByteButtyAgent,params:"+agentArgs);
//這裡RedefinitionStrategy必須註意,預設的DISABLED是不支持retransform
new AgentBuilder.Default().with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION).type(new AgentBuilder.RawMatcher() {
@Override
public boolean matches(TypeDescription typeDescription, ClassLoader classLoader, JavaModule module, Class<?> classBeingRedefined, ProtectionDomain protectionDomain) {
return typeDescription.getName().contains("FirstRequestController");
}
}).transform(new AgentBuilder.Transformer() {
@Override
public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder, TypeDescription typeDescription, ClassLoader classLoader, JavaModule module) {
System.out.println("enhance"+typeDescription.getName());
return builder.method(named("testAgent"))
.intercept(FixedValue.value("transformed"));
}
//這裡採用disableClassFormatChanges的方案,好像還可以使用advice
}).disableClassFormatChanges().installOn(instrumentation);
try {
instrumentation.retransformClasses(Class.forName("com.qisi.mybatis.app.controller.FirstRequestController"));
} catch (UnmodifiableClassException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
VirtualMachine
不同於-javaagent命令,這裡需要使用自jdk6開始提供的VirtualMachine類,在tool.jar包里
下麵的方法是我參考arthas寫的一個attach的流程,選擇我們想要attach的進程,然後載入我們上面寫好的jar包就好了。
public class AgentTest {
public static void main(String[] args) throws IOException, AttachNotSupportedException {
String pid = null;
try {
Process jps = Runtime.getRuntime().exec("jps");
InputStream inputStream = jps.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String line;
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
System.out.println("選擇要attach的進程");
pid= new Scanner(System.in).nextLine();
System.out.println("選擇的pid是"+pid);
} catch (IOException e) {
e.printStackTrace();
}
for (VirtualMachineDescriptor virtualMachineDescriptor : VirtualMachine.list()) {
if (virtualMachineDescriptor.id().equals(pid)){
VirtualMachine attach = VirtualMachine.attach(virtualMachineDescriptor);
try {
attach.loadAgent("/xxxxx/agent/target/agent-1.0-SNAPSHOT.jar","參數1,參數2");
} catch (AgentLoadException e) {
e.printStackTrace();
} catch (AgentInitializationException e) {
e.printStackTrace();
} finally {
attach.detach();
}
break;
}
}
}
}
參考文檔:
探秘 Java 熱部署二(Java agent premain)
JAVA熱更新1:Agent方式熱更 | 花隱間-JAVA游戲技術解決方案
ByteBuddy入門教程
本文來自博客園,作者:起司啊,轉載請註明原文鏈接:https://www.cnblogs.com/qisi/p/java_agent.html