使用javassist改写类实现拦截sql语句输出实 ***

使用javassist改写类实现拦截sql语句输出实 *** ,第1张

众所周知,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-java
        8.0.19
        runtime
    

    
        org.javassist
        javassist
        3.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的输出。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/800228.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-06
下一篇 2022-05-06

发表评论

登录后才能评论

评论列表(0条)

保存