javaagnet为插桩提供入口,javassist实现字节码修改,这部分将记录3个demo,分别是静态agent结合javassist修改方法体,动态agent结合javassist修改方法体,最后一个demo综合使用javaagent和javassist监听c3p0数据源
静态agent修改方法
编写一个用于被篡改的类Calculate,示例代码如下:
12345678910public class Calculate {/*** 输入什么数字就返回什么数字* @param a 输入的数字* @return 输入的数字*/public static int getResult(int a){return a;}}编写ClassTransform的实现类和静态agent类,配置pom文件(参考 APM1 ),该类在Calculate类进行装载时会篡改他的字,使得他的getResult()方法返回输入数值的双倍,完毕后记得打包,示例代码如下:
1234567891011121314151617181920212223242526272829303132333435363738package com.transformer;import javassist.*;import java.io.IOException;import java.lang.instrument.ClassFileTransformer;import java.lang.instrument.IllegalClassFormatException;import java.security.ProtectionDomain;public class FirstAhentTransformer implements ClassFileTransformer {public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {String modifyClassName1 = "com/pojo/Calculate";//要修改的类String modifyClassName = "com.pojo.Calculate";//要修改的类if(modifyClassName1.equals(className)){ClassPool pool = new ClassPool();LoaderClassPath loaderClassPath = new LoaderClassPath(loader);pool.insertClassPath(loaderClassPath);try {CtClass ctClass = pool.get(modifyClassName);CtMethod resultMethod = ctClass.getDeclaredMethod("getResult");//添加一行a = 2*a;,如果成功该方法返回值应该是2aresultMethod.insertBefore("$1 = 2*$1;");return ctClass.toBytecode();} catch (NotFoundException e) {System.out.println("类没找到");e.printStackTrace();} catch (CannotCompileException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();}}//返回一个null表示,仍然装载之前的class,agent并没有对类进行修改return null;}}
|
|
启动方法,测试,注意添加javagent的jvm启动参数,示例代码如下:
12345678910package com.test;import com.pojo.Calculate;public class FirstAgentMain {public static void main(String[] args) {System.out.println("main方法中的输出");int result = Calculate.getResult(100);System.out.println("输入100获取到的值是:"+result);}}结果输出及结论:
123这里执行了premain()方法,进行了装载main方法中的输出输入100获取到的值是:200
在main方法调用Calculate类时,Calculate进行加载,加载过程中被agent类修改了字节码实现,agent类中,instrumentation.addTransformer()方法中返回修改后的字节码,若该方法返回值不为空,则虚拟机会加载该方法返回的字节码,若为空则使用原来的字节码,这里在该方法中使用javassist修改了Calculate类的getResult方法,因此当main方法中执行Calculate.getResult()方法时,此时加载的Calculate类是修改后的,因此正常输出100现在是200,其实这也相当于对Calculate类进行了代理
动态agent修改方法
编写被篡改的类Calculate,该类代码和上面相同,这里不在重复编写
ClassTransform的实现类同上,动态agent类示例代码如下:
12345678910public class DynamicAgent {public static void agentmain(String arg, Instrumentation instrumentation){System.out.println("这里执行了agentmain()方法,进行了装载");instrumentation.addTransformer(new FirstAhentTransformer(),true);try {instrumentation.retransformClasses(Calculate.class);} catch (UnmodifiableClassException e) {e.printStackTrace();}}添加pom相关依赖,打成jar包
123456789101112131415161718192021222324<build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-jar-plugin</artifactId><version>2.2</version><configuration><archive><manifestEntries><Project-name>${project.name}</Project-name><Project-version>${project.version}</Project-version><Premain-Class>com.agenttest.FirstAgent</Premain-Class><Boot-Class-Path>javassist-3.18.1-GA.jar</Boot-Class-Path><Agent-Class>com.agenttest.DynamicAgent</Agent-Class><Can-Redefine-Classes>true</Can-Redefine-Classes><Can-Retransform-Classes>true</Can-Retransform-Classes><!--<Main-Class>com.test.DynamicAgentJarTest</Main-Class>--></manifestEntries></archive><skip>true</skip></configuration></plugin></plugins></build>
注意这里相比于 APM2 多了两个标签,其含义可以参考JVM源码分析之javaagent原理完全解读
编写虚拟机监听器
之所以写这个是因为动态atach时不这样做就需要jvm的进程ID,这样就不需要在代码里手动的去改JVN的进程ID了12345678910111213141516171819202122232425262728293031323334353637383940414243444546package com.agenttest;import com.sun.tools.attach.VirtualMachine;import com.sun.tools.attach.VirtualMachineDescriptor;import java.util.List;public class VirtualMachineListener extends Thread{private final List<VirtualMachineDescriptor> listBefore;private final String jar;public VirtualMachineListener(String attachJar, List<VirtualMachineDescriptor> vms) {listBefore = vms; // 记录程序启动时的 VM 集合jar = attachJar;}public void run() {VirtualMachine vm = null;List<VirtualMachineDescriptor> listAfter = null;try {while (true) {listAfter = VirtualMachine.list();for (VirtualMachineDescriptor vmd : listAfter) {if (!listBefore.contains(vmd)) {// 如果 VM 有增加,我们就认为是被监控的 VM 启动了// 这时,我们开始监控这个 VMlistBefore.add(vmd);vm = VirtualMachine.attach(vmd);break;}}Thread.sleep(500);if (null != vm ) {System.out.println("已监控到目标虚拟机--执行动态atach");vm.loadAgent(jar);vm.detach();System.out.println("已监控到目标虚拟机--结束监听");break;}}} catch (Exception e) {e.printStackTrace();}}}两个启动测试方法
启动虚拟机的监听器123456789101112131415package com.test;import com.agenttest.VirtualMachineListener;import com.sun.tools.attach.VirtualMachine;public class DynamicAgentTest {public static void main(String[] args) throws Exception {//动态Agent所在项目打包的jar包String jarPath = System.getProperty("user.dir") + "/javaagentTestagent/target/javaagentTest-agent-1.0-SNAPSHOT.jar";System.out.println("------>"+jarPath);//启动虚拟机监听器new VirtualMachineListener(jarPath ,VirtualMachine.list()).start();System.out.println("atach已启动。。。。");}}
启动测试项目
结果输出及结论
先启动虚拟机监听器,一直监听JVM的启动,之后项目启动,调用getResult可以看到控制台先打印100,监听器监听到项目启动,动态atach到这个JVM上修改getResult()方法后,控制台打印200监听器的打印:
12345------>C:\软件\代码\smproject\javaagentTest/javaagentTestagent/target/javaagentTest-agent-1.0-SNAPSHOT.jaratach已启动。。。。##下面的打印是在启动项目后,监听器监听到,然后打印的信息已监控到目标虚拟机--执行动态atach已监控到目标虚拟机--结束监听项目的打印:
123456789main方法中的输出输入值是100,当前获得的结果是:100输入值是100,当前获得的结果是:100输入值是100,当前获得的结果是:100输入值是100,当前获得的结果是:100这里执行了agentmain()方法,进行了装载输入值是100,当前获得的结果是:100输入值是100,当前获得的结果是:200结束循环说明类已被动态修改
监听c3p0数据源
本次插桩是在com.mchange.v2.c3p0.ComboPooledDataSource类的构造方法中插入System.getProperties().put(“c3p0Source$agent”, $0);
保存一个全局的ComboPooledDataSource对象,然后将该对象在页面中输出
- 编写ClassTransform的实现类和静态agent类12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970public class C3p0Agent implements ClassFileTransformer {static String targetClass = "com.mchange.v2.c3p0.ComboPooledDataSource";public C3p0Agent() {try {//打开http服务openHttpServer();} catch (IOException e) {e.printStackTrace();}}// 获取ComboPooledDataSource对象信息public String getStatus() {Object source2 = System.getProperties().get("c3p0Source$agent");if (source2 == null) {return "未初始任何c3p0数据源";}return source2.toString();}//开启http端口 http://localhost:5555/serverpublic void openHttpServer() throws IOException {InetSocketAddress addr = new InetSocketAddress(5555);HttpServer server = HttpServer.create(addr, 0);server.createContext("/server", new HttpHandler());server.setExecutor(Executors.newCachedThreadPool());server.start();System.out.println("Server is listening on port 5555");}/*** 本次插桩是在com.mchange.v2.c3p0.ComboPooledDataSource类的构造方法中插入* System.getProperties().put("c3p0Source$agent", $0);* 保存一个全局的ComboPooledDataSource对象,然后将该对象在页面中输出*/public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {byte[] result = null;if (className != null && className.replace("/", ".").equals(targetClass)) {ClassPool pool = new ClassPool();pool.insertClassPath(new LoaderClassPath(loader));try {CtClass ctl = pool.get(targetClass);//获取构造方法 ()V无参构造CtConstructor constructor = ctl.getConstructor("()V");//$0 表示thisconstructor.insertAfter("System.getProperties().put(\"c3p0Source$agent\", $0);");result = ctl.toBytecode();} catch (Exception e) {e.printStackTrace();}}return result;}private class HttpHandler implements com.sun.net.httpserver.HttpHandler {public void handle(HttpExchange exchange) throws IOException {Headers responseHeaders = exchange.getResponseHeaders();responseHeaders.set("Content-Type", "text/plain;charset=UTF-8");exchange.sendResponseHeaders(200, 0);OutputStream responseBody = exchange.getResponseBody();// 输出c3p0状态responseBody.write(C3p0Agent.this.getStatus().getBytes());responseBody.flush();responseBody.close();}}}
|
|
- 编写测试demo
|
|
- 运行测试
注意agent类打包,添加pom配置等不在累述,resources下要有c3p0配置文件c3p0-config.xml,运行是添加jvm启动参数
控制台输出:
浏览器访问 http://localhost:5555/server 页面内容: