众所周知,javassist能够在字节码层面去重新构建一个已经存在的类,同时结合java虚拟机代理Instrumentation 根据类的字节码重定义类的能力。我们可以去动态改写一个类的方法,这个粒度可以精确到代码行。一般我们重定义类,可能希望增加或者减少字段,增加或者减少方法。但是结合我们常用的hotspot虚拟机具体实现,只对重写方法体逻辑生效,也就是我们只能重构类里面已经存在的方法。虽然虚拟机的实现只能局限于这种程度,但是并不意味着它的用途的狭隘,我们仍然能够利用这种支持做最大化的应用。只重写方法体的话我们能做什么呢,简单的捕获方法参数和改写方法的返回值都是可以的,实际上这也是AOP思想的体现,而且这种“注入拦截”操作是无侵入式样的,这种修改并不是基于源码的修改,而是基于运行时代码的修改。就像一个可插拔的USB一样的,需要的时候就插上,不需要的时候就拔掉,不影响不干扰业务逻辑的运转。我们经常接触的管理系统后台很多用的都是mysql,很多时候我们都需要了解sql语句的真实输出情况去排查问题,当然如果框架或者组件已经提供了sql输出的支持那自不必多说。在框架没有提供支持的情况下,我们就需要花费不少的精力去研究sql输出的情况下,这时候如果有一个工具能够无视框架的封装就可以捕获sql是不是就很完美呢。所以捕获sql的基础思路还是对于jdbc基础实现类进行方法的重写。以mysql为例,下面的demo将展示这个过程。
1、创建javaagent
javaagent以一个jar包的形式存在,它可以是一个只包含META-INF/MANIFEST.MF清单文件的空jar包。我们的示例程序中就是这么一个空jar包。这实际上是个不错的技巧,我们一般很容易认为agent代理类会一定放在jar包,其实不然。
此时指定的Premain-Class类没有放在jar包中,而是放在了jar包外面
2、创建一个普通的java maven项目,pom文件内容如下
mysql mysql-connector-java8.0.19 runtime org.javassist javassist3.25.0-GA
示例中我们使用mysql jdbc驱动版本为8.0.19
3、使用jdbc连接mysql
MySQLUtil.java
package com.suntown.jdbc;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
public class MySQLUtil{
Connection connection = null;
public MySQLUtil(){
try{
Class.forName("com.mysql.cj.jdbc.Driver");
connection = DriverManager.getConnection(
"jdbc:mysql://192.168.2.184:3306/archivessive?characterEncoding=UTF-8&useSSL=false&allowMultiQueries=true&serverTimezone=UTC",
"root","root");
}catch(ClassNotFoundException e){
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
}
}
public MySQLUtil test1(){
try{
PreparedStatement preparedStatement = connection.prepareStatement("select * from sys_unit where name='xyz'");
preparedStatement.execute();
}catch(SQLException e){
e.printStackTrace();
}
return this;
}
static int counter = 0;
public MySQLUtil test2(){
try{
counter++;
PreparedStatement preparedStatement = connection.prepareStatement("insert into test_ids(id,dh) values('"+counter+"','"+counter+"');");
preparedStatement.execute();
}catch(SQLException e){
e.printStackTrace();
}
return this;
}
}
4、使用 javassist重写mysql驱动包中类 com.mysql.cj.NativeSession的execSQL方法
不同版本的驱动包实现可能有差异要结合实际情况,如果要写出通用的需要做适配处理
下面的代码是在execSQL的第一行插入了一段代码com.suntown.injecthandler.MySQLSessionInjectHandler.getSQL(this,$1,$2);
package com.suntown.util;
import javassist.*;
import java.io.File;
import java.lang.instrument.ClassDefinition;
import java.lang.instrument.Instrumentation;
public class JavassistInjector extends Thread{
public static void premain(String args, Instrumentation instrumentation0){
instrumentation = instrumentation0;
}
public static void agentmain(String args, Instrumentation instrumentation0){
instrumentation = instrumentation0;
}
public void run(){
injectMySQL();
}
public static CtClass[] getMethodParam(String methodsign, ClassPool pool)
{
int sinx = methodsign.indexOf("(") + 1;
int einx = methodsign.indexOf(")");
String str = methodsign.substring(sinx, einx);
if(str!=null && !"".equals(str.trim())){
String[] arr = str.split(",");
CtClass[] param = new CtClass[arr.length];
for(int i = 0; i < param.length; i++){
try {
param[i] = pool.get(arr[i]);
} catch (NotFoundException e) {
e.printStackTrace();
}
}
return param;
}
return new CtClass[0];
}
static Instrumentation instrumentation = null;
public Class findClass(String className){
Class[] classes = instrumentation.getAllLoadedClasses();
for(Class c : classes){
if(c.getName().equals(className)){
return c;
}
}
return null;
}
public Class findClassInterval(String className){
Class cls = null;
for(int i=0;i<5;i++){
cls = findClass(className);
if(cls != null){
return cls;
}
try{
Thread.sleep(1000);
}catch(InterruptedException e){
e.printStackTrace();
}
}
return null;
}
public void injectMySQL(){
ClassPool pool = ClassPool.getDefault();
Class nsessionClass = findClassInterval("com.mysql.cj.NativeSession");
ClassDefinition classDefinition = null;
try{
pool.insertClassPath(new ClassClassPath(nsessionClass));
Class mysqlHandlerClass = nsessionClass.getClassLoader().loadClass("com.suntown.injecthandler.MySQLSessionInjectHandler");
CtClass ctClass = pool.get("com.mysql.cj.NativeSession");
if(ctClass.isFrozen()){
ctClass.defrost();
}
CtMethod cm = null;
try{
//此时对应mysql的版本为 mysql-connector-java-8.0.19.jar
cm = ctClass.getDeclaredMethod("execSQL",
getMethodParam("public com.mysql.cj.protocol.Resultset com.mysql.cj.NativeSession" +
".execSQL(com.mysql.cj.Query,java.lang.String,int,com.mysql.cj.protocol.a.NativePacketPayload,boolean,com.mysql.cj.protocol.ProtocolEntityFactory,com.mysql.cj.protocol.ColumnDefinition,boolean)",pool));
}catch(Throwable tw){
tw.printStackTrace();
}
cm.insertBefore("com.suntown.injecthandler.MySQLSessionInjectHandler.getSQL(this,$1,$2);");
byte[] buffer = ctClass.toBytecode();
if(ctClass.isFrozen()){
ctClass.defrost();
}
classDefinition = new ClassDefinition(nsessionClass,buffer);
instrumentation.redefineClasses(classDefinition);
}catch(Throwable tw){
tw.printStackTrace();
}
}
}
涉及到的类com.suntown.injecthandler.MySQLSessionInjectHandler实现如下
package com.suntown.injecthandler;
import java.text.SimpleDateFormat;
import java.util.Date;
public class MySQLSessionInjectHandler{
public static void getSQL(Object nativeSession,Object query,String queryString){
if(query == null){
return;
}
String strSQL = query.toString();
SimpleDateFormat sdf = new SimpleDateFormat("yy-MM-dd HH:mm:ss");
String dateStr = sdf.format(new Date());
System.out.println("<<<拦截到SQL "+strSQL+",当前线程:"+Thread.currentThread()+",时间:"+dateStr+" >>>");
}
}
5、在主类中调用测试
在主类中开启两个线程,分别执行指定次数的查询和插入sql
package com.suntown;
import com.suntown.jdbc.MySQLUtil;
import com.suntown.util.JavassistInjector;
public class Main{
public static void main(String[] args){
new JavassistInjector().start();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
for(int i=0;i<5;i++){
try{
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new MySQLUtil().test1();
}
}
});
thread1.start();
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
for(int i=0;i<5;i++){
try{
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new MySQLUtil().test2();
}
}
});
thread2.start();
}
}
6、运行结果
此时运行配置中 的vmoption为
看结果确实捕获了sql的输出。



