JDBC API 万字详解(通俗易懂)

举报
Cyan_RA9 发表于 2023/07/16 12:07:29 2023/07/16
【摘要】 JDBC 第二节 详解API 通俗易懂!

目录

一、前言

二、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概述

        JDBC API是一系列的接口,它统一和规范了应用程序与数据库的连接,执行SQL语句并得到返回结果等各类操作,相关类和接口在java.sqljavax.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类的两个方法,如下——

  1. static void registerDriver(Driver driver) : 根据传入的Driver类对象,注册Driver驱动。
  2. 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------------------------------------------------------------------------------"); 

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。