關(guān)于java agent這里只是做一個(gè) 簡(jiǎn)單的介紹,因?yàn)樵敿?xì)的介紹官網(wǎng)上有很多地址:https://www.ibm.com/developerworks/cn/java/j-lo-jse61/index.html,為了節(jié)省大家的時(shí)間。所以重點(diǎn)介紹應(yīng)用場(chǎng)景已經(jīng)應(yīng)用方式。
案例:對(duì)一個(gè)應(yīng)用程序的指定方法的調(diào)用增加耗時(shí)監(jiān)控(在不修改原來應(yīng)用代碼的情況下)
premain方式
public static void premain(String agentArgs, Instrumentation inst);
public static void premain (String agentArgs);
premain 顧名思義是在需要被代理的應(yīng)用main方法執(zhí)行前執(zhí)行。但是個(gè)人認(rèn)為這種方式的局限性太大了。如果需要對(duì)一個(gè)應(yīng)用進(jìn)行處理,需要停止應(yīng)用。這在生產(chǎn)環(huán)境中危險(xiǎn)是很大的。實(shí)用場(chǎng)景較少,所以本文不會(huì)重點(diǎn)對(duì)它進(jìn)行說明。但是也會(huì)貼上一個(gè)簡(jiǎn)單的應(yīng)用的實(shí)現(xiàn)代碼。因?yàn)榭酉鄬?duì)于另一種方式較少。所以只貼代碼不進(jìn)行詳細(xì)說明了。
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方法之前運(yùn)行,與main方法運(yùn)行在同一個(gè)JVM中
*
* @param agentArgs
* @param inst
*/
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("=========premain方法執(zhí)行1========");
System.out.println(agentArgs);
// 添加Transformer
inst.addTransformer(new MyTransformer());
}
/**
* 如果不存在 premain(String agentArgs, Instrumentation inst)
* 則會(huì)執(zhí)行 premain(String agentArgs)
*
*/
public static void premain(String agentArgs) {
System.out.println("=========premain方法執(zhí)行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("/",".");
// 判斷傳入的類路徑是否在監(jiān)控中
if( classMapName.containsKey(className)) {
CtClass ctclass = null;
try{
// 根據(jù)類全名獲取字節(jié)碼類信息
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);
// 根據(jù)方法名得到這方法實(shí)例
CtMethod ctMethod = ctclass.getDeclaredMethod(methodName);
// 新定義一個(gè)方法叫做比如sayHello$old
String newMethodName = methodName + "$old";
// 將原來的方法名字修改
ctMethod.setName(newMethodName);
// 創(chuàng)建新的方法,復(fù)制原來的方法,名字為原來的名字
CtMethod newMethod = CtNewMethod.copy(ctMethod, methodName, ctclass, null);
// 構(gòu)建新的方法體
StringBuilder bodyStr = new StringBuilder();
bodyStr.append("{");
bodyStr.append(prefix);
// 調(diào)用原有代碼,類似于method();($$)表示所有的參數(shù)
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 當(dāng)中編寫的那個(gè)帶有 premain 的 Java 類
用如下方式運(yùn)行帶有 Instrumentation 的 Java 程序:
java -javaagent:jar 文件的位置 [= 傳入 premain 的參數(shù) ]
agentmain方式
優(yōu)勢(shì):premain是靜態(tài)修改,在類加載之前修改; attach是動(dòng)態(tài)修改,在類加載后修改要使premain生效重啟應(yīng)用,而attach不重啟應(yīng)用即可修改字節(jié)碼并讓其重新加載。
和premain類似 agentmain也有兩個(gè)類似的方法
public static void agentmain (String agentArgs, Instrumentation inst); // [1]
public static void agentmain (String agentArgs); // [2]
//[1] 的優(yōu)先級(jí)比 [2] 高,將會(huì)被優(yōu)先執(zhí)行
agentmain 與 premain 不同在于agentmain需要在 main 函數(shù)開始運(yùn)行后才啟動(dòng),既然是要在main函數(shù)開始運(yùn)行后才啟動(dòng),那他的啟動(dòng)時(shí)機(jī)如何確定,這就需要引出一個(gè)概念 Java SE 6 當(dāng)中提供的 Attach API。
Attach API 很簡(jiǎn)單,只有 2 個(gè)主要的類,都在 com.sun.tools.attach 包里面: VirtualMachine 代表一個(gè) Java 虛擬機(jī),也就是程序需要監(jiān)控的目標(biāo)虛擬機(jī),提供了 JVM 枚舉,Attach 動(dòng)作和 Detach 動(dòng)作(Attach 動(dòng)作的相反行為,從 JVM 上面解除一個(gè)代理)等等 ; VirtualMachineDescriptor 則是一個(gè)描述虛擬機(jī)的容器類,配合 VirtualMachine 類完成各種功能。整個(gè)過程其實(shí)和premain方式類似,主要的區(qū)別在于執(zhí)行時(shí)機(jī)的不同。
先貼代和效果圖,最后在來說在實(shí)現(xiàn)過程中遇到的坑,以及解決方案。
兩個(gè)應(yīng)用的結(jié)構(gòu)非常簡(jiǎn)單,因?yàn)橹攸c(diǎn)不是這里所以隨意了些

MyApplication
public class MyApplication {
private static Logger logger = LogManager.getLogger(MyApplication.class);
public void run() throws Exception{
logger.info("run 運(yùn)行...");
Run run = new Run();
for(;;){
run.run();
}
}
}
Launcher
public class Launcher {
// 主函數(shù)
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>
<!--打包時(shí)加入依賴-->
<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
##執(zhí)行命令
java -jar myAgent-jar-with-dependencies.jar
這個(gè)工程就是作為我們?cè)谏a(chǎn)上運(yùn)行的應(yīng)用實(shí)例,雖然不會(huì)這么簡(jiǎn)單。這里沒有什么問題。我們甚至可以用springboot構(gòu)建,只是表現(xiàn)形式不同而已。接下來重點(diǎn)來了

先貼代碼:
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的進(jìn)程標(biāo)識(shí)
String applicationName = "myAgent";
//查到需要監(jiān)控的進(jìn)程
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");
//需要監(jiān)控的類
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);
//需要監(jiān)控的方法
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);
// 開始時(shí)間
m.addLocalVariable("startTime", CtClass.longType);
m.insertBefore("startTime = System.currentTimeMillis();");
StringBuilder endBlock = new StringBuilder();
// 結(jié)束時(shí)間
m.addLocalVariable("endTime", CtClass.longType);
endBlock.append("endTime = System.currentTimeMillis();");
// 時(shí)間差
m.addLocalVariable("opTime", CtClass.longType);
endBlock.append("opTime = endTime-startTime;");
// 打印方法耗時(shí)
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>
<!--打包時(shí)加入依賴-->
<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
##執(zhí)行命令
java -Dfile.encoding=UTF-8 -Dsun.jnu.encoding=UTF-8 -Djava.ext.dirs="%JAVA_HOME%\lib" -jar myAcctach-jar-with-dependencies.jar
坑點(diǎn)1:
在打包完執(zhí)行jar包時(shí),最開始我是直接用
java -jar myAcctach-jar-with-dependencies.jar
出現(xiàn)了下面的錯(cuò)誤
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因?yàn)槭莏re環(huán)境中的本地包,所以我們?cè)诖蛲臧螅瑢?shí)際上這個(gè)jar包是沒有被打進(jìn)去的。所以在執(zhí)行的時(shí)候要指定-Djava.ext.dirs 在網(wǎng)上找了很多文章他們都是這樣寫的
-Djava.ext.dirs=${JAVA_HOME}\lib -jar 說對(duì)于linux windows都可以。我也不知道他們有沒有驗(yàn)證,反正這種方式在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外層加上引號(hào),因?yàn)槲业膉dk路徑是在C:\Program Files 大家發(fā)現(xiàn)沒有中間有一個(gè)空格,如果你不加引號(hào)當(dāng)做一個(gè)整體,windows下會(huì)給你切分。

前面那一部分沒有了。
坑2:
VirtualMachine jvm = VirtualMachine.attach(jvmPid);
// 要注意這里是加載自身的jar進(jìn)去 來對(duì)需要代理的應(yīng)用進(jìn)行處理。這里不要弄混了。
// 本人就是在這個(gè)地方被磨了很久,一直報(bào)找不到j(luò)ar.....~~~~(>_<)~~~~
jvm.loadAgent(agentFile.getAbsolutePath());
jvm.detach();

上面的截圖是隨機(jī)休眠一段時(shí)間并打印睡眠時(shí)間的方法
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);
}
}
現(xiàn)在有一個(gè)需求在不改原來的代碼基礎(chǔ)上增加監(jiān)控統(tǒng)計(jì)開始結(jié)束時(shí)間

這是在執(zhí)行了另外一個(gè)應(yīng)用之后產(chǎn)生的效果。
需要注意的地方基本上就是上面這幾個(gè)了。其實(shí)仔細(xì)想想這個(gè)技術(shù)還是挺有應(yīng)用場(chǎng)景的。有興趣的不妨去學(xué)學(xué)。