文档章节

从源码角度聊一聊JDBC和Mysql的预编译特性

kailuncen
 kailuncen
发布于 2017/05/20 18:49
字数 2591
阅读 1126
收藏 1
点赞 1
评论 2

凯伦说,公众号ID: KailunTalk,努力写出最优质的技术文章,欢迎关注探讨。
 

背景

最近因为工作调整的关系,都在和数据库打交道,增加了许多和JDBC亲密接触的机会,其实我们用的是Mybatis啦。知其然,知其所以然,是我们工程师童鞋们应该追求的事情,能够帮助你更好的理解这个技术,面对问题时更游刃有余。所以呢,最近就在业务时间对JDBC进行了小小的研究,有一些小收获,在此做个记录。

我们都知道市面上有很多数据库,比如Oracle,Sqlserver以及Mysql等,因为Mysql开放性以及可定制性比较强,平时在学校里或者在互联网从业的开发人员应该接触Mysql最多,本文后续的讲解也主要针对的是JDBC在Mysql驱动中的相关实现。

提纲

本文简单介绍了JDBC的由来,介绍了JDBC使用过程中的驱动加载代码,介绍了几个常用的接口,着重分析了Statement和Preparement使用上以及他们对待SQL注入上的区别。最后着重分析了PrepareStatement开启预编译前后,防SQL注入以及具体执行上的区别。

为什么需要JDBC

我们都知道,每家数据库的具体实现都会有所不同,如果开发者每接触一种新的数据库,都需要对其具体实现进行编程了,那我估计真正的代码还没开始写,先累死在底层的开发上了,同时这也不符合Java面向接口编程的特点。于是就有了JDBC。

JDBC(Java Data Base Connectivity,java数据库连接)是一种用于执行SQL语句的Java API,可以为多种关系数据库提供统一访问,它由一组用Java语言编写的类和接口组成。

 

 

 

 

 

 

 

 

 

 

如果用图来表示的话,如上图所示,开发者不必为每家数据通信协议的不同而疲于奔命,只需要面向JDBC提供的接口编程,在运行时,由对应的驱动程序操作对应的DB。

示例代码

光说不练假把式,奉上一段简单的示例代码,主要完成了获取数据库连接,执行SQL语句,打印返回结果,释放连接的过程。

package jdbc;

import java.sql.*;

/**
 * @author cenkailun
 * @Date 17/5/20
 * @Time 下午5:09
 */
public class Main {

	private static final String url = "jdbc:mysql://127.0.0.1:3306/demo";
	private static final String user = "root";
	private static final String password = "123456";

	static {
		try {
			Class.forName("com.mysql.jdbc.Driver");
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	public static void main(String[] args) throws SQLException {
		Connection connection = DriverManager.getConnection(url, user, password);

		System.out.println("Statement 语句结果: ");
		Statement statement = connection.createStatement();
		statement.execute("SELECT * FROM SU_City limit 3");
		ResultSet resultSet = statement.getResultSet();
		printResultSet(resultSet);
		resultSet.close();
		statement.close();
		System.out.println();

		System.out.println("PreparedStatement 语句结果: ");
		PreparedStatement preparedStatement = connection
				.prepareStatement("SELECT * FROM SU_City WHERE city_en_name = ? limit 3");
		preparedStatement.setString(1, "beijing");
		preparedStatement.execute();
		resultSet = preparedStatement.getResultSet();
		printResultSet(resultSet);
		resultSet.close();
		preparedStatement.close();
		connection.close();

	}

	/**
	 * 处理返回结果集
	 */
	private static void printResultSet(ResultSet rs) {
		try {
			ResultSetMetaData meta = rs.getMetaData();
			int cols = meta.getColumnCount();
			StringBuffer b = new StringBuffer();
			while (rs.next()) {
				for (int i = 1; i <= cols; i++) {
					b.append(meta.getColumnName(i) + "=");
					b.append(rs.getString(i) + "\t");
				}
				b.append("\n");
			}
			System.out.print(b.toString());
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

主要接口:

  • DriverManager: 管理驱动程序,主要用于调用驱动从数据库获取连接。
  • Connection: 代表了一个数据库连接。
  • Statement: 持有Sql语句,执行并返回执行后的结果。
  • ResulSet: Sql执行完毕,返回的记过持有

代码分析

接下来我们对示例代码进行分析,阐述相关的知识点,具体实现均针对

<dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.42</version>
</dependency>

驱动加载

在示例代码的static代码块,我们执行了

Class.forName("com.mysql.jdbc.Driver");

Class.forName会通过反射,初始化一个类。在com.mysql.jdbc.Driver,目测来说这是mysql对于JDBC中Driver接口的一个具体实现,在这个类里面,在其static代码块,它向DriverManager注册了自己。

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    //
    // Register ourselves with the DriverManager
    //
    static {
        try {
            java.sql.DriverManager.registerDriver(new Driver());
        } catch (SQLException E) {
            throw new RuntimeException("Can't register driver!");
        }
    }

    /**
     * Construct a new driver and register it with DriverManager
     * 
     * @throws SQLException
     *             if a database error occurs.
     */
    public Driver() throws SQLException {
        // Required for Class.forName().newInstance()
    }
}

 在DriverManger有一个CopyOnWriterArrayList,保存了注册驱动,以后可以再介绍一下它,它是在写的时候复制一份出去写,写完再复制回去。

    private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<DriverInfo>();

注册完驱动后,我们可以通过DriverManager拿到Connection,这里有一个疑问,如果注册了多个驱动怎么办? JDBC对这种也有应对方法,在选择使用哪个驱动的时候,会调用每个驱动实现的acceptsURL,判断这个驱动是不是符合条件。

public static Driver getDriver(String url)
        throws SQLException {
        Class<?> callerClass = Reflection.getCallerClass();
        for (DriverInfo aDriver : registeredDrivers) {
            if(isDriverAllowed(aDriver.driver, callerClass)) {
                try {
                    if(aDriver.driver.acceptsURL(url)) {
                         return (aDriver.driver);
                    }
..............................................

如果有多个符合条件的驱动,就先到先得呗~

接下来是构建Sql语句。statement有三个具体的实现类:

  1. PreparedStatement: PreparedStatement创建时就传过去一个sql语句,开始预编译的话,会返回语句ID,下次传语句ID和参数过去,就少了一次编译过程。
  2. Statement: Statement用Connection得到一个空的执行器,在执行的时候给它传拼好的死的sql ,因为是整一个SQL,所以完全匹配的概率低,每次都需要重新解析编译。
  3. CallableStatement   用于执行存储过程,目前没遇到过。

下文主要讲StatementPreparedStatement。

前提:mysql执行脚本的大致过程如下:prepare(准备)-> optimize(优化)-> exec(物理执行),其中,prepare也就是我们所说的编译。前面已经说过,对于同一个sql模板,如果能将prepare的结果缓存,以后如果再执行相同模板而参数不同的sql,就可以节省掉prepare(准备)的环节,从而节省sql执行的成本

Statement

Statement可以理解为,每次都会把SQL语句,完整传输到Mysql端,被人一直诟病的,就是其难以防止最简单的Sql注入。

2017-05-20T10:07:20.439856Z	   15 Query	SET NAMES latin1
2017-05-20T10:07:20.440138Z	   15 Query	SET character_set_results = NULL
2017-05-20T10:07:20.440733Z	   15 Query	SET autocommit=1
2017-05-20T10:07:20.445518Z	   15 Query	SELECT * FROM SU_City limit 3

我们对statement语句做适当改变,city_en_name = "'beijing' OR 1 = 1",就完成了SQL注入,因为普通的statement不会对SQL做任何处理,该例中单引号后的OR 生效,拉出了所有数据。

2017-05-20T10:10:02.739761Z	   17 Query	SELECT * FROM SU_City WHERE city_en_name = 'beijing' OR 1 = 1 limit 3

PreparedStatement

对于PreparedStatement,之前的认识是因为使用了这个,它会预编译,所以能防止SQL注入,所以为什么它能防止呢,说不清楚。我们先来看一下效果。

2017-05-20T10:14:16.841835Z	   19 Query	SELECT * FROM SU_City WHERE city_en_name = '\'beijing\' OR 1 = 1 ' limit 3

同样的代码,单引号被转义了,所以没被SQL注入。

但我希望大家注意到,在这里,我们并没有开启预编译哦。所以说因为开启预编译,能防止SQL注入是不对的。

围观了下代码,发现在未开启预编译的时候,在setString时,使用的是mysql驱动的PreparedStatement,在这个方法里,会对参数进行处理。

public void setString(int parameterIndex, String x) throws SQLException {

大致是在这里。

  for (int i = 0; i < stringLength; ++i) {
                        char c = x.charAt(i);

                        switch (c) {
                            case 0: /* Must be escaped for 'mysql' */
                                buf.append('\\');
                                buf.append('0');

                                break;

                            case '\n': /* Must be escaped for logs */
                                buf.append('\\');
                                buf.append('n');

                                break;

                            case '\r':
                                buf.append('\\');
                                buf.append('r');

                                break;

                            case '\\':
                                buf.append('\\');
                                buf.append('\\');

                                break;

                            case '\'':
                                buf.append('\\');
                                buf.append('\'');

                                break;

所以因为开启预编译才防止SQL注入是不对的,当然开启预编译后,确实也能防止。

Mysql其实是支持预编译的。你需要在JDBCURL里指定,这样就开启预编译成功。

"jdbc:mysql://127.0.0.1:3306/demo?useServerPrepStmts=true"

同时我们可以证明开启服务端预编译后,参数是在Mysql端进行转义了。下文是开启服务端预编译后,具体的日志情况。开启wireshark,可以看到传参数时是没有转义的,所以在服务端Mysql也能够对个别字符进行转义处理。

2017-05-20T10:27:53.618269Z	   20 Prepare	SELECT * FROM SU_City WHERE city_en_name = ? limit 3
2017-05-20T10:27:53.619532Z	   20 Execute	SELECT * FROM SU_City WHERE city_en_name = '\'beijing\' OR 1 = 1 ' limit 3

再深入一点,如果是新开启一个PrepareStatement,会看到,还是要预编译两次,那预编译的意义就没有了,等于每次都多了一次网络传输。

2017-05-20T10:33:26.206977Z	   23 Prepare	SELECT * FROM SU_City WHERE city_en_name = ? limit 3
2017-05-20T10:33:26.208019Z	   23 Execute	SELECT * FROM SU_City WHERE city_en_name = '\'beijing\' OR 1 = 1 ' limit 3
2017-05-20T10:33:26.208829Z	   23 Prepare	SELECT * FROM SU_City WHERE city_en_name = ? limit 3
2017-05-20T10:33:26.209098Z	   23 Execute	SELECT * FROM SU_City WHERE city_en_name = '\'beijing\' OR 1 = 1 ' limit 3

查询资料后,发现还要开启一个参数,让JVM端缓存,缓存是Connection级别的。然后看效果。

"jdbc:mysql://127.0.0.1:3306/demo?useServerPrepStmts=true&cachePrepStmts=true";

 查看日志,发现还是两次,😤我了。

2017-05-20T10:34:51.540301Z	   25 Prepare	SELECT * FROM SU_City WHERE city_en_name = ? limit 3
2017-05-20T10:34:51.541307Z	   25 Execute	SELECT * FROM SU_City WHERE city_en_name = '\'beijing\' OR 1 = 1 ' limit 3
2017-05-20T10:34:51.542025Z	   25 Prepare	SELECT * FROM SU_City WHERE city_en_name = ? limit 3
2017-05-20T10:34:51.542278Z	   25 Execute	SELECT * FROM SU_City WHERE city_en_name = '\'beijing\' OR 1 = 1 ' limit 3

阴差阳错,点进PrepareStatement的close方法,才看到如下代码,恍然大悟,一定要关闭,缓存才会生效。

public void close() throws SQLException {
        MySQLConnection locallyScopedConn = this.connection;

        if (locallyScopedConn == null) {
            return; // already closed
        }

        synchronized (locallyScopedConn.getConnectionMutex()) {
            if (this.isCached && isPoolable() && !this.isClosed) {
                clearParameters();
                this.isClosed = true;
                this.connection.recachePreparedStatement(this);
                return;
            }

            realClose(true, true);
        }
    }

其实是假装关闭了statement,其实是把statement塞进缓存了。然后我们再看看效果,完美。

2017-05-20T10:39:39.410584Z	   26 Prepare	SELECT * FROM SU_City WHERE city_en_name = ? limit 3
2017-05-20T10:39:39.411715Z	   26 Execute	SELECT * FROM SU_City WHERE city_en_name = '\'beijing\' OR 1 = 1 ' limit 3
2017-05-20T10:39:39.412388Z	   26 Execute	SELECT * FROM SU_City WHERE city_en_name = '\'beijing\' OR 1 = 1 ' limit 3

 

结论

1.JDBC是个好东西。

2.Statement没有防止SQL注入的能力。

3.PrepareStatement在没有开启预编译时,在本地对SQL进行参数化处理,对个别字符进行转移,开启预编译时,交由mysql端进行转移处理。

4.建议都使用PrepareStatement,因为其在本地也可以进行防SQL注入的简单处理,传输时和statement一样传输一条完整的sql。

5.如果开启PrepareStatement的useServerPrepStmts=true特性,请同时开启cachePrepStmts=true,否则同样的SQL模板,每次要进行一次编译,一次执行,网络开销成倍了,影响效率。

 

© 著作权归作者所有

共有 人打赏支持
kailuncen
粉丝 83
博文 17
码字总数 28778
作品 0
卢湾
后端工程师
加载中

评论(2)

kailuncen
kailuncen

引用来自“yongk”的评论

楼主,看mysql执行过程的log,2017-05-20T10:39:39.410584Z   26 Prepare  SELECT * FROM SU_City WHERE city_en_name = ? limit 3,这种log是mysql服务器打出的吗?不是jdbc驱动(Java程序)打出的吧?

回复@yongk : mysql打出来的
yongk
yongk
楼主,看mysql执行过程的log,2017-05-20T10:39:39.410584Z   26 Prepare  SELECT * FROM SU_City WHERE city_en_name = ? limit 3,这种log是mysql服务器打出的吗?不是jdbc驱动(Java程序)打出的吧?
sharding-jdbc源码解析全集

本文转自“天河聊技术”微信公众号 sharding-jdbc源码解析之词法解析 sharding源码解析之api分析 sharding-jdbc源码解析之spring集成 sharding-jdbc源码解析之spring集成分片构造实现 shardi...

天河2018
05/03
0
0
新浪、百度、好未来3offer到手全记录 | 牛客面经

新浪、百度、好未来3offer到手全记录 牛客面经 原创 2017-09-19 牛友 招聘消息汇总 渣渣的秋招之路 附上新浪,百度,好未来面经 作者:offer快到碗里来?。! 来源:牛客网 楼主是本科渣渣,...

公子只识黛玉
04/17
0
0
JDBC 5种常见数据库连接的获取

JDBC在开发中很少直接使用(持久化层有许多杰出的框架,如:Hibernate、mybatis...),但这又是Java程序员必须清楚的基础知识,下面是一些知识的基类,方便以后复习时使用。 Java对数据库的操...

learn_more
2014/12/11
0
0
sharding-jdbc源码分析—准备工作

原文作者:阿飞Javaer 原文链接:https://www.jianshu.com/p/7831817c1da8 接下来对sharding-jdbc源码的分析基于tag为源码,根据sharding-jdbc Features深入学习sharding-jdbc的几个主要特性...

飞哥-Javaer
05/03
0
0
一起学Java7新功能扩展——深入历险分享(一)

特此声明:因网友疑问,这里声明一个重要的安全,就是大家所知的java惊现0day漏洞!8月30日,Oralce紧急发布了新版本的JDK和JRE,原因是发现了一个严重的0day漏洞CVE-2012-4681,远程攻击者可...

Beyond-Bit
2012/09/03
0
26
JDBC(Java Data Base Connectivity,java数据库连接)

JDBC是一种用于执行SQL语句的Java API,可以为多种关系数据库提供统一访问,它由一组用Java语言编写的类和接口组成。 Java数据库连接体系结构是用于Java应用程序连接数据库的标准方法,JDBC对...

冰雷卡尔
2012/06/03
0
0
通过JDBC进行简单的增删改查(以MySQL为例)

前言:什么是JDBC 一、准备工作(一):MySQL安装配置和基础学习 二、准备工作(二):下载数据库对应的jar包并导入 三、JDBC基本操作 (1)定义记录的类(可选) (2)连接的获取 (3)insert (4...

Airship
2015/07/13
0
0
菜鸟网络java岗面经 已拿offer

牛客网上看了很多面经现在回馈一下牛友。 我是一个双非二本java。首先要谢谢我的一个李姓同学。他先去蚂蚁金服。这才告诉我们,双非二本只要技术好大公司也是不会拒绝你的。 还有就是牛客网上...

牛客网
05/10
0
0
为Java应用程序提供了空前的代码保护控件DashO-Pro

DashO-Pro是第三代的Java混淆器(obfuscator)、压缩机(compactor)、优化工具和水印工具(watermarker)。它为Java应用程序提供了空前的代码保护,并减少程序体积高达70%!除此之外,DashO...

netkongjian
2014/04/22
0
0
另辟蹊径创建移动应用:iOS和Android代码共享

过去几年,移动应用席卷了整个世界,在工作和生活的方方面面改变着我们使用互联网的方式。创建移动应用的各种技术也随之兴起,各种开发流程也 将移动应用视为一等公民,开始考虑适应移动开发...

程序袁_绪龙
2014/09/27
0
1

没有更多内容

加载失败,请刷新页面

加载更多

下一页

扫码二维码跳转到某个网站

添加maven依赖 <dependency><groupId>com.google.zxing</groupId><artifactId>core</artifactId><version>3.0.0</version></dependency><dependency><groupId>com.goog......

gaomq
2分钟前
0
0
Windows平台下搭建Git服务器的图文教程

Git没有客户端服务器端的概念,但是要共享Git仓库,就需要用到SSH协议(FTP , HTTPS , SFTP等协议也能实现Git共享,此文档不讨论),但是SSH有客户端服务器端,所以在windows下的开发要把自己...

MKChan
8分钟前
0
0
告警系统主脚本&告警系统配置文件&告警系统监控项目

20.20 告警系统主脚本 准备工作 定义监控系统的各个目录,然后再去定义主脚本,因为是分布式的,所以需要每一台机器都需要定义,事先创建好各个脚本和各个目录,随后脚本直接拷贝过去即可,然...

影夜Linux
9分钟前
0
0
谈谈神秘的ES6——(一)初识ECMAScript

谈谈神秘的ES6——(一)初识ECMAScript 在《零基础入门JavaScript》我们就说过,ECMAScript是JavaScript的核心,是JavaScript语法和语义的解释器,同时也是一个标准。而ECMAScript标准其实也...

JandenMa
56分钟前
1
0
第16章 Tomcat配置

16.1 Tomcat介绍 ####Tomcat介绍 LNMP架构针对的开发语言是PHP语言,php 是一门开发web程序非常流行的语言,早些年流行的是asp,在Windows平台上运行的一种编程语言,但安全性差,就网站开发...

Linux学习笔记
今天
0
0
流利阅读笔记29-20180718待学习

高等教育未来成谜,前景到底在哪里? Ray 2018-07-18 1.今日导读 在这个信息爆炸的年代,获取知识是一件越来越容易的事情。人们曾经认为,如此的时代进步会给高等教育带来众多便利。但事实的...

aibinxiao
今天
11
0
OSChina 周三乱弹 —— 你被我从 osc 老婆们名单中踢出了

Osc乱弹歌单(2018)请戳(这里) 【今日歌曲】 @小鱼丁:分享五月天的单曲《后来的我们 (电影《后来的我们》片名曲)》: 《后来的我们 (电影《后来的我们》片名曲)》- 五月天 手机党少年们想...

小小编辑
今天
513
18
Spring Boot Admin 2.0开箱体验

概述 在我之前的 《Spring Boot应用监控实战》 一文中,讲述了如何利用 Spring Boot Admin 1.5.X 版本来可视化地监控 Spring Boot 应用。说时迟,那时快,现在 Spring Boot Admin 都更新到 ...

CodeSheep
今天
4
0
Python + Selenium + Chrome 使用代理 auth 的用户名密码授权

米扑代理,全球领导的代理品牌,专注代理行业近十年,提供开放、私密、独享代理,并可免费试用 米扑代理官网:https://proxy.mimvp.com 本文示例,是结合米扑代理的私密、独享、开放代理,专...

sunboy2050
今天
0
0
实现异步有哪些方法

有哪些方法可以实现异步呢? 方式一:java 线程池 示例: @Test public final void test_ThreadPool() throws InterruptedException { ScheduledThreadPoolExecutor scheduledThre......

黄威
今天
1
0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部