本系列文章旨在学习整理MyBatis的一些知识。本文是本系列的第一篇,用来介绍JDBC的内容,本文不是纯新手教程,如果有疑问,请查询官方文档。
1. JDBC的基本使用
JDBC(Java database connectivity),是Java官方出的一个接口标准,第三方厂家可以通过实现此标准来让自己的产品可以通过JDBC通用接口来连接。
常用的几个类:
- Connection:表示一个数据库连接
- Statement / PreparedStatement:表示一个数据库语句,区别是PreparedStatement可以防止SQL注入
- ResultSet:通过Statement查到的结果集
- ResultSetMetaData:通过ResultSet获得的结果集元数据
- DataSource:数据库连接池,对Connection进行池化的操作接口
通过Connection、Statement 、ResultSet就可以使用JDBC连接数据库了。以下是代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| import java.sql.*;
public class JdbcDemo {
public static void main(String[] args) { String username = "root"; String password = "1234"; String jdbcUrl = "jdbc:MySQL:///lu_tale"; String driverClass = "com.MySQL.jdbc.Driver"; String sql = "SELECT uid, username, password" + " FROM t_users WHERE username = ?"; Connection connection = null; PreparedStatement ps = null; ResultSet rs = null; ResultSetMetaData rsmd = null; try { Class.forName(driverClass); connection = DriverManager.getConnection(jdbcUrl, username, password); ps = connection.prepareStatement(sql); ps.setString(1, "admin' and 1 = 1"); rs = ps.executeQuery(); rsmd = rs.getMetaData(); if (rs.next()) { int id = rs.getInt(1); String name = rs.getString(2); String bestSong = rs.getString(3); System.out.println(rsmd.getColumnLabel(1) + ": " + id + ", " + rsmd.getColumnLabel(2) + ": " + name + ", " + rsmd.getColumnLabel(3) + ": " + bestSong); }
} catch (Exception e) { e.printStackTrace(); } finally { if (rs != null) { try { rs.close(); } catch (Exception e) { e.printStackTrace(); } } if (ps != null) { try { ps.close(); } catch (Exception e) { e.printStackTrace(); } } if (connection != null) { try { connection.close(); } catch (Exception e) { e.printStackTrace(); } } } } }
|
使用数据库连接池:
数据库连接池实现常用的有3种:DBCP、C3P0和Druid。
此处不详述,具体可以参见我刚开始学习时写的demo code。
2. PreparedStatement为什么能防SQL注入?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
| public void setString(int parameterIndex, String x) throws SQLException { synchronized(this.checkClosed().getConnectionMutex()) { if (this.isLoadDataQuery || this.isEscapeNeededForString(x, stringLength)) { needsQuoted = false; buf = new StringBuilder((int)((double)x.length() * 1.1)); buf.append('\'');
for(int i = 0; i < stringLength; ++i) { char c = x.charAt(i); switch (c) { case '\u0000': buf.append('\\'); buf.append('0'); break; case '\n': buf.append('\\'); buf.append('n'); break; case '\r': buf.append('\\'); buf.append('r'); break; case '\u001a': buf.append('\\'); buf.append('Z'); break; case '"': if (this.usingAnsiMode) { buf.append('\\'); }
buf.append('"'); break; case '\'': buf.append('\\'); buf.append('\''); break; case '\\': buf.append('\\'); buf.append('\\'); break; case '¥': case '₩': if (this.charsetEncoder != null) { CharBuffer cbuf = CharBuffer.allocate(1); ByteBuffer bbuf = ByteBuffer.allocate(1); cbuf.put(c); cbuf.position(0); this.charsetEncoder.encode(cbuf, bbuf, true); if (bbuf.get(0) == 92) { buf.append('\\'); } }
buf.append(c); break; default: buf.append(c); } }
buf.append('\''); parameterAsString = buf.toString(); }
}
|
这是MySQL实现的PreparedStatement
的setString()
方法实现,代码略去了一些无关的代码,看这行:
1 2 3 4 5
| case '\'': buf.append('\\'); buf.append('\''); break;
|
在进入这个逻辑之前,先进行了判断
1
| if (this.isLoadDataQuery || this.isEscapeNeededForString(x, stringLength))
|
这行中调用了isEscapeNeededForString()
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| private boolean isEscapeNeededForString(String x, int stringLength) { boolean needsHexEscape = false;
for(int i = 0; i < stringLength; ++i) { char c = x.charAt(i); switch (c) { case '\u0000': needsHexEscape = true; break; case '\n': needsHexEscape = true; break; case '\r': needsHexEscape = true; break; case '\u001a': needsHexEscape = true; break; case '"': needsHexEscape = true; break; case '\'': needsHexEscape = true; break; case '\\': needsHexEscape = true; }
if (needsHexEscape) { break; } }
return needsHexEscape; }
|
也就是,如果传入的参数中含有单引号'
,PreparedStatement会为其添加转义符,以防止SQL注入。
1 2 3 4
| [admin' and '1' = '1]
[admin\' and \'1\' = \'1]
|