最近由于工做调整的关系,都在和数据库打交道,增长了许多和JDBC亲密接触的机会,其实咱们用的是Mybatis啦。知其然,知其因此然,是咱们工程师童鞋们应该追求的事情,可以帮助你更好的理解这个技术,面对问题时更游刃有余。因此呢,最近就在业务时间对JDBC进行了小小的研究,有一些小收获,在此作个记录。java
咱们都知道市面上有不少数据库,好比Oracle,Sqlserver以及Mysql等,由于Mysql开放性以及可定制性比较强,平时在学校里或者在互联网从业的开发人员应该接触Mysql最多,本文后续的讲解也主要针对的是JDBC在Mysql驱动中的相关实现。mysql
本文简单介绍了JDBC的由来,介绍了JDBC使用过程当中的驱动加载代码,介绍了几个经常使用的接口,着重分析了Statement和Preparement使用上以及他们对待SQL注入上的区别。最后着重分析了PrepareStatement开启预编译先后,防SQL注入以及具体执行上的区别。sql
咱们都知道,每家数据库的具体实现都会有所不一样,若是开发者每接触一种新的数据库,都须要对其具体实现进行编程了,那我估计真正的代码还没开始写,先累死在底层的开发上了,同时这也不符合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(); } } }
接下来咱们对示例代码进行分析,阐述相关的知识点,具体实现均针对网络
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.42</version> </dependency>
在示例代码的static代码块,咱们执行了app
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有三个具体的实现类:
下文主要讲Statement和PreparedStatement。
前提:mysql执行脚本的大体过程以下:prepare(准备)-> optimize(优化)-> exec(物理执行),其中,prepare也就是咱们所说的编译。前面已经说过,对于同一个sql模板,若是能将prepare的结果缓存,之后若是再执行相同模板而参数不一样的sql,就能够节省掉prepare(准备)的环节,从而节省sql执行的成本
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,以前的认识是由于使用了这个,它会预编译,因此能防止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,在这个方法里,会对参数进行处理。
publicvoidsetString(intparameterIndex, String x)throwsSQLException {
大体是在这里。
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
想进一步了解更多,能够关注个人微信公众号