JDBC持久层访问相关解决方案
1.1 JDBC的思想和价值
JDBC是Java给出的一套基于面向接口的,针对持久层访问的解决方案。这些接口按照使用逻辑顺序有Driver/Connection/Statement/ResultSet(java.sql包下),分别解决驱动注册、连接、执行语句、结果集,而各大数据库厂商基于这套接口各自针对自己的数据库进行实现,是一个标准的面向接口编程的经典案例。
Java应用开发者同样面向接口去编程,通过接口规范方法进行数据库的CRUD操作,无需关心实现类细节,当然需要引入对应的数据库厂商的实现类,即jar包。从以上可以真切的看出了面向接口编程的巨大优势:
- 实现了调用者(码农)与被调用者(数据库厂商)解耦
- 增强了java应用对持久层实现的可移植性,同一套应用可以轻松的跨数据库运行
- 如果在项目团队中,那就很大提高了分工协作,提升团队开发效率
1.2 JDBC方式实现Mysql数据库访问以及原理详解
1.2.1 原理详解
- 注册驱动
- 数据库厂商实现Driver接口
- 应用程序通过 Class.forName()把Driver实现类加载到内存
- Driver实现类中编写了静态代码块,把驱动注册到驱动管理类
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
public Driver() throws SQLException {
}
static {
try {
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException("Can't register driver!");
}
}
}
重要说明:
DriverManager,该类类似工具类,各数据库厂在实现Driver类时,会借助驱动管理类的静态注册方法将驱动注册上去,随后开发人员可以通过驱动管理类静态方法获取数据库连接,从这个角度,可以总结JDBC就是
1 + N的解决方案,1-DriverManager工具类,N-一套接口。
- 获取连接
- 数据库厂商实现Connection接口
- 通过DriverManager静态方法获取连接DriverManager.getConnection(url,user,password)
- url:JDBC规定了url的格式,三部分组成:jdbc协议名,mysql子协议名,库连接(数据库厂商提供规范),其中IP代表目标主机、3306是数据库端口、test是数据库database、?问号后面可以跟参数,characterEncoding是指定客户端字符集编码,防止中文乱码。
样例:
jdbc:mysql://IP:3306/test?characterEncoding=utf-8
1.2.2 代码实现
import java.sql.*;
/**
* @author fengqingyang
* @create 2022-05-26 8:58
*/
public class JDBCTest {
public static void main(String[] args) {
Connection connection = null;
Statement statement = null;
ResultSet resultSet = null;
try {
//1 注册驱动
Class.forName("com.mysql.jdbc.Driver");
// 2 获取连接
String url = "jdbc:mysql://IPxxx:3306/test?characterEncoding=utf-8";
connection = DriverManager.getConnection(url, "root", "root");
// 3 获取执行平台
statement = connection.createStatement();
// 4 执行并处理结果
String name = "张三";
String sql = "select * from citizen where cust_name = '" + name + "'";
System.out.println("拼接的sql=" + sql);
resultSet = statement.executeQuery(sql);
while (resultSet.next()) {
String cust_name = resultSet.getString("cust_name");
String cust_cert = resultSet.getString("cust_cert");
System.out.println("cust_name=" + cust_name + ",cust_cert=" + cust_cert);
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
} finally {
// 5 关闭资源,遵循先开后关原则
if(resultSet != null){
try {
resultSet.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if(statement != null){
try {
statement.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if(connection != null){
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
}
打印结果:
1.3 关于SQL注入的理解和解决方式
1.3.1 原理详解&代码演示
- SQL注入原因:
首先能明确观察到上面JDBC查询代码中,SQL是以字符串拼接的形式完成的,这就带来了隐患,加上SQL入参由外部或者前台传入,若传入的字符串参数修改了原本SQL的语义,就造成了SQL注入。
代码演示:
// 4 执行并处理结果
//String name = "张三"; //正常的入参
String name = "不存在的名字' or '1'='1"; // 造成SQL注入的入参
String sql = "select * from citizen where cust_name = '" + name + "'";
System.out.println("拼接的sql=" + sql);
resultSet = statement.executeQuery(sql);
SQL注入导致异常查询结果:
- SQL注入的解决:
知道注入的原因是字符串拼接,那么就需要规避这个原因来规避SQL注入,java提供的预编译接口类PrepareStatement(Statement的子接口,同样由数据库厂商提供实现)可以解决SQL注入问题,该接口中执行sql方法(prepareStatement)会把传入的sql进行预编译,同事传入的sql不存在字符串拼接,而是通过占位符【?】来代替传参,随后执行时,给预编译的SQL传递参数,无论如何都不会改变SQL原本业务逻辑或者语义,注入问题也就解决了,同时预编译也会提高执行效率,同样的SQL一次编译,多次执行(避免多次编译)。
备注:
编译是数据库SQL执行前需要完成数据库内部工作,细分来说需要经过分析器(语法语义判断分析)、优化器(生成执行计划)、执行器(调用存储引擎进行查询),执行器之前的工作视为是SQL编译。
代码演示:
@Test
public void testSQLInjection() {
Connection connection = null;
Statement statement = null;
ResultSet resultSet = null;
try {
//1 注册驱动
Class.forName("com.mysql.jdbc.Driver");
// 2 获取连接
String url = "jdbc:mysql://linux124:3306/test?characterEncoding=utf-8";
connection = DriverManager.getConnection(url, "root", "root");
// 3 获取执行平台
String sql = "select * from citizen where cust_name = ?";
//进行sql预编译
PreparedStatement preparedStatement = connection.prepareStatement(sql);
// 4 执行并处理结果
//String name = "张三"; //正常的入参
String name = "不存在的名字' or '1'='1"; // 造成SQL注入的入参
//设置参数
preparedStatement.setString(1, name);
System.out.println("占位符的sql=" + sql);
resultSet = preparedStatement.executeQuery();
while (resultSet.next()) {
String cust_name = resultSet.getString("cust_name");
String cust_cert = resultSet.getString("cust_cert");
System.out.println("cust_name=" + cust_name + ",cust_cert=" + cust_cert);
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
} finally {
// 5 关闭资源,遵循先开后关原则
if (resultSet != null) {
try {
resultSet.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (statement != null) {
try {
statement.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
结果查询(已规避SQL注入):
1.4 JDBC的事务支持
1.4.1 原理详解
- 为什么需要事务?
由于实际业务中,我们可能需要进行一连串/多次数据操作,并且需要他们同时执行成功,例如银行转账业务,订单下单与库存数量等等,这其实就是业务的原子性、一致性,从数据库层面给出了解决方案,那就是事务控制,事务的特点就是ACID,原子性,一致性,隔离性、持久性。
- 使用事务也需要解决的问题?
由于事务之间针对共享数据的读写会存在脏读、不可重复度等问题。脏读就是A事务读取了B事务未提交的数据,不可重复度就是A事务中第一次读取数据结果与后续读取的结果不同(当然在A事务未提交期间,有其他事务对共享数据做了修改导致)。为此数据库提供了事务隔离的四个级别,这里我们只需要熟悉两个即可(其他两个基本用不到):
- 读已提交:这是oracle等采用的默认级别,可以解决脏读;
- 可重复读:Myql的默认级别,可以解决脏读和不可重复读;
代码演示:读已提交的比较容易理解,这里只演示下可重复读。
- 测试数据查看,状态=1的是两条数据:
- 窗口1中:开启事务(利用begin开启)后,进行数据查询(不进行commit)
- 窗口2中:开启事务,修改一条数据状态为0,然后提交事务(这里其实可以不用手动开启再提交,为了演示效果才这样),在窗口2中已经可以查看到一条记录被修改为state=0;
此时回到窗口1再次查看(窗口1此时未提交事务),可以看到此时在该事务中保持了可重复度。
然后在窗口1提交事务,再次查看,此时由于事务已经提交,所以能查看到窗口2(事务2)之前修改过的数据。
1.4.2 代码演示
JDBC的实现比较简单,就是调用connection的事务方法,需要事务控制的代码之前开启事务,正常结束之后提交事务,异常时回滚事务,详看代码注释:
@Test
public void testJDBCTransaction(){
Connection connection = null;
Statement statement = null;
ResultSet resultSet = null;
try {
//1 注册驱动
Class.forName("com.mysql.jdbc.Driver");
// 2 获取连接
String url = "jdbc:mysql://linux124:3306/test?characterEncoding=utf-8";
connection = DriverManager.getConnection(url, "root", "root");
// 3 获取执行平台
String sql = "update citizen set cust_name = ? where cust_name = ?";
//进行sql预编译
PreparedStatement preparedStatement = connection.prepareStatement(sql);
// 4 执行并处理结果
//设置参数
preparedStatement.setString(1, "王五");
preparedStatement.setString(2, "张三");
// 开始事务: 关闭默认提交就是开启事务
connection.setAutoCommit(false);
preparedStatement.executeUpdate();
// 正常提交
connection.commit();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
// 异常时,事务回滚
try {
connection.rollback();
} catch (SQLException e1) {
e1.printStackTrace();
}
} finally {
// 5 关闭资源,遵循先开后关原则
if (resultSet != null) {
try {
resultSet.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (statement != null) {
try {
statement.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
后续,我们在交流分享数据库连接池和DBUtils框架,感谢!