Steve's Blog

Talk is cheap, show me the code.

0%

MyBatis系列之-JDBC

截屏2022-08-15 23.04.54

本系列文章旨在学习整理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实现的PreparedStatementsetString()方法实现,代码略去了一些无关的代码,看这行:

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]