众所周知,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,,);"); 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的输出。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)