關於java agent這裏只是做一個 簡單的介紹,因爲詳細的介紹官網上有很多地址:https://www.ibm.com/developerworks/cn/java/j-lo-jse61/index.html,爲了節省大家的時間。所以重點介紹應用場景已經應用方式。
案例:對一個應用程序的指定方法的調用增加耗時監控(在不修改原來應用代碼的情況下)
premain方式
public static void premain(String agentArgs, Instrumentation inst);
public static void premain (String agentArgs);
premain 顧名思義是在需要被代理的應用main方法執行前執行。但是個人認爲這種方式的侷限性太大了。如果需要對一個應用進行處理,需要停止應用。這在生產環境中危險是很大的。實用場景較少,所以本文不會重點對它進行說明。但是也會貼上一個簡單的應用的實現代碼。因爲坑相對於另一種方式較少。所以只貼代碼不進行詳細說明了。
agentTest工程:
MyTest:code
public class MyTest {
public static void main(String[] args) {
//MyTest myTest = new MyTest();
sayHello();
sayHello2("hello world11");
}
public static void sayHello() {
try {
Thread.sleep(2000);
System.out.println("hello world!!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void sayHello2(String hello) {
try {
Thread.sleep(1000);
System.out.println(hello);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
MANIFEST.MF
Manifest-Version: 1.0
Main-Class: test.demo.MyTest
javaagent4工程
AgentDemo
public class AgentDemo {
/**
* 該方法在main方法之前運行,與main方法運行在同一個JVM中
*
* @param agentArgs
* @param inst
*/
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("=========premain方法執行1========");
System.out.println(agentArgs);
// 添加Transformer
inst.addTransformer(new MyTransformer());
}
/**
* 如果不存在 premain(String agentArgs, Instrumentation inst)
* 則會執行 premain(String agentArgs)
*
*/
public static void premain(String agentArgs) {
System.out.println("=========premain方法執行2========");
System.out.println(agentArgs);
}
}
MyTransformer
```java
public class MyTransformer implements ClassFileTransformer {
final static String prefix = "\nlong startTime = System.currentTimeMillis();\n";
final static String postfix = "\nlong endTime = System.currentTimeMillis();\n";
final static Map<String, List<String>> classMapName = new ConcurrentHashMap<>();
public MyTransformer(){
add("test.demo.MyTest.sayHello");
add("test.demo.MyTest.sayHello2");
}
private void add(String className){
String classNameStr = className.substring(0,className.lastIndexOf("."));
String methodName = className.substring(className.lastIndexOf(".")+1);
List<String> lists = classMapName.get(classNameStr);
if(null == lists){
lists = new ArrayList<>();
classMapName.put(classNameStr,lists);
}
lists.add(methodName);
}
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
// 該路徑顯示方式
className = className.replace("/",".");
// 判斷傳入的類路徑是否在監控中
if( classMapName.containsKey(className)) {
CtClass ctclass = null;
try{
// 根據類全名獲取字節碼類信息
ctclass = ClassPool.getDefault().get(className);
for (String methodName : classMapName.get(className)) {
String outputStr = "\nSystem.out.println(\"this method " + methodName
+ " cost:\" +(endTime - startTime) +\"ms.\");";
System.out.println(outputStr);
// 根據方法名得到這方法實例
CtMethod ctMethod = ctclass.getDeclaredMethod(methodName);
// 新定義一個方法叫做比如sayHello$old
String newMethodName = methodName + "$old";
// 將原來的方法名字修改
ctMethod.setName(newMethodName);
// 創建新的方法,複製原來的方法,名字爲原來的名字
CtMethod newMethod = CtNewMethod.copy(ctMethod, methodName, ctclass, null);
// 構建新的方法體
StringBuilder bodyStr = new StringBuilder();
bodyStr.append("{");
bodyStr.append(prefix);
// 調用原有代碼,類似於method();($$)表示所有的參數
bodyStr.append(newMethodName + "($$);\n");
bodyStr.append(postfix);
bodyStr.append(outputStr);
bodyStr.append("}");
// 替換新方法
newMethod.setBody(bodyStr.toString());
// 增加新方法
ctclass.addMethod(newMethod);
}
return ctclass.toBytecode();
}catch (Exception e){
System.out.println(e.getMessage());
}
}
return null;
}
}
MANIFEST.MF
Manifest-Version: 1.0
Created-By: 0.0.1 (Demo Inc.)
Premain-Class: agent.AgentDemo
Premain-Class:指定步驟 1 當中編寫的那個帶有 premain 的 Java 類
用如下方式運行帶有 Instrumentation 的 Java 程序:
java -javaagent:jar 文件的位置 [= 傳入 premain 的參數 ]
agentmain方式
優勢:premain是靜態修改,在類加載之前修改; attach是動態修改,在類加載後修改要使premain生效重啓應用,而attach不重啓應用即可修改字節碼並讓其重新加載。
和premain類似 agentmain也有兩個類似的方法
public static void agentmain (String agentArgs, Instrumentation inst); // [1]
public static void agentmain (String agentArgs); // [2]
//[1] 的優先級比 [2] 高,將會被優先執行
agentmain 與 premain 不同在於agentmain需要在 main 函數開始運行後才啓動,既然是要在main函數開始運行後才啓動,那他的啓動時機如何確定,這就需要引出一個概念 Java SE 6 當中提供的 Attach API。
Attach API 很簡單,只有 2 個主要的類,都在 com.sun.tools.attach 包裏面: VirtualMachine 代表一個 Java 虛擬機,也就是程序需要監控的目標虛擬機,提供了 JVM 枚舉,Attach 動作和 Detach 動作(Attach 動作的相反行爲,從 JVM 上面解除一個代理)等等 ; VirtualMachineDescriptor 則是一個描述虛擬機的容器類,配合 VirtualMachine 類完成各種功能。整個過程其實和premain方式類似,主要的區別在於執行時機的不同。
先貼代和效果圖,最後在來說在實現過程中遇到的坑,以及解決方案。
兩個應用的結構非常簡單,因爲重點不是這裏所以隨意了些
MyApplication
public class MyApplication {
private static Logger logger = LogManager.getLogger(MyApplication.class);
public void run() throws Exception{
logger.info("run 運行...");
Run run = new Run();
for(;;){
run.run();
}
}
}
Launcher
public class Launcher {
// 主函數
public static void main(String[] args) throws Exception {
MyApplication myApplication = new MyApplication();
myApplication.run();
}
}
Run
public class Run {
private static final Logger logger = LogManager.getLogger(Run.class);
public void run() throws InterruptedException{
long sleep = (long)(Math.random() * 1000 + 200);
Thread.sleep(sleep);
logger.info("run in [{}] millis!", sleep);
}
}
MANIFEST.MF
Main-Class: com.demo.application.Launcher
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.demo</groupId>
<artifactId>agent</artifactId>
<version>1.0-SNAPSHOT</version>
<build>
<finalName>myAgent</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<archive>
<!--避免MANIFEST.MF被覆蓋-->
<manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
</archive>
<descriptorRefs>
<!--打包時加入依賴-->
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions>
<execution>
<id>make-assembly</id> <!-- this is used for inheritance merges -->
<phase>package</phase> <!-- bind to the packaging phase -->
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<!-- Project dependencies -->
<dependencies>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.11.1</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.11.1</version>
</dependency>
</dependencies>
</project>
##打包命令
mvn clean package
##執行命令
java -jar myAgent-jar-with-dependencies.jar
這個工程就是作爲我們在生產上運行的應用實例,雖然不會這麼簡單。這裏沒有什麼問題。我們甚至可以用springboot構建,只是表現形式不同而已。接下來重點來了
先貼代碼:
Launcher
public class Launcher {
private static Logger logger = LogManager.getLogger(Launcher.class);
public static void main(String[] args) {
//指定jar路徑
String agentFilePath = "myAcctach-jar-with-dependencies.jar";
//需要attach的進程標識
String applicationName = "myAgent";
//查到需要監控的進程
Optional<String> jvmProcessOpt = Optional.ofNullable(VirtualMachine.list()
.stream()
.filter(jvm -> {
logger.info("jvm:{}", jvm.displayName());
return jvm.displayName().contains(applicationName);
})
.findFirst().get().id());
if(!jvmProcessOpt.isPresent()) {
logger.error("Target Application not found");
return;
}
File agentFile = new File(agentFilePath);
try {
String jvmPid = jvmProcessOpt.get();
logger.info("Attaching to target JVM with PID: " + jvmPid);
VirtualMachine jvm = VirtualMachine.attach(jvmPid);
jvm.loadAgent(agentFile.getAbsolutePath());
jvm.detach();
logger.info("Attached to target JVM and loaded Java agent successfully");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
MyInstrumentationAgent
public class MyInstrumentationAgent {
private static Logger logger = LogManager.getLogger(MyInstrumentationAgent.class);
public static void agentmain(String agentArgs, Instrumentation inst) {
logger.info("[Agent] In agentmain method");
//需要監控的類
String className = "com.demo.application.Run";
transformClass(className, inst);
}
private static void transformClass(String className, Instrumentation instrumentation) {
Class<?> targetCls = null;
ClassLoader targetClassLoader = null;
// see if we can get the class using forName
try {
targetCls = Class.forName(className);
targetClassLoader = targetCls.getClassLoader();
transform(targetCls, targetClassLoader, instrumentation);
return;
} catch (Exception ex) {
logger.error("Class [{}] not found with Class.forName");
}
// otherwise iterate all loaded classes and find what we want
for(Class<?> clazz: instrumentation.getAllLoadedClasses()) {
if(clazz.getName().equals(className)) {
targetCls = clazz;
targetClassLoader = targetCls.getClassLoader();
transform(targetCls, targetClassLoader, instrumentation);
return;
}
}
throw new RuntimeException("Failed to find class [" + className + "]");
}
private static void transform(Class<?> clazz, ClassLoader classLoader, Instrumentation instrumentation) {
MyTransformer dt = new MyTransformer(clazz.getName(), classLoader);
instrumentation.addTransformer(dt, true);
try {
instrumentation.retransformClasses(clazz);
} catch (Exception ex) {
throw new RuntimeException("Transform failed for class: [" + clazz.getName() + "]", ex);
}
}
}
MyTransformer
public class MyTransformer implements ClassFileTransformer {
private static Logger logger = LogManager.getLogger(MyTransformer.class);
//需要監控的方法
private static final String WITHDRAW_MONEY_METHOD = "run";
/** The internal form class name of the class to transform */
private String targetClassName;
/** The class loader of the class we want to transform */
private ClassLoader targetClassLoader;
public MyTransformer(String targetClassName, ClassLoader targetClassLoader) {
this.targetClassName = targetClassName;
this.targetClassLoader = targetClassLoader;
}
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
byte[] byteCode = classfileBuffer;
String finalTargetClassName = this.targetClassName.replaceAll("\\.", "/"); //replace . with /
if (!className.equals(finalTargetClassName)) {
return byteCode;
}
if (className.equals(finalTargetClassName) && loader.equals(targetClassLoader)) {
logger.info("[Agent] Transforming class" + className);
try {
ClassPool cp = ClassPool.getDefault();
CtClass cc = cp.get(targetClassName);
CtMethod m = cc.getDeclaredMethod(WITHDRAW_MONEY_METHOD);
// 開始時間
m.addLocalVariable("startTime", CtClass.longType);
m.insertBefore("startTime = System.currentTimeMillis();");
StringBuilder endBlock = new StringBuilder();
// 結束時間
m.addLocalVariable("endTime", CtClass.longType);
endBlock.append("endTime = System.currentTimeMillis();");
// 時間差
m.addLocalVariable("opTime", CtClass.longType);
endBlock.append("opTime = endTime-startTime;");
// 打印方法耗時
endBlock.append("logger.info(\"completed in:\" + opTime + \" millis!\");");
m.insertAfter(endBlock.toString());
byteCode = cc.toBytecode();
cc.detach();
} catch (Exception e) {
logger.error("Exception", e);
}
}
return byteCode;
}
}
MANIFEST.MF
Main-Class: com.acttach.agent.Launcher
Agent-Class: com.acttach.agent.MyInstrumentationAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Permissions: all-permissions
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.acttch</groupId>
<artifactId>acttch</artifactId>
<version>1.0-SNAPSHOT</version>
<build>
<finalName>myAcctach</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<archive>
<!--避免MANIFEST.MF被覆蓋-->
<manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
</archive>
<descriptorRefs>
<!--打包時加入依賴-->
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions>
<execution>
<id>make-assembly</id> <!-- this is used for inheritance merges -->
<phase>package</phase> <!-- bind to the packaging phase -->
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<!-- Project dependencies -->
<dependencies>
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>1.8</version>
<scope>system</scope>
<systemPath>${java.home}/../lib/tools.jar</systemPath>
</dependency>
<!-- https://mvnrepository.com/artifact/org.javassist/javassist -->
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.24.1-GA</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.11.1</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.11.1</version>
</dependency>
</dependencies>
</project>
##打包命令
mvn clean package
##執行命令
java -Dfile.encoding=UTF-8 -Dsun.jnu.encoding=UTF-8 -Djava.ext.dirs="%JAVA_HOME%\lib" -jar myAcctach-jar-with-dependencies.jar
坑點1:
在打包完執行jar包時,最開始我是直接用
java -jar myAcctach-jar-with-dependencies.jar
出現了下面的錯誤
D:\litter\acttch\target>java -jar myAcctach-jar-with-dependencies.jar
Exception in thread "main" java.lang.NoClassDefFoundError: com/sun/tools/attach/VirtualMachine
at com.acttach.agent.Launcher.main(Launcher.java:31)
Caused by: java.lang.ClassNotFoundException: com.sun.tools.attach.VirtualMachine
at java.net.URLClassLoader.findClass(Unknown Source)
at java.lang.ClassLoader.loadClass(Unknown Source)
at sun.misc.Launcher$AppClassLoader.loadClass(Unknown Source)
at java.lang.ClassLoader.loadClass(Unknown Source)
... 1 more
tools.jar因爲是jre環境中的本地包,所以我們在打完包之後,實際上這個jar包是沒有被打進去的。所以在執行的時候要指定-Djava.ext.dirs 在網上找了很多文章他們都是這樣寫的
-Djava.ext.dirs=${JAVA_HOME}\lib -jar 說對於linux windows都可以。我也不知道他們有沒有驗證,反正這種方式在windows上行不通的。 我的windows上 只有 ava -Dfile.encoding=UTF-8 -Dsun.jnu.encoding=UTF-8 -Djava.ext.dirs="%JAVA_HOME%\lib" -jar myAcctach-jar-with-dependencies.jar 這樣纔行,至於爲什麼需要在%JAVA_HOME%\lib外層加上引號,因爲我的jdk路徑是在C:\Program Files 大家發現沒有中間有一個空格,如果你不加引號當做一個整體,windows下會給你切分。
前面那一部分沒有了。
坑2:
VirtualMachine jvm = VirtualMachine.attach(jvmPid);
// 要注意這裏是加載自身的jar進去 來對需要代理的應用進行處理。這裏不要弄混了。
// 本人就是在這個地方被磨了很久,一直報找不到jar.....~~~~(>_<)~~~~
jvm.loadAgent(agentFile.getAbsolutePath());
jvm.detach();
上面的截圖是隨機休眠一段時間並打印睡眠時間的方法
public class Run {
private static final Logger logger = LogManager.getLogger(Run.class);
public void run() throws InterruptedException{
long sleep = (long)(Math.random() * 1000 + 200);
Thread.sleep(sleep);
logger.info("run in [{}] millis!", sleep);
}
}
現在有一個需求在不改原來的代碼基礎上增加監控統計開始結束時間
這是在執行了另外一個應用之後產生的效果。
需要注意的地方基本上就是上面這幾個了。其實仔細想想這個技術還是挺有應用場景的。有興趣的不妨去學學。