目录
一、前言
二、JDBC API概述
三、获取连接的三种方式
0.朝花夕拾 :
1.方式一 —— 通过new关键字 :
2.方式二 —— 通过反射机制 :
3.方式三 —— 通过DriverManager
Δ方式三简化版
Δ方式三优化版
四、 ResultSet
1.简介 :
2.代码演示 :
3.底层实现 :
五、SQL注入
1.什么是SQL注入?
2.SQL注入演示 :
3.PreparedStatement :
①简介
②牛逼之处
③使用演示
六、总结 :
- 第二节内容,up主要和大家分享一下JDBC——API方面的内容。
- 注意事项——①代码中的注释也很重要;②不要眼高手低;③点击文章的侧边栏目录或者文章开头的目录可以进行跳转。
- 良工不示人以朴,所有文章都会适时补充完善。大家如果有问题都可以在评论区进行交流或者私信up。感谢阅读!
JDBC API是一系列的接口,它统一和规范了应用程序与数据库的连接,执行SQL语句并得到返回结果等各类操作,相关类和接口在java.sql和javax.sql包下。
相关体系图如下(建议阅读完毕后返回来细品☕) :
上一小节内容中,我们提到了编写JDBC程序的核心四部曲,这里再来回顾一下——
这里我们要重点再说一下第二个步骤——即获取数据库的连接。
这也是我们在第一小节中,演示第一个JDBC程序时用到的方法。即先通过com.mysql.cj.jdbc.Driver()来获取到Driver类对象,然后再通过Driver类中的connect方法来获取连接。connect方法的详细信息如下:
Connection connect(String url, Properties info) :需要传入一个包含数据库信息的url字符串对象,以及一个包含登录用户信息的Properties对象。
这种方法有什么弊端?
通过new的方法获取到Driver对象,Driver对象属于第三方,并且是静态加载,导致灵活性低,依赖性强。
up以JdbcConn类为演示类,来给大家演示一下第一种方式获取连接,其实就是把第一小节的程序演示再来一遍罢了(当然这里我们不会像第一小节讲那么细了)。
代码如下 :
package api.connection;
import com.mysql.cj.jdbc.Driver;
import org.testng.annotations.Test;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;
/**
* @author : Cyan_RA9
* @version : 21.0
*/
public class JdbcConn {
//演示JDBC连接数据库的三种方式
//1.方式一 —— new关键字静态加载
@Test
public void connection_1() throws SQLException {
Driver driver = new Driver();
String url = "jdbc:mysql://localhost:3306/jdbc_ex";
Properties info = new Properties();
info.setProperty("user","root");
info.setProperty("password","RA9_Cyan");
Connection connect = driver.connect(url, info);
System.out.println("方式一获取到的连接 = " + connect);
connect.close();
System.out.println("--------------------------------------------------");
}
}
运行结果 :
提到了灵活性和依赖性,我们就不由得想到了反射机制。反射机制可以动态的加载和构建对象,属于动态加载,相比new关键字的方式具有更高的灵活性,同时也减低了依赖性。我们可以使用 Class.forName("com.mysql.cj.jdbc.Driver"); 来获取Driver类实例。
up仍然以JdbcConn类为演示类,代码如下 :
package api.connection;
import com.mysql.cj.jdbc.Driver;
import org.testng.annotations.Test;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;
public class JdbcConn {
//演示JDBC连接数据库的三种方式
//2.方式二 —— 反射机制
@Test
public void connection_2() throws ClassNotFoundException, InstantiationException, IllegalAccessException, SQLException {
Class> clazz = Class.forName("com.mysql.cj.jdbc.Driver");
Driver driver = (Driver) clazz.newInstance();
String url = "jdbc:mysql://localhost:3306/jdbc_ex";
Properties info = new Properties();
info.setProperty("user", "root");
info.setProperty("password", "RA9_Cyan");
Connection connect = driver.connect(url, info);
System.out.println("方式二获取到的连接 = " + connect);
System.out.println("--------------------------------------------------");
}
}
运行结果 :
在反射机制的基础上,使用DriverManager替代Driver,进行统一管理,具有更好的拓展性。并且,单独定义url, user, password也具有更高的灵活性。
需要用到DriverManager类的两个方法,如下——
- static void registerDriver(Driver driver) : 根据传入的Driver类对象,注册Driver驱动。
- static Connection getConnection(String url, String user, String password) : 根据传入的数据库URL,获取数据库连接。
up仍然以JdbcConn类为演示类,代码如下 :
package api.connection;
import com.mysql.cj.jdbc.Driver;
import org.testng.annotations.Test;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Properties;
public class JdbcConn {
//演示JDBC连接数据库的三种方式
//方式三 —— 通过DriverManager
@Test
public void connection_3() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, SQLException {
//使用反射机制加载Driver类
Class> clazz = Class.forName("com.mysql.cj.jdbc.Driver");
Constructor> constructor = clazz.getConstructor();
Driver driver = (Driver) constructor.newInstance();
//创建url,user,password
String url = "jdbc:mysql://localhost:3306/jdbc_ex";
String user = "root";
String password = "RA9_Cyan";
//注册Driver驱动
DriverManager.registerDriver(driver);
//获取连接
Connection connection = DriverManager.getConnection(url, user, password);
System.out.println("方式三获取到的连接= " + connection);
}
}
运行结果 :
PS_1 :
其实,在方式三的基础上,可以进行简化——
①通过Class.forName()方法动态加载Driver类后,不需要接收Class对象,也不需要获取构造器对象再得到Driver类对象。
②不需要通过DriverManager类的registerDriver方法来注册Driver驱动,即不需要注册驱动,而是直接通过getConnection方法来获取连接。
仍然以JdbcConn类为演示类,代码如下 :
package api.connection;
import com.mysql.cj.jdbc.Driver;
import org.testng.annotations.Test;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Properties;
public class JdbcConn {
//演示JDBC连接数据库的三种方式
//方式三 —— DriverManager(简化版)
@Test
public void connection_3() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, SQLException {
Class.forName("com.mysql.cj.jdbc.Driver");
String url = "jdbc:mysql://localhost:3306/jdbc_ex";
String user = "root";
String password = "RA9_Cyan";
Connection connection = DriverManager.getConnection(url, user, password);
System.out.println("方式三简化后得到的连接 = " + connection);
}
}
运行结果 :
可以看到, 简化后,整个代码简洁了许多。
但是,这时候可能就要有p小将(Personable小将,指风度翩翩的人)出来bb问了:把编写JDBC程序的核心四部曲背的比家谱都熟,第一步就是注册驱动,好家伙,隔你这儿直接给省略了?给爬!
p哥先息怒,其实这里之所以能顺利获取连接,是因为jvm底层做了优化,当Driver类被动态加载时,会自动帮我们注册Driver驱动,我们查看com.mysql.cj.jdbc.Driver类的源码,可以找到一个静态代码块,如下 :
static {
try {
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException("Can't register driver!");
}
}
答案很明显了——当Driver类被动态加载时,静态代码块被执行。而静态代码块里的try语句中,调用了DriverManager类的registerDriver方法,完成了“注册驱动”的操作。
还要说明一点,这种“简化版”的第三种方式,是实际开发中用到最多的。
PS_2 :
其实,在上述“简化版”的第三种方式中,就连调用forName的语句都可以省略。MySQL 5.1.6及以上版本无需使用forName语句;从JDK1.5以后使用了JDBC4,不再需要显示调用Class.forName(...)注册驱动,而是自动调用驱动,根据jar包下META-INF\services\java.sql.Driver文本中的类名称去注册,如下图所示 :
但是,就像我们上面说的那样,“简化版”的方式三是实际开发中用到最多的方式,因此还是建议大家写上,以更明确。
在简化版的基础上,我们可以将url, user,以及password中的各种信息,诸如端口,数据库,用户名和用户密码等保存到properties配置文件中,使得我们的操作更加快捷和灵活。
up先在JdbcConn类本包下,创建一个mysql.properties文件,如下图所示 :
JdbcConn类代码如下 :
package api.connection;
import com.mysql.cj.jdbc.Driver;
import org.testng.annotations.Test;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Properties;
public class JdbcConn {
//演示JDBC连接数据库的三种方式
//方式三 —— DriverManager
@Test
public void connection_3() throws ClassNotFoundException, SQLException, IOException {
//通过Properties对象获取配置文件信息
Properties properties = new Properties();
properties.load(new FileInputStream("src/api/connection/mysql.properties"));
//通过获取到的配置文件信息,得到对应的值
String driver = properties.getProperty("driver");
String url = properties.getProperty("url");
String user = properties.getProperty("user");
String password = properties.getProperty("password");
//注册驱动
Class.forName(driver);
//获取连接
Connection connection = DriverManager.getConnection(url, user, password);
System.out.println("方式三优化后得到的连接 = " + connection);
}
}
ResultSet表示数据结果集的数据表,通常通过DQL(Data Query Language)来生成。ResultSet对象保持一个光标,该光标指向其当前的数据行。最初,光标位于第一行之前,next方法会使光标移动到下一行,并且当ResultSet对象中没有更多行时返回false,因此可以使用While循环来遍历结果集。
默认的ResultSet对象不可更新,并且只有一个向前移动的光标。因此,默认只能从第一行到最后一行迭代一次。但是,可以手动生成可滚动/可更新的ResultSet对象。
PS_1 : 若有需求让光标向上移动一行,可以使用previous()方法;如果再往上没有行可以返回时,返回false。
PS_2 : 使用getXxx()方法返回获得的记录(一行数据)中指定的字段,需要传入要获取的字段的索引(从1开始);或者也可以直接传入字段名。
PS_3 : 若有需求以对象的形式来接收返回的字段,可以使用getObject(...)方法,传入的实参与getXxx方法一致。
根据对ResultSet的描述,我们不难会联想到迭代器的执行原理。只不过相比迭代器来说,ResultSet的next方法是把两件事都干了——判断和移动指针。
现有一张学生表如下 :
根据ResultSet结果集的简介,当我们通过while循环遍历结果集时,一开始ResultSet保持的光标位置会指在学生表第一条记录的上面,如下图所示 :
现在我们通过JDBC的方式查询这张表,up以ResultSet_Demo类为演示类,代码如下 :
package api.resultSet;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.sql.*;
import java.util.Properties;
public class ResultSet_Demo {
public static void main(String[] args) throws IOException, ClassNotFoundException, SQLException {
//编写JDBC程序核心四部曲:
Properties properties = new Properties();
properties.load(new FileInputStream("src/api/connection/mysql.properties"));
String driver = properties.getProperty("driver");
String url = properties.getProperty("url");
String user = properties.getProperty("user");
String password = properties.getProperty("password");
//1.注册驱动
Class.forName(driver);
//2.获取连接
Connection connection = DriverManager.getConnection(url, user, password);
//3.执行SQL
Statement statement = connection.createStatement();
String sql = "SELECT * FROM stus;";
ResultSet resultSet = statement.executeQuery(sql);
/**
注意 : 执行DQL(数据查询语句)要使用Statement类中的executeQuery方法。
*/
while (resultSet.next()) { //使用while循环来遍历结果集
//获取当前光标指向的记录的第一个字段
int id = resultSet.getInt(1);
//获取第二个字段
String name = resultSet.getString(2);
//获取第三个字段
String sex = resultSet.getString(3);
//获取第四个字段
double score = resultSet.getDouble(4);
/*打印获取的字段*/
System.out.println(String.format("%d\t%5s\t%s\t%.2f", id,name,sex,score));
}
//4.释放资源
resultSet.close(); //结果集也需要关闭!
statement.close();
connection.close();
}
}
运行结果 :
接下来,我们通过Debug的方式看一下ResultSet类的源码,看看它底层到底是如何实现的。
在返回结果集的代码行设置断点,进入Debug,如下图所示 :
可以发现ResultSet对象其实是一个ResultSet接口的实现类(JDBC规定要实现的接口),如下图所示 :
该实现类又继承了NativeResultset类,如下图所示 :
至于为什么要说这个事儿呢?接着往下看你就明白了。
在该实现类的众多成员中,存放数据的成员是rowData,我们可以在ResultSetImpl类中找到这个rowData,如下图所示 :
但是,当我们使用Ctrl + b/B快捷键访问rowData源码时,会发现rowData其实不是ResultSetImpl类的成员,而是它的父类NativeResultset中的成员,如下图所示 :
可以看到,rowData本身是ResultsetRows类型(是个接口),此处使用protected访问权限修饰符,表示其可以被子类访问。
但在实际使用中,rowData的类型其实是一个实现了ResultsetRows接口的ResultsetRowsStatic类的对象。而ResultsetRowsStatic类的成员rows才是真正存放表中数据的地方,rows本身是List接口类型,如下图所示 :
但实际使用中,它是一个实现了List接口的ArrayList类对象,其中存放了表中所有行的数据(所有记录)。
可以看到,仍然是我们熟悉的elementData数组(up之前出过ArrayList类的源码分析,大家有兴趣可以去看看)。现在elementData数组中有四个元素,对应我们要查询的学生表中共四条记录。
继续,elementData数组中元素的类型实际是ByteArrayRow类型,而ByteArrayRow类中有包含一个成员internalRowData,是一个byte类型的数组,如下图所示 :
这个byte数组中又有四个元素,是对应了我们学生表中的四个字段(id,name,sex,score),此处存放的是字段的值对应的ASCII码值。
Statement也是JDBC规范的接口之一。用于执行静态SQL语句并返回其生成的结果的对象。
在建立连接后,需要对数据库进行访问,执行SQL语句,可以通过Statement, PreparedStatement(预处理), 或者CallableStatement(存储过程)三种途径。
但是,使用Statement会存在SQL注入的风险。所谓SQL注入,指的是利用某些系统没有对用户输入的数据进行充分的检查,而在用户输入数据中注入非法的SQL语句段或命令,恶意攻击数据库。
防范SQL注入可以使用PreparedStatement来取代Statement。
举一个简单的SQL注入的栗子,输入用户的用户名为:1' OR,输入用户的密码为:OR '1' = '1。因为我们在WHERE子句中确定name和password时,会使用单引号。那么当我们以上述的用户名和密码来登录时,就会造成如下效果 :
...WHERE name = '1' OR' AND password = 'OR '1' = '1';
...WHERE name = '1' OR' AND password = 'OR '1' = '1';
可以看到,由于输入的用户名和密码中恶意使用了单引号,使得原来的条件验证被改成了条件1 OR 条件2 OR 条件3的格式,并且这里的条件3 —— '1' = '1'是永真式。
up以用户表users来演示(表示可登录的用户),创建表的代码如下 :
CREATE TABLE IF NOT EXISTS `users`(
`name` VARCHAR(32) NOT NULL,
`password` VARCHAR(32) NOT NULL
) CHARACTER SET utf8mb3 COLLATE utf8mb3_bin ENGINE INNODB;
INSERT INTO users
VALUES
('Ice', '12345'),
('Bob', 'bbbbb');
SELECT * FROM users;
users表效果如下 :
测试SQL注入,如下:
SELECT * FROM users
WHERE `name` = '1' OR'
AND password = 'OR '1' = '1';
查询结果如下 :
如果登录程序以“能否查询到表中的内容”为判定管理员是否存在,那么SQL注入的方式就可以顺利侵入数据库。
接下来我们使用Java程序来演示一下SQL注入。
up以Sumulation类为演示类,代码如下:
package api.sql_injection;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.sql.*;
import java.util.Properties;
import java.util.Scanner;
public class Simulation {
public static void main(String[] args) throws ClassNotFoundException, SQLException, IOException {
//核心四部曲
Scanner scanner = new Scanner(System.in);
System.out.println("请输入要登录用户的用户名:");
String name = scanner.nextLine();
System.out.println("请输入要登录用户的密 码:");
String password_ex = scanner.nextLine();
Properties properties = new Properties();
properties.load(new FileInputStream("src/api/connection/mysql.properties"));
String driver = properties.getProperty("driver");
String url = properties.getProperty("url");
String user = properties.getProperty("user");
String password = properties.getProperty("password");
//1.注册驱动
Class.forName(driver);
//2.获取连接
Connection connection = DriverManager.getConnection(url, user, password);
//3.执行SQL
String sql = "SELECT * FROM users " +
"WHERE `name` = '" + name + "'" +
"AND password = '" + password_ex + "';";
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery(sql);
/**
* 认为 ———— 只要查询到表中的内容,就说明当前管理员是存在的,判定登录成功。
*/
if (resultSet.next()) {
System.out.println("Log on successfully!");
} else {
System.out.println("Failed to log on!");
}
//4.释放资源
resultSet.close();
statement.close();
connection.close();
scanner.close();
}
}
运行结果 :
PreparedStatement也是一个接口,并且是Statement接口的子接口,因此也可以使用Statement接口中的一些方法。
PreparedStatement执行的SQL语句中的参数用?来表示(?表示占位符),通过调用该类的setXxx方法来设置这些参数。如下图所示 :
可以看到,这些setXxx方法均有两个形参。其中,第一个形参均为int类型,代表了要设置的参数在对应SQL语句中存在的位置(从1开始);第二个形参便是具体要设置的值。
PS :
1>同Statement类似,调用executeQuery()方法来执行DQL(查),返回ResultSet对象;而调用executeUpdate()来执行DML(增,删,改),返回int类型的受影响的行数。
2>获取PreparedStatement时,直接传入要执行的SQL字符串,使两者关联;之后调用executeQuery和executeUpdate方法时,不再需要传入形参。
- ----->不再需要使用+拼接SQL语句,减少了编程时的语法错误;
- ----->有效解决了SQL注入的问题;
- ----->大大减少了编译次数,执行效率较高。
up以Prepared_Demo类作为演示类,代码如下 :
package api.sql_injection;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.sql.*;
import java.util.Properties;
import java.util.Scanner;
/**
* @author : Cyan_RA9
* @version : 21.0
*/
public class PreparedStatement_Demo {
public static void main(String[] args) throws IOException, ClassNotFoundException, SQLException {
Scanner scanner = new Scanner(System.in);
System.out.println("请输入用户名:");
String name = scanner.nextLine();
System.out.println("请输入密 码:");
String password_ex = scanner.nextLine();
Properties properties = new Properties();
properties.load(new FileInputStream("src/api/connection/mysql.properties"));
String driver = properties.getProperty("driver");
String url = properties.getProperty("url");
String user = properties.getProperty("user");
String password = properties.getProperty("password");
//JDBC核心四部曲
//1.注册驱动
Class.forName(driver);
//2.获取连接
Connection connection = DriverManager.getConnection(url, user, password);
String sql = "SELECT * FROM users " +
"WHERE `name` = ? " +
"AND password = ? ;";
PreparedStatement ps = connection.prepareStatement(sql);
ps.setString(1, name);
ps.setString(2, password_ex);
//3.执行SQL
ResultSet resultSet = ps.executeQuery();
if (resultSet.next()) {
System.out.println("Log on successfully!");
} else {
System.out.println("Failed to log on!");
}
//4.释放资源
resultSet.close();
ps.close();
connection.close();
scanner.close();
}
}
运行效果 :
我们先来测试一下输入正确的用户 :
再来测试一下SQL注入,如下图所示 :
可以看到,使用PreparedStatement代替Statement后,SQL注入被成功拦截。
对于PreparedStatement执行DML的情况,很简单,大家可以自己去试试,改用executeUpdate方法,把ResultSet去掉,用int类型的变量做接收。非常容易,这里不做演示。
- ,以上就是JDBC 第二节的全部内容了。
- 总结一下,我们在日常开发中最终要使用的JDBC连接方式,就是方式三(DriverManager)的简化版的优化版,以核心四部曲为框架,即——①直接使用Class.forName(...)的反射形式动态加载Driver类,底层自动完成注册驱动的操作;②使用DriverManager类的getConnection方法来获取连接(传入的参数从properties配置文件获得);③使用PreparedStatement来执行SQL;④释放资源。
- 下一节内容——JDBC Utils,我们不见不散。感谢阅读!
System.out.println("END------------------------------------------------------------------------------");