前言

我们在使用数据库时,为了使业务系统性能达到最优,往往都需要避免慢SQL查询,不能等到线上告警了再排查是否为慢SQL导致。在开发阶段,每个开发人员就应该针对自己写的SQL看是否可能为慢SQL,从而进行相应的SQL优化和索引优化等。项目中可能用的是mybatis,也可能用的jpa,针对写的SQL如何能自动执行explain sql语句来查看每条SQL的执行计划呢?于是有了本篇文章,带你实现这样一个简单好用通用的插件。

探索

针对这个问题,一个最基本的思路就是:修改底层执行sql的代码,在sql执行前先explain sql,输出执行计划,然后再继续执行原本的逻辑。如下图:

 

以下针对使用了Java+MySQL的项目进行实现讲解,其他技术方式有兴趣的小伙伴可以自己实现哈。
 
操作MySQL,项目中一般会引入依赖:
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
<scope>runtime</scope>
</dependency>
执行SQL时,经过跟踪代码执行路径,最终会到达这个方法:com.mysql.jdbc.PreparedStatement#executeInternal
/**
* Actually execute the prepared statement. This is here so server-side
* PreparedStatements can re-use most of the code from this class.
*
* @param maxRowsToRetrieve the max number of rows to return
* @param sendPacket SQL语句packet
* @param createStreamingResultSet should a 'streaming' result set be created?
* @param queryIsSelectOnly is this query doing a SELECT?
*/
protected ResultSetInternalMethods executeInternal(int maxRowsToRetrieve, Buffer sendPacket, boolean createStreamingResultSet, boolean queryIsSelectOnly,
Field[] metadataFromCache, boolean isBatch) throws SQLException {}
其中,参数sendPacket就是最终要执行的完整sql语句。我们需要在这个方法中加入explain sql逻辑。

实现

我们所写的插件应该是通用的,不能侵入现有代码。不限于以下两种实现方式:
  • javaagent            -- 相对复杂一点,但是很通用,无特殊依赖
  • jrebel自定义插件   -- 需要安装jrebel插件,idea和eclipse均支持
回看标题,本篇仅讲解基于jrebel平台来实现这么一个神奇的插件。关于jrebel自定义插件开发,请参阅官方文档:Custom JRebel plugins
 
实现过程还是非常轻松的,一起来吧!

1. 新建一个maven项目

pom依赖如下:
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<sdk.version>7.0.0</sdk.version>
</properties> <repositories>
<repository>
<id>zt-public</id>
<url>https://repos.zeroturnaround.com/nexus/content/groups/zt-public</url>
</repository>
</repositories> <dependencies>
<dependency>
<groupId>org.zeroturnaround</groupId>
<artifactId>jr-sdk</artifactId>
<version>${sdk.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.zeroturnaround</groupId>
<artifactId>jr-utils</artifactId>
<version>${sdk.version}</version>
<scope>provided</scope>
</dependency> <dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
<scope>provided</scope>
</dependency>
</dependencies>

2. 编写jrebel CBP

继承org.zeroturnaround.javarebel.integration.support.JavassistClassBytecodeProcessor
package com.example.plugin.mysql.cbp;

public class MySQLExplainCBP extends JavassistClassBytecodeProcessor {

    @Override
public void process(ClassPool cp, ClassLoader cl, CtClass ctClass) throws Exception {
LoggerFactory.getInstance().echo("MySQLExplainCBP...");
// 自定义处理类所在包
cp.importPackage("com.example.plugin.mysql.explain"); // 找到方法:com.mysql.jdbc.PreparedStatement#executeInternal
// 在该方法第一行之前插入自定义逻辑
CtMethod m = ctClass.getDeclaredMethod("executeInternal");
m.insertBefore(
"MySQLExplain.explainSql(this.connection, sendPacket);"
);
}
}

3. 自定义explain逻辑

查看MySQL执行计划
package com.example.plugin.mysql.explain;

public final class MySQLExplain {
private static final Logger logger = LoggerFactory.getInstance(); /**
* 调用者:MySQLExplainCBP#process
*/
public static void explainSql(com.mysql.jdbc.MySQLConnection conn, com.mysql.jdbc.Buffer sendPacket) {
byte[] bytes = new byte[sendPacket.getPosition()];
System.arraycopy(sendPacket.getByteBuffer(), 0, bytes, 0, bytes.length); String sql = new String(bytes, 5, bytes.length - 5);
// 前置检查,检查是否执行计划
if (!processBefore(sql)) {
return;
} try {
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("EXPLAIN " + sql); List<ExplainResultVo> explainResultList = new ArrayList<>();
while (rs.next()) {
ExplainResultVo explainResultVo = new ExplainResultVo();
explainResultVo.setId(rs.getString("id"));
explainResultVo.setSelectType(rs.getString("select_type"));
explainResultVo.setTable(rs.getString("table"));
explainResultVo.setPartitions(rs.getString("partitions"));
explainResultVo.setType(rs.getString("type"));
explainResultVo.setPossibleKeys(rs.getString("possible_keys"));
explainResultVo.setKey(rs.getString("key"));
explainResultVo.setKeyLen(rs.getString("key_len"));
explainResultVo.setRef(rs.getString("ref"));
explainResultVo.setRows(rs.getString("rows"));
explainResultVo.setFiltered(rs.getString("filtered"));
explainResultVo.setExtra(rs.getString("Extra")); explainResultList.add(explainResultVo);
} rs.close(); // 打印结果
analyzeResult(sql, explainResultList);
} catch (Exception se) {
logger.errorEcho("EXPLAIN SQL异常", se);
}
} /**
* 前置检查,检查是否执行计划,自定添加逻辑
*/
private static boolean processBefore(String sql) {
return true;
} /**
* 分析结果,打印执行计划,可自定添加过滤条件
*/
private static void analyzeResult(String sql, List<ExplainResultVo> explainResultList) {
ExplainHelper.printExplainResult(sql, explainResultList, showSQL);
}
}

4. 编写jrebel插件入口类

实现接口:org.zeroturnaround.javarebel.Plugin,需在pom中配置该类

package com.example.plugin.mysql;

/**
* 插件注册,需在pom中配置该类
*/
public class MySQLExplainPlugin implements Plugin {
private static final Logger logger = LoggerFactory.getInstance(); @Override
public void preinit() {
Integration integration = IntegrationFactory.getInstance();
ClassLoader cl = getClass().getClassLoader(); logger.echo("MySQL执行计划插件启用, 配置详情: \n" + Config.getMySQLConfTable());
integration.addIntegrationProcessor(
cl,
"com.mysql.jdbc.PreparedStatement",
new MySQLExplainCBP());
} // 指定依赖条件,依赖存在时才启用本插件
@Override
public boolean checkDependencies(ClassLoader cl, ClassResourceSource crs) {
return crs.getClassResource("com.mysql.jdbc.PreparedStatement") != null;
} @Override
public String getId() {
return "mysql-explain-jr-plugin";
} // 省略其余方法,默认返回null即可
}

5. 在pom中指定插件入口类

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.6.1</version>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.0.2</version>
<configuration>
<archive>
<manifestEntries>
<JavaRebel-Plugin>com.example.plugin.mysql.MySQLExplainPlugin</JavaRebel-Plugin>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>

6. 打包

mvn clean package -Dmaven.test.skip=true

7. 使用该插件

项目需要使用jrebel启动,并且指定该插件所在路径,通过jvm参数指定
-Drebel.plugins=/your/path/mysql-explain-jr-plugin.jar
若生效,控制台会输出“MySQLExplainCBP...”等字样。

8 演示效果

此处加了一些自定义逻辑
  • 启动时输出MySQL执行计划配置:

  • 执行计划打印示例:

总结

基本实现了功能,可以结合idea插件达到更加便捷友好的使用体验!

项目地址

暂无,稍后补充

参考

基于JRebel开发的MySQL Explain插件的更多相关文章

  1. 基于JRebel开发的MybatisPlus热加载插件

    前言 前天项目中使用了mybatis-plus,但是搭配Jrebel开发项目时,发现修改mapper的xml,或者mapper方法中的注解,Jrebel并没有能够reload mapper.于是就有了 ...

  2. 手把手教你基于C#开发WinCC语音报警插件「附源代码」

    写在前面 众所周知,WinCC本身是可以利用C脚本或者VBS脚本来做语音报警,但是这种方式的本质是调用已存在的音频文件,想要实现实时播报报警信息是不行的,灵活性还不够,本文主要介绍基于C#/.NET开 ...

  3. 一些基于jQuery开发的控件

    基于jQuery开发,非常简单的水平方向折叠控件.主页:http://letmehaveblog.blogspot.com/2007/10/haccordion-simple-horizontal-a ...

  4. 「完整案例」基于Socket开发TCP传输客户端

    ​1 程序界面设计 TCP客户端在上位机开发中应用很广,大多数情况下,上位机软件都是作为一个TCP客户端来与PLC或其他服务器进行通信的.TCP客户端的主要功能就是连接服务器.发送数据.接收数据.断开 ...

  5. mac 下基于firebreath 开发多浏览器支持的浏览器插件

    mac 下基于firebreath 开发多浏览器支持的浏览器插件 首先要区分什么是浏览器扩展和浏览器插件;插件可以像本地程序一样做的更多 一. 关于 firebreath http://www.fir ...

  6. Mysql Explain 解读(基于MySQL 5.6.36)

    Mysql Explain 解读(基于MySQL 5.6.36) 1.语法 explain < table_name > #例子 explain select * from t3 wher ...

  7. Dropdown.js基于jQuery开发的轻量级下拉框插件

    Dropdown.js 前言 在SPA(Single Page Application)盛行的时代,jQuery插件的轮子正在减少,由于我厂有需求而开发了这个插件.如果觉得本文对您有帮助,请给个赞,以 ...

  8. PostgreSQL 优势,MySQL 数据库自身的特性并不十分丰富,触发器和存储过程的支持较弱,Greenplum、AWS 的 Redshift 等都是基于 PostgreSQL 开发的

    PostgreSQL 优势 2016-10-20 21:36 686人阅读 评论(0) 收藏 举报  分类: MYSQL数据库(5)  PostgreSQL 是一个自由的对象-关系数据库服务器(数据库 ...

  9. 基于gin的golang web开发:mysql增删改查

    Go语言访问mysql数据库需要用到标准库database/sql和mysql的驱动.标准库的Api使用比较繁琐这里再引入另一个库github.com/jmoiron/sqlx. go get git ...

随机推荐

  1. IT技术人员的自我修养

    1. 前言        在IT领域摸爬滚打多年,从一个普通程序员到技术主管,到技术经理,再到技术总监,踩过不少坑.加过不少班,也背过不少锅,在提升自身技术能力与管理能力的同时,也一直在思考,作为IT ...

  2. 剖析std::function接口与实现

    目录 前言 一.std::function的原理与接口 1.1 std::function是函数包装器 1.2 C++注重运行时效率 1.3 用函数指针实现多态 1.4 std::function的接 ...

  3. element el-table resetfields() 不生效

    表单中的重置按钮不生效的问题,结合文档对照后,发现是没有为el-form-item设置prop字段 所以,想让resetfields()生效有2个前提: form要设置ref,且ref值要与 this ...

  4. spring cloud eureka + feign,api远程调用

    网上教程不少,有些就是复制粘贴,不结合实际生产. eureka不再阐述. 一般正常开发会有多个工程,且多个module. 我的习惯是: eureka server.权限.config.gateway ...

  5. 跟着大彬读源码 - Redis 8 - 对象编码之字典

    目录 1 字典的实现 2 插入算法 3 rehash 与 渐进式 rehash 总结 字典,是一种用于保存键值对的抽象数据结构.由于 C 语言没有内置字典这种数据结构,因此 Redis 构建了自己的字 ...

  6. Nginx + Lua 搭建网站WAF防火墙

    前言 对于项目里面只是使用代理等常用功能,在线安装即可,如需制定化模块,则推荐编译安装 PS:本文不仅仅包含Nginx相关的知识点,还包含了逆天学习方法(对待新事物的处理) 官方网站:https:// ...

  7. HTML5 Device Access (设备访问)

    camera api (含图片预览) 参考地址 主要为利用input type=file, accept="image/*" 进行处理 图片预览方式(两种) const file ...

  8. Mysql 局域网连接设置——Windows

    在公司工作中,会遇到mysql数据库存储于某个人的电脑上,大家要想连接mysql服务,装有mysql服务的电脑就必须开启远程连接. 其实不仅仅是局域网,只要你有数据库所在服务器的公网IP地址都能连上. ...

  9. SonarQube系列三、Jenkins集成SonarQube(dotnetcore篇)

    [前言] 本系列主要讲述sonarqube的安装部署以及如何集成jenkins自动化分析.netcore项目.目录如下: SonarQube系列一.Linux安装与部署 SonarQube系列二.分析 ...

  10. 如何在GitHub上删除自己的项目?

    话不多说,直奔主题~ 1.打开GitHub,在主页左边有自己写的库. 2.拿删除第二个库wlh-hub/vue-zsgc为例,点击它,进入下面页面. 3.在导航栏一栏中,找到settings,并点击. ...