窥探p6spy的实现原理,抽取核心代码完成自己的SQL执行监控器

版权声明:本文为博主原创文章,未经允许不得转载。 https://blog.csdn.net/qq_31142553/article/details/84405154

某一天线上项目突然炸了,报障说出现系统登录不了、数据查询超慢等一系列问题...奇怪,之前明明还跑的好好的,怎么会这样子了呢?后来我们的数据库大神(还是妹子哦)查了数据库,统计执行比较耗时的SQL语句,对其中的一些字段临时加了索引,问题算是暂时解决了,给她点个赞QAQ。

这个时候,我就萌发了一个想法,可否在项目里面引入一个监控、记录SQL运行情况的功能,并提供查询接口和显示界面。

说干就干,首先想到的是看一下当前已有哪些现成的工具。

之前,接触过赫赫有名的阿里开源的数据源Druid(https://github.com/alibaba/druid),专为监控而生,性能也不低。二话不说,马上百度一波教程,很快引进了项目并看到了效果,功能很齐全,牛逼!

由于项目已经使用了HikariCP数据库连接池(号称最快的),虽然可以通过配置实现Druid和HikariCP随时切换,但是感觉不是那么好,而且暂时还需要不到这么多的功能(比如URI监控等),就忍痛割爱,放弃Druid了。

 然后,又找到了MyBatis和Hibernate的拦截器。看了一波,Mybatis的拦截器是可以实现我需要的功能的;但是,公司使用了Hibernate,它的拦截器就不是那么友好了,反正是没有找到我需要的,具体你们可以去研究研究。

接着,偶然机会看到了p6spy,这玩意从底层的Connection、Statement(PreparedStatement)、ResultSet入手去做拦截、装饰,很有想法,我很喜欢。

最后,发现p6spy默认是将监控情况输出到日志的,并不能实时查看(其实也可以通过重写它的处理方法,在里面实现自己的功能)。

编了一大堆,其实就是我对它的源码感兴趣而已。。。因此,废话不说了,还是直入主题吧!

一、认识p6spy

github地址:https://github.com/p6spy/p6spy

扫描二维码关注公众号,回复: 4676021 查看本文章

官方介绍:P6Spy is a framework that enables database data to be seamlessly intercepted and logged with no code changes to existing application. The P6Spy distribution includes P6Log, an application which logs all JDBC transactions for any Java application.

翻译过来就是,P6Spy是一个框架,它可以无缝地拦截和记录数据库数据,而无需更改现有应用程序的代码。P6Spy发行版包括P6Log,这是一个为任何Java应用程序记录所有JDBC事务的应用程序。

具体使用不说了,百度教程一大堆,文章重心不在此。

二、p6spy实现原理

源码涉及的设计模式:装饰者模式、观察者模式、工厂模式、单例模式......

1、首先,我们之前使用jdbc都写过这样的代码吧:String driver = xxx;Class.forName(driver)。这里,需要我们将driver设置成com.p6spy.engine.spy.P6SpyDriver,然后调用Class.forName(...)时会对这个类进行加载和连接。初始化的时候,会执行静态代码块,将驱动注册到DriverManager里(registerDriver方法会检查classpath有没有实现了Driver接口的这个类)。

static {
    try {
      DriverManager.registerDriver(P6SpyDriver.instance);
    } catch (SQLException e) {
      throw new IllegalStateException("Could not register P6SpyDriver with DriverManager", e);
    }
  }

2、DriverManager.getConnection(...)的时候,会遍历所有已注册的驱动信息,判断每一个驱动跟所填的URL是否匹配(所有驱动都强制实现了Driver的acceptsURL方法)。根据我们填的URL以jdbc:p6spy:开头,最终确定了Driver是P6SpyDriver。

    @Override
	public boolean acceptsURL(final String url) {
		return url != null && url.startsWith("jdbc:p6spy:");
	}

3、获取连接最终会调用到具体驱动类的connect(...)方法。首先,P6SpyDriver的实现里面,它先要获得实际的、真实的数据库驱动(Oracle还是Mysql),怎么获得呢?把我们填写的url的"p6spy:"去掉就得到真正驱动的地址了,然后去匹配DriverManager中已经注册的Driver驱动,总有一款合适,否则就报错了。

@Override
  public Connection connect(String url, Properties properties) throws SQLException {
    // if there is no url, we have problems
    if (url == null) {
      throw new SQLException("url is required");
    }

    if( !acceptsURL(url) ) {
      return null;
    }

    // find the real driver for the URL
    Driver passThru = findPassthru(url);

    P6LogQuery.debug("this is " + this + " and passthru is " + passThru);

    final long start = System.nanoTime();

    if (P6SpyDriver.jdbcEventListenerFactory == null) {
      P6SpyDriver.jdbcEventListenerFactory = JdbcEventListenerFactoryLoader.load();
    }
    final Connection conn;
    final JdbcEventListener jdbcEventListener = P6SpyDriver.jdbcEventListenerFactory.createJdbcEventListener();
    final ConnectionInformation connectionInformation = ConnectionInformation.fromDriver(passThru);
    connectionInformation.setUrl(url);
    jdbcEventListener.onBeforeGetConnection(connectionInformation);
    try {
      conn =  passThru.connect(extractRealUrl(url), properties);
      connectionInformation.setConnection(conn);
      connectionInformation.setTimeToGetConnectionNs(System.nanoTime() - start);
      jdbcEventListener.onAfterGetConnection(connectionInformation, null);
    } catch (SQLException e) {
      connectionInformation.setTimeToGetConnectionNs(System.nanoTime() - start);
      jdbcEventListener.onAfterGetConnection(connectionInformation, e);
      throw e;
    }

    return ConnectionWrapper.wrap(conn, jdbcEventListener, connectionInformation);
  }

4、拿到真正的驱动之后,先记录它的相关信息,然后根据真正驱动和它的信息创建一个连接包装器ConnectionWrapper(装饰者模式:包装器与被包装者实现了相同的接口或者拥有着共同的父类,包装器内部据有一个被包装者的引用,包装器在实现了接口或者重写了父类的方法中,实际是调用了被包装者的相同方法,只是在前后做一些处理。或者认为是静态代理模式)

5、连接包装类ConnectionWrapper到底做了什么呢?主要是对createStatement(...)方法和prepareStatement(...)方法得到的Statement和PreparedStatement也进行了包装,类似ConnectionWrapper。

    @Override
	public Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability)
			throws SQLException {
		return StatementWrapper.wrap(
				delegate.createStatement(resultSetType, resultSetConcurrency, resultSetHoldability),
				new StatementInformation(connectionInformation), jdbcEventListener);
	}

	@Override
	public PreparedStatement prepareStatement(String sql) throws SQLException {
		return PreparedStatementWrapper.wrap(delegate.prepareStatement(sql),
				new PreparedStatementInformation(connectionInformation, sql), jdbcEventListener);
	}

6、 StatementWrapper主要对执行语句的方法做了包装,在前后利用监听器触发对应的处理逻辑(观察者模式:当一个被观察者或者叫发布者发生了改变之后,执行其通知方法,所有实现了观察者接口并且注册了订阅者身份的对象都会收到提醒,并进行各自的处理)。

7、其它包装类ResultSetWrapper、CallableStatementWrapper等类似,在此不做过多解释。

8、前面说到的监听器,也就是观察者模式。它们有一个接口JdbcEventListener,并提供了很多个实现类,其中有一个很特别的CompoundJdbcEventListener,它可以添加很多个其它单一功能的监听器实例到自己内部集合里,然后在调用接口方法时,分别执行其集合里监听器的对应方法。在包装类或者其它地方调用监听器的时候,默认是使用这个混合监听器执行对应操作,这些操作在JdbcEventListener接口做了定义,并且不同实现类提供了不同的处理方式,里面最常用的就是记录日志了。

9、具体会注册哪些监听器到CompoundJdbcEventListener的集合里面,首先需要读取配置信息,然后会使用工厂模式创建对应的监听器实例,最后加入到混合监听器中。

总之,p6psy的核心思想是对具体厂商的驱动包里面的几个类做了包装,在一些方法的前后增加了调用监听器指定方法来对操作发生时做相应处理

三、实现自己的SQL执行监控器

1、在读懂了源码之后,就很容易对其进行改造了。它的包装类、信息类、监听器接口和默认几个实现类都做得很好,暂时没有必要自己写(得花很多时间和精力,费力不讨好),直接拿过来就好。

2、如下,左边是原来的类,很多吧(不过功能也非常多);右边是拷过来的核心类(自己写的几个也在里面了)。

 

3、解决一下编译问题,根据自己的情况做处理(不知道的话下载项目源码看一下,文章末尾会提供链接)。  

4、下面就是自定义的功能了。

(1)首先,建立一个用于记录SQL的执行情况的类,我将其命名为SimpleStatementInformation,主要是因为里面的信息基本都来自StatementInformation类。

/**
 * 用于记录SQL的执行情况
 * @author z_hh
 * @time 2018年11月23日
 */
public class SimpleStatementInformation implements Serializable {

	private static final long serialVersionUID = 4104996496129361823L;

	/** 执行结束时间 */
	private Date execEndTime;
	
	/** 编译语句 */
	private String sourceSql;
	
	/** 执行语句 */
	private String sqlWithValue;
	
	/** 语句类型 */
	private String type;
	
	/** 是否成功 */
	private Boolean success;
	
	/** 耗时(毫秒) */
	private Double timeElapsedMillis;
	
	/** 错误信息 */
	private String errorMsg;
	
	public SimpleStatementInformation() {
		// TODO Auto-generated constructor stub
	}

	public SimpleStatementInformation(Date execEndTime, String sourceSql, String sqlWithValue, String type,
			Boolean success, Double timeElapsedMillis, String errorMsg) {
		super();
		this.execEndTime = execEndTime;
		this.sourceSql = sourceSql;
		this.sqlWithValue = sqlWithValue;
		this.type = type;
		this.success = success;
		this.timeElapsedMillis = timeElapsedMillis;
		this.errorMsg = errorMsg;
	}
    // 省略getter、setter、toString方法
}

(2)然后,实现一个自己的监听器。这里我选择继承SimpleJdbcEventListener类,因为现在只想记录SQL语句执行后的信息,所以只需重写SimpleJdbcEventListener类扩展的onAfterAnyExecute、onAfterAnyAddBatch两个方法即可。又因为两个方法中做记录的代码是一样的,所以封装了一个addRecore(...)方法。

/**
 * This implementation of {@link JdbcEventListener} must always be applied as
 * the first listener. It populates the information objects
 * {@link StatementInformation}, {@link PreparedStatementInformation},
 * {@link cn.zhh.sql_monitor.common.CallableStatementInformation} and
 * {@link ResultSetInformation}
 */
public class MyEventListener extends SimpleJdbcEventListener {

	public static final MyEventListener INSTANCE = new MyEventListener();
	
	/**
	 * 线程安全,但是效率低。后续考虑使用concurrent包下的类
	 */
	private static final List<SimpleStatementInformation> SQLS = Collections.synchronizedList(new ArrayList<>());
	
	public static List<SimpleStatementInformation> getSqls() {
		return SQLS;
	}
	
	private MyEventListener() {
	}
	
	/**
	 * This callback method is executed after any {@link java.sql.Statement}.execute* method is invoked
	 *
	 * @param statementInformation The meta information about the {@link java.sql.Statement} being invoked
	 * @param timeElapsedNanos     The execution time of the execute call
	 * @param e                    The {@link SQLException} which may be triggered by the call (<code>null</code> if
	 *                             there was no exception).
	 */
	@Override
	public void onAfterAnyExecute(StatementInformation statementInformation, long timeElapsedNanos, SQLException e) {
		addRecord(statementInformation, timeElapsedNanos, e);
	}
	
	/**
	 * This callback method is executed before any {@link java.sql.Statement}.addBatch* method is invoked
	 *
	 * @param statementInformation The meta information about the {@link java.sql.Statement} being invoked
	 * @param timeElapsedNanos     The execution time of the execute call
	 * @param e                    The {@link SQLException} which may be triggered by the call (<code>null</code> if
	 *                             there was no exception).
	 */
	@Override
	public void onAfterAnyAddBatch(StatementInformation statementInformation, long timeElapsedNanos, SQLException e) {
		addRecord(statementInformation, timeElapsedNanos, e);
	}
	
	/**
	 * 记录语句执行信息
	 * @param statementInformation
	 * @param timeElapsedNanos
	 * @param e
	 */
	private void addRecord(StatementInformation statementInformation, long timeElapsedNanos, SQLException e) {
		double timeElapsedMillis = timeElapsedNanos / 1000_000;
		boolean success = Objects.isNull(e);
		SimpleStatementInformation information = new SimpleStatementInformation(
				new Date(),
				statementInformation.getSql(),
				statementInformation.getSqlWithValues(),
				"Execute",
				success,
				timeElapsedMillis,
				success ? null : e.getMessage());
		SQLS.add(information);
	}

}

(3)接着,在JdbcEventListenerFactory类中createJdbcEventListener()方法里面创建一个CompoundJdbcEventListener实例,并将默认的DefaultEventListener(记录SQL执行时间的)、自己定义的MyEventListener加进去。

(4)最后,写个测试来爽一下。

执行几组语句,最后面将MyEventListener里面的记录打印出来。

/**
 * 客户端测试
 * @author z_hh
 * @time 2018年11月25日
 */
public class Client {

	public static void main(String[] args) throws Exception {
		String driver = "cn.zhh.sql_monitor2.spy.P6SpyDriver";
		String url = "jdbc:p6spy:mysql://ip:3306/test?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true";
		String user = "root";
		String password = "password";
		Class.forName(driver);
		
		String selectSql = "select name from student where id = 1";
		String selectSqlForPrepare = "select address from student where id = ?";
		String insertSqlForPrepare = "insert into student values(?, ?, 1, ?, ?, ?)";
		String deleteSql = "delete from student where id = 3";
		String deleteSqlForError = "delete from not_exist_table where id = 2";
		
		try (Connection con = DriverManager.getConnection(url, user, password);
				Statement statement = con.createStatement();
				PreparedStatement preparedStatementSelect = con.prepareStatement(selectSqlForPrepare);
				PreparedStatement preparedStatementInsert = con.prepareStatement(insertSqlForPrepare);
				) {
			// 1、查询
			ResultSet resultSet1 = statement.executeQuery(selectSql);
			System.out.println("selectSql执行结果:" + (resultSet1.next() ? resultSet1.getString(1) : null));
			// 2、预处理查询
			preparedStatementSelect.setInt(1, 4);
			preparedStatementSelect.executeQuery();
			ResultSet resultSet2 = preparedStatementSelect.executeQuery();
			System.out.println("preparedStatementSelect执行结果:" + (resultSet2.next() ? resultSet1.getString(1) : null));
			// 3、预处理插入
			preparedStatementInsert.setInt(1, 6);
			preparedStatementInsert.setString(2, "zhh");
			preparedStatementInsert.setString(3, "13800138000");
			preparedStatementInsert.setDate(4, new java.sql.Date(2018, 12, 31));
			preparedStatementInsert.setString(5, "广州市天河区");
			boolean success1 = preparedStatementInsert.execute();
			System.out.println("insertSqlForPrepare执行结果:" + success1);
			// 4、删除
			boolean success2 = statement.execute(deleteSql);
			System.out.println("deleteSql执行结果:" + success2);
			// 5、不存在表的删除
			boolean success3 = statement.execute(deleteSqlForError);
		} catch (Exception e) {
			e.printStackTrace();
		}
		
		
		MyEventListener.getSqls().forEach(information -> {
			System.out.println(information);
		});

	}

}
12:44:55.029 [main] DEBUG cn.zhh.sql_monitor2.spy.P6SpyDriver - this is cn.zhh.sql_monitor2.spy.P6SpyDriver@7dc5e7b4 and passthru is com.mysql.jdbc.Driver@41975e01
selectSql执行结果:zhh
preparedStatementSelect执行结果:zhh
insertSqlForPrepare执行结果:false
deleteSql执行结果:false
com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: Table 'test.not_exist_table' doesn't exist
	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
	at com.mysql.jdbc.Util.handleNewInstance(Util.java:425)
	at com.mysql.jdbc.Util.getInstance(Util.java:408)
	at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:944)
	at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3973)
	at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3909)
	at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:2527)
	at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2680)
	at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2480)
	at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2438)
	at com.mysql.jdbc.StatementImpl.executeInternal(StatementImpl.java:845)
	at com.mysql.jdbc.StatementImpl.execute(StatementImpl.java:745)
	at cn.zhh.sql_monitor2.wrapper.StatementWrapper.execute(StatementWrapper.java:118)
	at cn.zhh.sql_monitor2.Client.main(Client.java:53)
SimpleStatementInformation [
 execEndTime=Sun Nov 25 12:44:55 CST 2018,
 sourceSql=select name from student where id = 1,
 sqlWithValue=select name from student where id = 1,
 type=Execute,
 success=true,
 timeElapsedMillis=29.0,
 errorMsg=null]
SimpleStatementInformation [
 execEndTime=Sun Nov 25 12:44:55 CST 2018,
 sourceSql=select address from student where id = ?,
 sqlWithValue=select address from student where id = 4,
 type=Execute,
 success=true,
 timeElapsedMillis=6.0,
 errorMsg=null]
SimpleStatementInformation [
 execEndTime=Sun Nov 25 12:44:55 CST 2018,
 sourceSql=select address from student where id = ?,
 sqlWithValue=select address from student where id = 4,
 type=Execute,
 success=true,
 timeElapsedMillis=9.0,
 errorMsg=null]
SimpleStatementInformation [
 execEndTime=Sun Nov 25 12:44:55 CST 2018,
 sourceSql=insert into student values(?, ?, 1, ?, ?, ?),
 sqlWithValue=insert into student values(6, 'zhh', 1, '13800138000', '3919-01-31', '广州市天河区'),
 type=Execute,
 success=true,
 timeElapsedMillis=33.0,
 errorMsg=null]
SimpleStatementInformation [
 execEndTime=Sun Nov 25 12:44:55 CST 2018,
 sourceSql=delete from student where id = 3,
 sqlWithValue=delete from student where id = 3,
 type=Execute,
 success=true,
 timeElapsedMillis=16.0,
 errorMsg=null]
SimpleStatementInformation [
 execEndTime=Sun Nov 25 12:44:55 CST 2018,
 sourceSql=delete from not_exist_table where id = 2,
 sqlWithValue=delete from not_exist_table where id = 2,
 type=Execute,
 success=false,
 timeElapsedMillis=24.0,
 errorMsg=Table 'test.not_exist_table' doesn't exist]

MyEventListener的处理逻辑仅做测试,ArrayList使用synchronized同步很影响性能,高并发下考虑ConcurrentHashMap或者异步记录。

相关源码已上传,请前往下载

本文内容到此结束了,有什么问题或者建议,欢迎在评论区进行探讨!

猜你喜欢

转载自blog.csdn.net/qq_31142553/article/details/84405154
今日推荐