目录
一、前言
- 第二节内容,up主要和大家分享一下JDBC——API方面的内容。
- 注意事项——①代码中的注释也很重要;②不要眼高手低;③点击文章的侧边栏目录或者文章开头的目录可以进行跳转。
- 良工不示人以朴,所有文章都会适时补充完善。大家如果有问题都可以在评论区进行交流或者私信up。感谢阅读!
二、JDBC API概述
JDBC API是一系列的接口,它统一和规范了应用程序与数据库的连接,执行SQL语句并得到返回结果等各类操作,相关类和接口在java.sql和javax.sql包下。
相关体系图如下(建议阅读完毕后返回来细品☕) :
编辑
三、获取连接的三种方式
0.朝花夕拾 :
上一小节内容中,我们提到了编写JDBC程序的核心四部曲,这里再来回顾一下——
- 1° 注册驱动
- 2° 获取连接
- 3° 执行SQL
- 4° 释放资源
这里我们要重点再说一下第二个步骤——即获取数据库的连接。
1.方式一 —— 通过new关键字 :
这也是我们在第一小节中,演示第一个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("--------------------------------------------------"); } }
运行结果 :
编辑
2.方式二 —— 通过反射机制 :
提到了灵活性和依赖性,我们就不由得想到了反射机制。反射机制可以动态的加载和构建对象,属于动态加载,相比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("--------------------------------------------------"); } }
运行结果 :
编辑
3.方式三 —— 通过DriverManager
在反射机制的基础上,使用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
1.简介 :
ResultSet表示数据结果集的数据表,通常通过DQL(Data Query Language)来生成。ResultSet对象保持一个光标,该光标指向其当前的数据行。最初,光标位于第一行之前,next方法会使光标移动到下一行,并且当ResultSet对象中没有更多行时返回false,因此可以使用While循环来遍历结果集。
默认的ResultSet对象不可更新,并且只有一个向前移动的光标。因此,默认只能从第一行到最后一行迭代一次。但是,可以手动生成可滚动/可更新的ResultSet对象。
PS_1 : 若有需求让光标向上移动一行,可以使用previous()方法;如果再往上没有行可以返回时,返回false。
PS_2 : 使用getXxx()方法返回获得的记录(一行数据)中指定的字段,需要传入要获取的字段的索引(从1开始);或者也可以直接传入字段名。
PS_3 : 若有需求以对象的形式来接收返回的字段,可以使用getObject(...)方法,传入的实参与getXxx方法一致。
2.代码演示 :
根据对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(); } }
运行结果 :
编辑
3.底层实现 :
接下来,我们通过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码值。
五、SQL注入
1.什么是SQL注入?
Statement也是JDBC规范的接口之一。用于执行静态SQL语句并返回其生成的结果的对象。
在建立连接后,需要对数据库进行访问,执行SQL语句,可以通过Statement, PreparedStatement(预处理), 或者CallableStatement(存储过程)三种途径。
但是,使用Statement会存在SQL注入的风险。所谓SQL注入,指的是利用某些系统没有对用户输入的数据进行充分的检查,而在用户输入数据中注入非法的SQL语句段或命令,恶意攻击数据库。
防范SQL注入可以使用PreparedStatement来取代Statement。
2.SQL注入演示 :
举一个简单的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(); } }
运行结果 :
编辑
3.PreparedStatement :
①简介
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------------------------------------------------------------------------------");