1、什么是JDBC

Java数据库连接Java Database Connectivity,简称JDBC)是Java语言中用来规范客户端程序如何来访问数据库的应用程序接口,提供了增删改查数据库中数据的方法。

**注:**我们通常说的JDBC是面向关系型数据库的。

JDBC操作数据库的过程如下图所示:

JDBC

由图可见:SUN公司为了简化Java 程序员对数据库的操作,制定了两套接口

  • 一套是JDBC API,供程序员操作数据库使用;
  • 另一套是JDBC驱动API,供数据库厂商将自家的数据库插入到驱动管理器中使用。

因此,对于程序员而言,只需掌握JDBC API即可。

2、JDBC依赖包与核心类

2.1、依赖包

  • JDBC核心类库包含在JDK自带的java.sql包和javax.sql包中。

  • 程序员若要使用JDBC操作MySQL数据库,则还需导入一个数据库驱动mysql-connector-java-5.1.47.jar包。

**注:**JDBC依赖包我们既可以手动下载导入,也可以使用Maven自动下载导入(重点掌握)。

2.2、核心类

2.1.1、DriverManager

DriverManager类:

  • 负责加载数据库驱动;
  • 负责建立与数据库的连接。

2.1.2、Connection

Connection类的对象:

  • 是一种特定数据库的连接(会话);
  • 主要用于创建Statement(SQL语句执行器)对象和数据库操作。

注:一个Connection(连接器)对象可以创建多个Statement(SQL语句执行器)对象。

2.1.3、Statement(重点)

Statement类:用于向数据库发送静态 SQL 语句,并返回 SQL 语句的执行结果。

Statement对象的executeQuery方法用于向数据库发送SQL查询语句,返回查询结果的ResultSet(结果集)对象;

Statement对象的executeUpdate方法用于向数据库发送SQL增加、删除、修改语句,返回数据库受影响的行数。

注:

  • Statement对象只有在执行SQL查询语句时,才会返回ResultSet(结果集)对象。
  • Statement对象在执行SQL更新、插入和删除语句时,返回的是受影响的行数。
  • 一个Statement(SQL语句执行器)最多只能获取一个结果集。

(1)CRUD操作——create

使用executeUpdate(String sql)方法完成数据添加操作。示例操作:

1
2
3
4
5
6
7
8
9
/*************创建Statement(SQL语句执行器)对象**************/
Statement statement = connection.createStatement();
/*************执行SQL语句*************/
//将待执行的SQL语句放入字符串中
String sql = "INSERT INTO 表名[(字段名1,字段名2,……)] VALUES(字段1的值,字段2的值,……)[,(字段1的值,字段2的值,……),……]";
int num = statement.executeUpdate(sql);//执行SQL语句,返回受影响的行数
if(num > 0){
System.out.println("插入成功");
}

(2)CRUD操作——delete

使用executeUpdate(String sql)方法完成数据删除操作。示例操作:

1
2
3
4
5
6
7
8
9
/*************创建Statement(SQL语句执行器)对象**************/
Statement statement = connection.createStatement();
/*************执行SQL语句*************/
//将待执行的SQL语句放入字符串中
String sql = "DELETE FROM 表名 WHERE 删除条件";
int num = statement.executeUpdate(sql);//执行SQL语句,返回受影响的行数
if(num > 0){
System.out.println("删除成功");
}

(3)CRUD操作——update

使用executeUpdate(String sql)方法完成数据修改操作。示例操作:

1
2
3
4
5
6
7
8
9
/*************创建Statement(SQL语句执行器)对象**************/
Statement statement = connection.createStatement();
/*************执行SQL语句*************/
//将待执行的SQL语句放入字符串中
String sql = "UPDATE 表名 SET 字段名1 = 字段1的新值[, 字段名2 = 字段2的新值,……] where 修改条件";
int num = statement.executeUpdate(sql);//执行SQL语句,返回受影响的行数
if(num > 0){
System.out.println("修改成功");
}

(4)CRUD操作——read

使用executeQuery(String sql)方法完成数据查询操作。示例操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*************创建Statement(SQL语句执行器)对象**************/
Statement statement = connection.createStatement();
/*************执行SQL语句,获取结果集*************/
//将待执行的SQL语句放入字符串中
String sql = "SELECT `id`,`name`,`sex` FROM `student` WHERE `name` LIKE '李%'";
ResultSet resultSet = statement.executeQuery(sql);//执行SQL语句,获得结果集
/*************处理结果集*************/
//根据获取列的数据类型,分别调用resultSet对象的相应方法将查询结果映射到java对象中
while (resultSet.next()){
System.out.println("id=" + resultSet.getObject("id"));
System.out.println("name=" + resultSet.getObject("name"));
System.out.println("sex=" + resultSet.getObject("sex"));
System.out.println("----------------------------------------");
}

2.1.4、PreparedStatement(重点)

详见:6、PreparedStatement对象详解

2.1.5、ResultSet

ResultSet (结果集)类的对象:用来存放SQL查询语句的查询结果。

这样,我们便可以在应用层使用ResultSet 类的方法对SQL查询语句的查询结果进行处理了。

3、如何使用JDBC操作数据库

3.1、JDBC操作数据库的步骤

  1. 加载驱动;
  2. 设置连接信息(URL、用户名和密码);
  3. 连接数据库,返回Connection(连接器)对象;
  4. Connection(连接器)对象创建Statement(SQL语句执行器)对象;
  5. Statement(SQL语句执行器)对象执行SQL语句,获取结果集;(核心步骤)
  6. 处理结果集;
  7. 释放连接资源。

**附:**URL格式

1
jdbc:mysql://主机地址:端口号/数据库名?参数1&参数2&参数3

注:MySQL默认端口号为3306

3.2、示例(第一个JDBC程序)

(1)新建一个JDBC测试项目

  • 新建项目:
创建一个项目
  • 选择SDK版本:
创建一个Java项目
  • 确定项目名称和存放位置:
创建一个JDBC测试项目

(2)在数据库中新建一个测试表

测试(student)表如下图所示:

student表

(3)导入数据库驱动包

  • 在项目中新建一个lib目录:
新建lib目录
  • 下载数据库驱动包mysql-connector-java-8.0.29.jar,并将它移动到lib目录下:
移到jar包

**注:**此时jar包并未导入到项目中,jar包不能展开。

  • 右键将lib目录下的jar包添加到项目的库中:
导入jar包
  • 创建lib库。
创建库

**注:**此时jar包便导入到项目中,jar包便能自然展开了。如下图所示:

导入后效果

(4)编写测试代码

  • 在src目录下新建一个com.atang.test包:
新建包
  • com.atang.test包中新建一个jdbcDemo类:

新建类

  • 编写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
package com.atang.test;

import java.sql.*;//JDBC核心类库包含在JDK自带的java.sql包和javax.sql包中

//JDBC测试程序
public class jdbcDemo {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
/*************1、加载驱动*************/
Class.forName("com.mysql.jdbc.Driver");//固定写法(加载驱动器类)

/*************2、设置连接信息(URL、用户名和密码)**************/
// 使用Unicode编码:useUnicode=true
// 使用utf-8字符集:characterEncoding=utf8
// 使用安全的连接:useSSL=true"
String url = "jdbc:mysql://localhost:3306/new_db?useUnicode=true&characterEncoding=utf8&useSSL=true";
String userName = "root";
String password = "123456";

/*************3、连接数据库,返回Connection(连接器)对象**************/
//使用驱动连接new_db数据库,返回Connection(连接器)对象
Connection connection = DriverManager.getConnection(url, userName, password);

/*************4、创建Statement(SQL语句执行器)对象**************/
Statement statement = connection.createStatement();
/*************5、执行SQL语句,获取结果集*************/
//将待执行的SQL语句放入字符串中
String sql = "SELECT `id`,`name`,`sex` FROM `student` WHERE `name` LIKE '李%'";
ResultSet resultSet = statement.executeQuery(sql);//执行SQL语句,获得结果集

/*************6、处理结果集*************/
while (resultSet.next()){
System.out.println("id=" + resultSet.getObject("id"));
System.out.println("name=" + resultSet.getObject("name"));
System.out.println("sex=" + resultSet.getObject("sex"));
System.out.println("----------------------------------------");
}
/*************7、释放连接资源*************/
resultSet.close();
statement.close();
connection.close();
}
}

执行结果为:

执行结果

4、简单封装

为了更加方便地使用JDBC操作数据库,避免代码的重复书写,我们可以在上述JDBC示例程序的基础上,对其进行简单封装。项目目录如下图所示:

简单封装-目录

4.1、新建配置文件

  • com.atang目录下新建一个db.properties配置文件(用于配置数据库连接信息)。
封装-配置文件

db.properties文件:

1
2
3
4
driver = com.mysql.jdbc.Driver
url = jdbc:mysql://localhost:3306/new_db?useUnicode=true&characterEncoding=utf8&useSSL=true
userName = root
password = 123456

4.2、封装JDBC工具类

  • 先在src目录下新建一个com.atang.jdbc1.utils包;
  • 然后,在utils目录下新建一个JdbcUtils类,用于封装JDBC工具。

JdbcUtils.java文件:

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
package com.atang.jdbc1.utils;

import java.io.IOException;
import java.io.InputStream;
import java.sql.*;
import java.util.Properties;

//简单封装的JDBC工具类
public class JdbcUtils {

//定义用于存储数据库配置信息的变量
private static String dirver = null;
private static String url = null;
private static String userName = null;
private static String password = null;

static {
try {
//读取配置文件,并将数据库配置信息流加载到Properties对象中
InputStream resourceAsStream = JdbcUtils.class.getClassLoader().getResourceAsStream("./com/atang/db.properties");
Properties properties = new Properties();
properties.load(resourceAsStream);

//获取Properties对象中的数据库配置信息
dirver = properties.getProperty("driver");
url = properties.getProperty("url");
userName = properties.getProperty("userName");
password = properties.getProperty("password");

//加载驱动(只需加载一次)
Class.forName(dirver);
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}

//获取数据库连接
public static Connection getConnection() throws SQLException {
return DriverManager.getConnection(url, userName, password);
}

//释放连接资源
public static void release(Connection conn, Statement st, ResultSet rs) throws SQLException {
if (rs != null){
rs.close();
}
if (st != null){
st.close();
}
if (conn != null){
conn.close();
}
}
}

4.3、封裝测试

(1)CRUD操作——create功能测试

  • jdbc1目录下新建一个TestInsert类,用于测试JDBC新增数据功能。

TestInsert.java文件:

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
package com.atang.jdbc1;

import com.atang.jdbc1.utils.JdbcUtils;

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class TestInsert {
public static void main(String[] args) throws SQLException {

Connection conn = null;//定义Connection对象
Statement st = null;//定义Statement对象
ResultSet rs = null;//定义ResultSet对象

try {
conn = JdbcUtils.getConnection();//获取数据库连接
st = conn.createStatement();//创建Statement(SQL语句执行器)对象
String sql = "INSERT INTO `student`(`name`,`sex`,`birthday`,`address`) VALUES('韩梅梅','女','2000-2-03','湖北武汉')";//将待执行的SQL语句放入字符串中
int i = st.executeUpdate(sql);//执行SQL语句,返回受影响的函数
if(i > 0){
System.out.println("插入数据成功");
}
} catch (SQLException throwables) {
throwables.printStackTrace();
} finally {
//释放连接资源
JdbcUtils.release(conn,st,rs);
}
}
}

运行后程序后,数据库数据变为:

简单封装create功能测试

(2)CRUD操作——delete功能测试

  • jdbc1目录下新建一个TestDelete类,用于测试JDBC删除数据功能。

TestDelete.java文件:

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
package com.atang.jdbc1;

import com.atang.jdbc1.utils.JdbcUtils;

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class TestDelete {
public static void main(String[] args) throws SQLException {

Connection conn = null;//定义Connection对象
Statement st = null;//定义Statement对象
ResultSet rs = null;//定义ResultSet对象

try {
conn = JdbcUtils.getConnection();//获取数据库连接
st = conn.createStatement();//创建Statement(SQL语句执行器)对象
String sql = "DELETE FROM `student` WHERE `name` = '韩梅梅'";//将待执行的SQL语句放入字符串中
int i = st.executeUpdate(sql);//执行SQL语句,返回受影响的函数
if(i > 0){
System.out.println("删除数据成功");
}
} catch (SQLException throwables) {
throwables.printStackTrace();
} finally {
//释放连接资源
JdbcUtils.release(conn,st,rs);
}
}
}

运行后程序后,数据库数据变为:

简单封装delete功能测试

(3)CRUD操作——update功能测试

  • jdbc1目录下新建一个TestUpdate类,用于测试JDBC修改数据功能。

TestUpdate.java文件:

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
package com.atang.jdbc1;

import com.atang.jdbc1.utils.JdbcUtils;

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class TestUpdate {
public static void main(String[] args) throws SQLException {

Connection conn = null;//定义Connection对象
Statement st = null;//定义Statement对象
ResultSet rs = null;//定义ResultSet对象

try {
conn = JdbcUtils.getConnection();//获取数据库连接
st = conn.createStatement();//创建Statement(SQL语句执行器)对象
String sql = "UPDATE `student` SET `birthday` = '1990-05-03' WHERE `name` = '张三'";//将待执行的SQL语句放入字符串中
int i = st.executeUpdate(sql);//执行SQL语句,返回受影响的函数
if(i > 0){
System.out.println("修改数据成功");
}
} catch (SQLException throwables) {
throwables.printStackTrace();
} finally {
//释放连接资源
JdbcUtils.release(conn,st,rs);
}
}
}

运行后程序后,数据库数据变为:

简单封装update功能测试

(4)CRUD操作——read功能测试

  • jdbc1目录下新建一个TestSelect类,用于测试JDBC查询数据功能。

TestSelect.java文件:

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
package com.atang.jdbc1;

import com.atang.jdbc1.utils.JdbcUtils;

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class TestSelect {
public static void main(String[] args) throws SQLException {

Connection conn = null;//定义Connection对象
Statement st = null;//定义Statement对象
ResultSet rs = null;//定义ResultSet对象

try {
conn = JdbcUtils.getConnection();//获取数据库连接
st = conn.createStatement();//创建Statement(SQL语句执行器)对象
String sql = "SELECT `id`,`name`,`address` FROM `student` WHERE `address` LIKE '%武汉%'";//将待执行的SQL语句放入字符串中
rs = st.executeQuery(sql);//执行SQL语句,返回查询结果集

//处理结果集
while (rs.next()){
System.out.println("id=" + rs.getObject("id"));
System.out.println("name=" + rs.getObject("name"));
System.out.println("address=" + rs.getObject("address"));
System.out.println("----------------------------------------");
}
} catch (SQLException throwables) {
throwables.printStackTrace();
} finally {
//释放连接资源
JdbcUtils.release(conn,st,rs);
}
}
}

运行程序后,查询结果为:

简单封装select功能测试

5、SQL注入问题

5.1、什么是SQL注入

SQL注入是指:web应用程序对用户输入数据的合法性没有判断或过滤不严,导致攻击者可以在web应用程序中事先定义好的查询语句的结尾上添加额外的SQL语句,在管理员不知情的情况下实现非法操作,以此来实现欺骗数据库服务器执行非授权的任意查询,从而进一步得到相应的数据信息。

注:

  • SQL注入是一种比较常见的网络攻击方式

  • SQL注入的本质是:由于实际项目中的SQL语句往往需要进行字符串拼接,黑客有时可以通过特殊的字符拼接,对SQL语句拼接过程中存在的漏洞进行攻击,进而实现无账号登录,篡改数据库。

5.2、典型示例

  • 在数据库中新建一个用户管理(users)表。

users表

  • jdbc1目录下新建一个SqlInjectProblem类,用于演示SQL注入问题。
SQL注入测试目录

SqlInjectProblem.java文件:

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
package com.atang.jdbc1;

import com.atang.jdbc1.utils.JdbcUtils;

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

public class SqlInjectProblem {
public static void main(String[] args) throws SQLException {
loginInfoDetect("张三","1234567");//校验登录用户信息
}

/****函数功能:登录用户信息校验****/
public static void loginInfoDetect(String userName, String password) throws SQLException {
Connection conn = null;//定义Connection对象
Statement st = null;//定义Statement对象
ResultSet rs = null;//定义ResultSet对象

try {
conn = JdbcUtils.getConnection();//获取数据库连接
st = conn.createStatement();//创建Statement(SQL语句执行器)对象
String sql = "SELECT * FROM `users` WHERE `name` = '" + userName+ "' AND `password` = '" + password +"'";//通过字符串拼接将待执行的SQL语句放入字符串中
rs = st.executeQuery(sql);//执行SQL语句,返回查询结果集
//判断用户信息是否验证成功
if (rs.isBeforeFirst()){
System.out.println("用户信息验证成功!");
//若验证成功,处理结果集
while (rs.next()){
System.out.println("name=" + rs.getObject("name"));
System.out.println("password=" + rs.getObject("password"));
System.out.println("----------------------------------------");
}
}
else {
System.out.println("用户信息验证失败!");
}
} catch (SQLException throwables) {
throwables.printStackTrace();
} finally {
//释放连接资源
JdbcUtils.release(conn,st,rs);
}
}
}

此时用户信息验证失败,运行结果为:

SQL注入测试验证失败

若在main函数中将校验登录的用户的信息改为:

1
2
3
public static void main(String[] args) throws SQLException {
loginInfoDetect("张三","123456");//校验登录用户信息
}

则用户信息验证成功,运行结果为:

SQL注入测试验证成功
  • 若在main函数中将校验登录的用户的信息改为:
1
2
3
public static void main(String[] args) throws SQLException {
loginInfoDetect(" 'or '1 = 1"," 'or '1 = 1");//SQL注入攻击
}

重新运行该程序,运行结果为:

SQL注入测试盗取用户信息成功

由此我们可以发现:即使我们没有输入正确的用户名和密码,我们仍然可以盗取所有的用户信息。

出现这一现象的主要问题在于:黑客利用了1 = 1恒成立的特性,通过特殊的字符拼接对SQL语句拼接过程中存在的漏洞进行了攻击,进而盗取到所有的用户信息,这一现象便是典型的**“SQL注入问题”**。

6、PreparedStatement对象详解

为了防止出现SQL注入问题,JAVA对Statement类进行了改进,为我们提供了一个PreparedStatement预备语句)类。

6.1、Statement对象 VS PreparedStatement对象

  • Statement对象执行SQL语句的步骤:

(1)创建Statement(SQL语句执行器)对象;

(2)将待执行的SQL语句放入字符串中;

(3)直接执行SQL语句。

  • PreparedStatement对象执行SQL语句的步骤:

(1)将待执行的SQL语句放入字符串中,但其中不填具体值,而是使用(占位符)占位;

(2)预编译SQL(不执行);

(3)依次给?(占位符)赋值;

(4)执行SQL语句。

注:

  • Statement类相比,PreparedStatement类更安全,效率更高。
  • 实际项目中,我们应当使用PreparedStatement对象执行SQL语句。

6.2、使用PreparedStatement对JDBC工具类进行封装测试

前面,我们介绍了如何使用Statement对象对JDBC工具类进行封装测试。接下来,我们将介绍如何使用PreparedStatement对象对JDBC工具类进行封装测试。

(1)CRUD操作——create功能测试

  • jdbc1目录下新建一个TestInsertPre类,用于测试JDBC新增数据功能。

TestInsertPre.java文件:

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
package com.atang.jdbc1;

import com.atang.jdbc1.utils.JdbcUtils;
import java.sql.*;

public class TestInsertPre {
public static void main(String[] args) throws SQLException {
Connection conn = null;//定义Connection对象
PreparedStatement preSt = null;//定义PreparedStatement对象
ResultSet rs = null;//定义ResultSet对象

try {
conn = JdbcUtils.getConnection();//获取数据库连接
//先将待执行的SQL语句放入字符串中,但values()中不填具体值,而是使用?(占位符)占位
String sql = "INSERT INTO `student`(`name`,`sex`,`birthday`,`address`) VALUES(?,?,?,?)";
preSt = conn.prepareStatement(sql);//然后,预编译SQL(不执行)
//然后,再依次给?(占位符)赋值
preSt.setString(1,"张三强");
preSt.setString(2,"男");
preSt.setDate(3,new java.sql.Date(new java.util.Date().getTime()));
preSt.setString(4,"安徽省合肥市");
//最后,再执行SQL语句,返回受影响的函数
int i = preSt.executeUpdate();//无形参

if(i > 0){
System.out.println("插入数据成功");
}
} catch (SQLException throwables) {
throwables.printStackTrace();
} finally {
//释放连接资源
JdbcUtils.release(conn,preSt,rs);
}
}
}

运行后程序后,数据库数据变为:

PreparedStatement之create功能测试

(2)CRUD操作——delete功能测试

  • jdbc1目录下新建一个TestDeletePre类,用于测试JDBC删除数据功能。

TestDeletePre.java文件:

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
package com.atang.jdbc1;

import com.atang.jdbc1.utils.JdbcUtils;
import java.sql.*;

public class TestDeletePre {
public static void main(String[] args) throws SQLException {
Connection conn = null;//定义Connection对象
PreparedStatement preSt = null;//定义PreparedStatement对象
ResultSet rs = null;//定义ResultSet对象

try {
conn = JdbcUtils.getConnection();//获取数据库连接
//先将待执行的SQL语句放入字符串中,但其中不填具体值,而是使用?(占位符)占位
String sql = "DELETE FROM `student` WHERE `name` = ?";
preSt = conn.prepareStatement(sql);//然后,预编译SQL(不执行)
//然后,再依次给?(占位符)赋值
preSt.setString(1,"李娜");
//最后,再执行SQL语句,返回受影响的函数
int i = preSt.executeUpdate();//无形参

if(i > 0){
System.out.println("删除数据成功");
}
} catch (SQLException throwables) {
throwables.printStackTrace();
} finally {
//释放连接资源
JdbcUtils.release(conn,preSt,rs);
}
}
}

运行后程序后,数据库数据变为:

PreparedStatement之delete功能测试

(3)CRUD操作——update功能测试

  • jdbc1目录下新建一个TestUpdatePre类,用于测试JDBC修改数据功能。

TestUpdatePre.java文件:

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
package com.atang.jdbc1;

import com.atang.jdbc1.utils.JdbcUtils;
import java.sql.*;

public class TestUpdatePre {
public static void main(String[] args) throws SQLException {
Connection conn = null;//定义Connection对象
PreparedStatement preSt = null;//定义PreparedStatement对象
ResultSet rs = null;//定义ResultSet对象

try {
conn = JdbcUtils.getConnection();//获取数据库连接
//先将待执行的SQL语句放入字符串中,但其中不填具体值,而是使用?(占位符)占位
String sql = "UPDATE `student` SET `address` = ? WHERE `name` = ?";
preSt = conn.prepareStatement(sql);//然后,预编译SQL(不执行)
//然后,再依次给?(占位符)赋值
preSt.setString(1,"湖北省武汉市东湖新技术开发区");
preSt.setString(2,"张三");
//最后,再执行SQL语句,返回受影响的函数
int i = preSt.executeUpdate();//无形参

if(i > 0){
System.out.println("修改数据成功");
}
} catch (SQLException throwables) {
throwables.printStackTrace();
} finally {
//释放连接资源
JdbcUtils.release(conn,preSt,rs);
}
}
}

运行后程序后,数据库数据变为:

PreparedStatement之update功能测试

(4)CRUD操作——read功能测试

  • jdbc1目录下新建一个TestSelectPre类,用于测试JDBC查询数据功能。

TestSelectPre.java文件:

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
package com.atang.jdbc1;

import com.atang.jdbc1.utils.JdbcUtils;
import java.sql.*;

public class TestSelectPre {
public static void main(String[] args) throws SQLException {
Connection conn = null;//定义Connection对象
PreparedStatement preSt = null;//定义PreparedStatement对象
ResultSet rs = null;//定义ResultSet对象

try {
conn = JdbcUtils.getConnection();//获取数据库连接
//先将待执行的SQL语句放入字符串中,但其中不填具体值,而是使用?(占位符)占位
String sql = "SELECT `id`,`name`,`address` FROM `student` WHERE `address` LIKE ?";
preSt = conn.prepareStatement(sql);//然后,预编译SQL(不执行)
//然后,再依次给?(占位符)赋值
preSt.setString(1,"%武汉%");
//最后,再执行SQL语句,返回查询结果集
rs = preSt.executeQuery();//无形参

//处理结果集
while (rs.next()){
System.out.println("id=" + rs.getObject("id"));
System.out.println("name=" + rs.getObject("name"));
System.out.println("address=" + rs.getObject("address"));
System.out.println("----------------------------------------");
}
} catch (SQLException throwables) {
throwables.printStackTrace();
} finally {
//释放连接资源
JdbcUtils.release(conn,preSt,rs);
}
}
}

运行程序后,查询结果为:

PreparedStatement之select功能测试

7、SQL注入问题的解决

接下来,我们在5.2、(SQL注入问题)典型示例的基础上,演示PreparedStatement对象是如何解决SQL注入问题的。

  • jdbc1目录下新建一个SqlCannotInject类,用于演示SQL注入问题。

SqlCannotInject.java文件:

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
package com.atang.jdbc1;

import com.atang.jdbc1.utils.JdbcUtils;

import java.sql.*;

public class SqlCannotInject {
public static void main(String[] args) throws SQLException {
loginInfoDetect("张三","123456");//校验登录用户信息
}

/****函数功能:登录用户信息校验****/
public static void loginInfoDetect(String userName, String password) throws SQLException {
Connection conn = null;//定义Connection对象
PreparedStatement preSt = null;//定义PreparedStatement对象
ResultSet rs = null;//定义ResultSet对象

try {
conn = JdbcUtils.getConnection();//获取数据库连接
//先将待执行的SQL语句放入字符串中,但其中不填具体值,而是使用?(占位符)占位
String sql = "SELECT * FROM `users` WHERE `name` = ? AND `password` = ?";
preSt = conn.prepareStatement(sql);//然后,预编译SQL(不执行)
//然后,再依次给?(占位符)赋值
preSt.setString(1,userName);
preSt.setString(2,password);
//最后,再执行SQL语句,返回查询结果集
rs = preSt.executeQuery();//无形参

//判断用户信息是否验证成功
if (rs.isBeforeFirst()){
System.out.println("用户信息验证成功!");
//若验证成功,处理结果集
while (rs.next()){
System.out.println("name=" + rs.getObject("name"));
System.out.println("password=" + rs.getObject("password"));
System.out.println("----------------------------------------");
}
}
else {
System.out.println("用户信息验证失败!");
}
} catch (SQLException throwables) {
throwables.printStackTrace();
} finally {
//释放连接资源
JdbcUtils.release(conn,preSt,rs);
}
}
}

此时用户信息验证成功,运行结果为:

SQL注入问题解决测试验证成功
  • 若在main函数中将校验登录的用户的信息改为:
1
2
3
public static void main(String[] args) throws SQLException {
loginInfoDetect(" 'or '1 = 1"," 'or '1 = 1");//SQL注入攻击
}

重新运行该程序,运行结果为:

SQL注入问题解决测试验证失败

由此我们可以发现:SQL注入问题被成功解决了

此时,黑客将无法在不知道用户名和密码的情况下,盗取数据库数据了。

8、JDBC处理事务

前面章节已经介绍事务的基本概念、ACID特性和SQL语句处理事务的过程,这里不再赘述。

JDBC处理事务与SQL语句处理事务的步骤基本一致,只是实现方式不同罢了。两者的区别主要包括

  • JDBC禁用事务自动提交功能的同时,事务会自动开启,不用再像SQL语句处理事务一样手动开启。
  • JDBC若处理事务失败,则自动回滚。(但为了逻辑清晰,一般不建议省略手动回滚代码!)
  • JDBC处理事务结束后会自动恢复事务自动提交功能。

**例:**已知account表的数据如下图所示,初始状态下:张三有100元、李四有50元。现在张三要转账20元给李四。

事务前

  • com.atang.test包中新建一个TransactionDemo类,用来演示JDBC处理事务的过程。

TransactionDemo.java文件:

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
package com.atang.test;

import com.atang.jdbc1.utils.JdbcUtils;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class TransactionDemo {
public static void main(String[] args) throws SQLException {
Connection conn = null;//定义Connection对象
PreparedStatement preSt = null;//定义PreparedStatement对象
ResultSet rs = null;//定义ResultSet对象

try {
conn = JdbcUtils.getConnection();//获取数据库连接
/******1、禁用事务自动提交功能,此时事务自动开启******/
conn.setAutoCommit(false);//关闭MySQL默认的事务自动提交功能(开启事务)

/*****2、编写事务中的业务******/
//先将待执行的SQL语句放入字符串中,但其中不填具体值,而是使用?(占位符)占位
String sql1 = "UPDATE `account` SET `money` = `money` - 20 WHERE `name` = ?";
preSt = conn.prepareStatement(sql1);//然后,预编译SQL(不执行)
//然后,再依次给?(占位符)赋值
preSt.setString(1,"张三");
//最后,再执行SQL语句,返回受影响的函数
preSt.executeUpdate();//无形参

//先将待执行的SQL语句放入字符串中,但其中不填具体值,而是使用?(占位符)占位
String sql2 = "UPDATE `account` SET `money` = `money` + 20 WHERE `name` = ?";
preSt = conn.prepareStatement(sql2);//然后,预编译SQL(不执行)
//然后,再依次给?(占位符)赋值
preSt.setString(1,"李四");
//最后,再执行SQL语句,返回受影响的函数
preSt.executeUpdate();//无形参
/******业务完毕,结束事务******/
/******3、若执行成功,则提交事务,使数据持久化******/
conn.commit();
System.out.println("事务处理成功!");
} catch (Exception e) {
/******4、若执行失败,则回滚事务,即:事务不执行,回滚到事务执行前的状态******/
conn.rollback();
System.out.println("事务处理失败!");
} finally {
//释放连接资源
JdbcUtils.release(conn,preSt,rs);
}
}
}

运行该程序,运行结果为:

事务处理成功

此时事务处理成功,数据库数据变为:

事务处理成功后数据

  • 若在事务的处理过程中添加如下代码:
1
int x = 1/0;//人为制造错误,使业务处理过程中断

此时TransactionDemo.java文件变为:

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
package com.atang.test;

import com.atang.jdbc1.utils.JdbcUtils;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class TransactionDemo {
public static void main(String[] args) throws SQLException {
Connection conn = null;//定义Connection对象
PreparedStatement preSt = null;//定义PreparedStatement对象
ResultSet rs = null;//定义ResultSet对象

try {
conn = JdbcUtils.getConnection();//获取数据库连接
/******1、禁用事务自动提交功能,此时事务自动开启******/
conn.setAutoCommit(false);//关闭MySQL默认的事务自动提交功能(开启事务)

/*****2、编写事务中的业务******/
//先将待执行的SQL语句放入字符串中,但其中不填具体值,而是使用?(占位符)占位
String sql1 = "UPDATE `account` SET `money` = `money` - 20 WHERE `name` = ?";
preSt = conn.prepareStatement(sql1);//然后,预编译SQL(不执行)
//然后,再依次给?(占位符)赋值
preSt.setString(1,"张三");
//最后,再执行SQL语句,返回受影响的函数
preSt.executeUpdate();//无形参

int x = 1/0;//人为制造错误,使业务处理过程中断

//先将待执行的SQL语句放入字符串中,但其中不填具体值,而是使用?(占位符)占位
String sql2 = "UPDATE `account` SET `money` = `money` + 20 WHERE `name` = ?";
preSt = conn.prepareStatement(sql2);//然后,预编译SQL(不执行)
//然后,再依次给?(占位符)赋值
preSt.setString(1,"李四");
//最后,再执行SQL语句,返回受影响的函数
preSt.executeUpdate();//无形参
/******业务完毕,结束事务******/
/******3、若执行成功,则提交事务,使数据持久化******/
conn.commit();
System.out.println("事务处理成功!");
} catch (Exception e) {
/******4、若执行失败,则回滚事务,即:事务不执行,回滚到事务执行前的状态******/
conn.rollback();
System.out.println("事务处理失败!");
} finally {
//释放连接资源
JdbcUtils.release(conn,preSt,rs);
}
}
}

运行该程序,运行结果为:

事务处理失败

此时事务处理失败,数据库数据回滚到事务执行前的状态。

事务处理失败后数据

9、数据库连接池(池化技术)

9.1、为什么使用数据库连接池

频繁地进行数据库连接和释放是十分浪费系统资源的,这一点在多用户的网页应用程序中体现得尤为突出。数据库连接池正是针对这个问题提出来的。

9.2、什么是数据库连接池

**数据库连接池(Database Connection Pooling)**是指在程序初始化时就创建一定数量的数据库连接对象并将其保存在一块内存区中,它允许应用程序重复使用同一个现有的数据库连接,而不是重新建立一个;释放空闲时间超过最大空闲时间的数据库连接以避免因为没有释放数据库连接而引起的数据库连接遗漏。

即在程序初始化的时候就创建一定数量的数据库连接,用完可以放回去,下一个再接着用,通过配置连接池的参数来控制连接池中的初始连接数、最小连接、最大连接、最大空闲时间等参数来保证访问数据库的数量在一定可控制的范围类,防止系统崩溃,提升用户体验。

数据库连接池

这项技术能明显提高对数据库操作的性能。

9.3、连接池的实现(数据源)

编写数据库连接池只需要实现一个DataSource接口,通常我们把DataSource的实现,即连接池的实现,按其英文含义称之为数据源

数据源中都包含了数据库连接池的实现,DataSource 接口由数据库驱动程序供应商实现。实际项目开发中我们不需要编写连接数据库代码,直接从数据源获得数据库的连接即可。

9.4、Java中常用的连接池

目前,Java中开源的数据库连接池主要有:

  • DBCP

DBCP **(Database Connection Pool)**是一个依赖Jakarta commons-pool对象池机制的数据库连接池。DBCP可能是使用最多的开源连接池,Tomcat的数据源使用的就是DBCP

  • C3P0

C3P0是另外一个开源的数据库连接池,在业界也比较有名。使用它的开源项目有Hibernate、Spring等。

  • Druid

Druid是阿里巴巴开源平台上一个数据库连接池实现,结合了 C3P0DBCP 等 DB 池的优点,同时加入了日志监控。它在功能、性能、扩展性方面,都超过其他数据库连接池,包括DBCP、C3P0、BoneCP、Proxool、JBoss DataSource等。

**注:**无论使用什么数据源,它们的本质都是实现DataSource接口。

(本讲完,系列博文持续更新中…… )

阿汤笔迹微信公众平台

关注**“阿汤笔迹”** 微信公众号,获取更多学习笔记。
原文地址:http://www.atangbiji.com/2022/08/31/jdbcInDetail
博主最新文章在个人博客 http://www.atangbiji.com/ 发布。