关于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> 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 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
4.0.0
com.demo
agent
1.0-SNAPSHOT
myAgent
org.apache.maven.plugins
maven-compiler-plugin
1.8
org.apache.maven.plugins
maven-assembly-plugin
src/main/resources/META-INF/MANIFEST.MF
jar-with-dependencies
make-assembly
package
single
org.apache.logging.log4j
log4j-api
2.11.1
org.apache.logging.log4j
log4j-core
2.11.1
##打包命令
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 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
4.0.0
com.acttch
acttch
1.0-SNAPSHOT
myAcctach
org.apache.maven.plugins
maven-compiler-plugin
3.5.1
1.8
org.apache.maven.plugins
maven-assembly-plugin
src/main/resources/META-INF/MANIFEST.MF
jar-with-dependencies
make-assembly
package
single
com.sun
tools
1.8
system
${java.home}/../lib/tools.jar
org.javassist
javassist
3.24.1-GA
org.apache.logging.log4j
log4j-api
2.11.1
org.apache.logging.log4j
log4j-core
2.11.1
##打包命令
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);
}
}
现在有一个需求在不改原来的代码基础上增加监控统计开始结束时间
这是在执行了另外一个应用之后产生的效果。
需要注意的地方基本上就是上面这几个了。其实仔细想想这个技术还是挺有应用场景的。有兴趣的不妨去学学。