APM(4)javassist结合javaagent使用

javaagnet为插桩提供入口,javassist实现字节码修改,这部分将记录3个demo,分别是静态agent结合javassist修改方法体,动态agent结合javassist修改方法体,最后一个demo综合使用javaagent和javassist监听c3p0数据源

静态agent修改方法

  1. 编写一个用于被篡改的类Calculate,示例代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public class Calculate {
    /**
    * 输入什么数字就返回什么数字
    * @param a 输入的数字
    * @return 输入的数字
    */
    public static int getResult(int a){
    return a;
    }
    }
  2. 编写ClassTransform的实现类和静态agent类,配置pom文件(参考 APM1 ),该类在Calculate类进行装载时会篡改他的字,使得他的getResult()方法返回输入数值的双倍,完毕后记得打包,示例代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    package 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 {
    @Override
    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;,如果成功该方法返回值应该是2a
    resultMethod.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;
    }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.agenttest;
import javassist.*;
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
public class FirstAgent {
/**
* @param arg agentArgs 是 premain 函数得到的程序参数,随同 “– javaagent”一起传入。
* @param instrumentation instrumentation 是一个 java.lang.instrument.Instrumentation 的实例,由 JVM 自动传入
*/
public static void premain(String arg, Instrumentation instrumentation){
System.out.println("这里执行了premain()方法,进行了装载");
instrumentation.addTransformer(new FirstAhentTransformer());
}
}
  1. 启动方法,测试,注意添加javagent的jvm启动参数,示例代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    package 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);
    }
    }
  2. 结果输出及结论:

    1
    2
    3
    这里执行了premain()方法,进行了装载
    main方法中的输出
    输入100获取到的值是:200

在main方法调用Calculate类时,Calculate进行加载,加载过程中被agent类修改了字节码实现,agent类中,instrumentation.addTransformer()方法中返回修改后的字节码,若该方法返回值不为空,则虚拟机会加载该方法返回的字节码,若为空则使用原来的字节码,这里在该方法中使用javassist修改了Calculate类的getResult方法,因此当main方法中执行Calculate.getResult()方法时,此时加载的Calculate类是修改后的,因此正常输出100现在是200,其实这也相当于对Calculate类进行了代理

动态agent修改方法

  1. 编写被篡改的类Calculate,该类代码和上面相同,这里不在重复编写

  2. ClassTransform的实现类同上,动态agent类示例代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public 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();
    }
    }
  3. 添加pom相关依赖,打成jar包

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    <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原理完全解读

1
2
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>

  1. 编写虚拟机监听器
    之所以写这个是因为动态atach时不这样做就需要jvm的进程ID,这样就不需要在代码里手动的去改JVN的进程ID了

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    package 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 启动了
    // 这时,我们开始监控这个 VM
    listBefore.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();
    }
    }
    }
  2. 两个启动测试方法
    启动虚拟机的监听器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    package 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已启动。。。。");
    }
    }

启动测试项目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class FirstAgentMain {
public static void main(String[] args) {
System.out.println("main方法中的输出");
while(true){
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
int result = Calculate.getResult(100);
System.out.println("输入值是100,当前获得的结果是:"+result);
if(result == 200){
System.out.println("结束循环说明类已被动态修改");
break;
}
}
}
}

  1. 结果输出及结论
    先启动虚拟机监听器,一直监听JVM的启动,之后项目启动,调用getResult可以看到控制台先打印100,监听器监听到项目启动,动态atach到这个JVM上修改getResult()方法后,控制台打印200

    监听器的打印:

    1
    2
    3
    4
    5
    ------>C:\软件\代码\smproject\javaagentTest/javaagentTestagent/target/javaagentTest-agent-1.0-SNAPSHOT.jar
    atach已启动。。。。
    ##下面的打印是在启动项目后,监听器监听到,然后打印的信息
    已监控到目标虚拟机--执行动态atach
    已监控到目标虚拟机--结束监听

    项目的打印:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    main方法中的输出
    输入值是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对象,然后将该对象在页面中输出

  1. 编写ClassTransform的实现类和静态agent类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    public 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/server
    public 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对象,然后将该对象在页面中输出
    */
    @Override
    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 表示this
    constructor.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 {
    @Override
    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();
    }
    }
    }
1
2
3
4
5
public static void premain(String args, Instrumentation inst) {
System.out.println(String.format("系统载入myAgent 参数%s 载入方法:premain", args));
System.out.println("载入C3p0Agent");
inst.addTransformer(new C3p0Agent());
}
  1. 编写测试demo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class C3p0Demo {
ComboPooledDataSource dataSource;
public C3p0Demo(){
dataSource = new ComboPooledDataSource("mysql");
}
public void exec(String sql) throws SQLException {
Connection conn = dataSource.getConnection();
boolean b = conn.createStatement().execute(sql);
conn.close();
}
public static void main(String[] args) throws IOException {
C3p0Demo s=new C3p0Demo();
while (true) {
byte[] bytes = new byte[1024];
int size = System.in.read(bytes);
String sql = new String(bytes, 0, size);
System.out.println(sql);
try {
s.exec(sql);
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
  1. 运行测试
    注意agent类打包,添加pom配置等不在累述,resources下要有c3p0配置文件c3p0-config.xml,运行是添加jvm启动参数

控制台输出:

1
2
3
4
5
6
7
8
9
系统载入myAgent 参数null 载入方法:premain
载入C3p0Agent
Server is listening on port 5555
三月 19, 2018 11:07:05 上午 com.mchange.v2.log.MLog <clinit>
信息: MLog clients using java 1.4+ standard logging.
三月 19, 2018 11:07:06 上午 com.mchange.v2.c3p0.C3P0Registry banner
信息: Initializing c3p0-0.9.1.2 [built 21-May-2007 15:04:56; debug? true; trace: 10]
select * from user;
select * from user;

浏览器访问 http://localhost:5555/server 页面内容:

1
com.mchange.v2.c3p0.ComboPooledDataSource [ acquireIncrement -> 3, acquireRetryAttempts -> 30, acquireRetryDelay -> 1000, autoCommitOnClose -> false, automaticTestTable -> null, breakAfterAcquireFailure -> false, checkoutTimeout -> 0, connectionCustomizerClassName -> null, connectionTesterClassName -> com.mchange.v2.c3p0.impl.DefaultConnectionTester, dataSourceName -> mysql, debugUnreturnedConnectionStackTraces -> false, description -> null, driverClass -> com.mysql.jdbc.Driver, factoryClassLocation -> null, forceIgnoreUnresolvedTransactions -> false, identityToken -> 1hge41t9ugpkfsah1jv37|21934d9d, idleConnectionTestPeriod -> 0, initialPoolSize -> 10, jdbcUrl -> jdbc:mysql://localhost:3306/springtest?useUnicode=true&characterEncoding=UTF8, maxAdministrativeTaskTime -> 0, maxConnectionAge -> 0, maxIdleTime -> 30, maxIdleTimeExcessConnections -> 0, maxPoolSize -> 100, maxStatements -> 200, maxStatementsPerConnection -> 0, minPoolSize -> 10, numHelperThreads -> 3, numThreadsAwaitingCheckoutDefaultUser -> 0, preferredTestQuery -> null, properties -> {user=******, password=******}, propertyCycle -> 0, testConnectionOnCheckin -> false, testConnectionOnCheckout -> false, unreturnedConnectionTimeout -> 0, usesTraditionalReflectiveProxies -> false ]

参考链接

可能问题
Instrumentation 新功能
JVM源码分析之javaagent原理完全解读


-------------本文结束感谢您的阅读-------------