【MySQL】JDBC的使用(万字解析)

目录

[1. 什么是JDBC](#1. 什么是JDBC)

[2. 为什么要使用JDBC?](#2. 为什么要使用JDBC?)

[3. 如何使用JDBC](#3. 如何使用JDBC)

[3.1 创建Maven工程](#3.1 创建Maven工程)

[3.2 配置MySQL驱动包](#3.2 配置MySQL驱动包)

[3.3 Connection 建立数据库连接](#3.3 Connection 建立数据库连接)

[a. 使用DriverManager类](#a. 使用DriverManager类)

[b. 使用DataSource类](#b. 使用DataSource类)

[3.4 创建Statement对象](#3.4 创建Statement对象)

[3.5 执行SQL语句并接收结果集](#3.5 执行SQL语句并接收结果集)

[3.6 ResultSet对于结果集的后续处理](#3.6 ResultSet对于结果集的后续处理)

[3.7 释放资源](#3.7 释放资源)

[4. 完整示例演示](#4. 完整示例演示)

[4.1 使用DriverManager的示例](#4.1 使用DriverManager的示例)

[4.2 使用DataSource的示例](#4.2 使用DataSource的示例)

[5. SQL注入问题](#5. SQL注入问题)

[5.1 什么是SQL注入?](#5.1 什么是SQL注入?)

[5.2 SQL语句的预处理对象PreparedStatement](#5.2 SQL语句的预处理对象PreparedStatement)

[6. JDBC的步骤总结](#6. JDBC的步骤总结)


1. 什么是JDBC

JDBC(Java Database Connectivity)是Java 官方提供的一套用于访问关系型数据库的标准 API。它定义了 Java 程序与数据库交互的统一接口和规范,而非具体的实现。

2. 为什么要使用JDBC?

不同的数据库对于同一个操作 不论是协议还是参数都各有不同 ,如果让程序员自己去实现,那就必须针对不同的数据库进⾏编码实现,这个⼯作量和维护成本显然太大

而把具体的实现交给数据库厂商去做,且它们都遵循同一个规范(也就是JDBC),那么Java程序员只需要按照需要调⽤接口中定义的方法即可。这样不论使⽤什么数据库,都对于Java程序没有任 何影响,即便是换⼀个数据库,也只需要换⼀下相应⼚商的实现依赖。

3. 如何使用JDBC

3.1 创建Maven工程

创建一个新项目,在创建页面中选择"Maven"

项目创建完成后会有一个叫pom.xml的配置文件,我们把该文件打开

3.2 配置MySQL驱动包

我们要去Maven仓库中找到的MySQL 官方实现的 JDBC 驱动包,地址:https://mvnrepository.com/artifact/mysql/mysql-connector-java

进入Maven后,我们选择8.0.33版本的JDBC驱动包:

进入对应的驱动包后,下拉页面,会看到下面这串代码,复制这串代码。它是驱动包的依赖路径:

在pom.xml文件中,自己加上一对<dependencies></dependencies>标签。然后在标签的内部粘贴上刚刚复制的代码,如下图所示:

点击右上角的"加载"图标。如果不小心点到"X"号,可以像我这样把"加载"图标找出来再点击:

依赖添加成功后,在外部库(External Libaries)中会添加一个包:

这个com.mysql:mysql-connector-j:8.0.33包就是我们JDBC要用到的驱动包。

3.3 Connection 建立数据库连接

JDBC提供了两种不同的数据连接方式,分别是DriverManager 和 DataSource。

  • 其中,DriverManager 是 JDBC 早期提供的工具类,直接通过驱动获取数据库连接。(无连接池)
  • DataSource 是 JDBC 规范定义的接口 ,可以使用第三方框架支持的数据库连接池来管理连接的复用、分配。

a. 使用DriverManager类

首先要明确的是,MySQL驱动包中有两个Driver驱动类:

  • 一个驱动类是 com.mysql.jdbc.Driver,它可以连接5.X系列版本的MySQL数据库。
  • 另一个驱动类是 com.mysql.cj.jdbc.Driver,它可以连接 8.X系列版本的MySQL数据库。

使用时要注意区分,一定要用对应的驱动类来连接对应版本的数据库!!!

JDBC 的本质是 "统一的数据库访问标准",而 DriverManager 作为执行官,它要完成驱动类的统一入口的职责。

每个**(外部库)驱动类** 都要注册到JVM中,然后由 jdk(标准库)中的 DriverManager 类 进行管理、调用

第一步:加载(注册)数据库厂商提供的驱动

所使用的代码如下:

java 复制代码
//1.加载数据库厂商提供的驱动
Class.forName("com.mysql.cj.jdbc.Driver");  //mysql实现的Driver类

这段代码做了什么?

  1. 使用forName()方法 加载Driver类的元数据,同时 (如果Driver类还没被加载过) 它会触发一次Driver类的静态代码块

  2. Driver类的静态代码块中,会使用DriverManager类的registerDriver()方法:

  1. registerDriver()方法的核心作用是:把当前驱动类的实例添加到 ++DriverManager 内部维护的 "已注册驱动列表"++ 中(它是一个线程安全的集合),让 DriverManager 能管理并调用这个驱动。

  2. 由于forName()方法只会触发一次类加载(Java语法特性) ,所以同一个注册类只会执行一次registerDriver()方法,自然不会重复注册


第二步:建立数据库连接

所使用的代码如下:

java 复制代码
​Connection connection = DriverManager.getConnection(
    "jdbc:mysql://127.0.0.1:3306/test1?characterEncoding=utf8" +
    "&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&useSSL=false"
    ,"root"
    ,"123456"
);

这段代码做了什么?

简单来说:通过DriverManager.getConnection()方法,建立Java 程序与数据库之间的物理连接 ,并得到代表该连接的Connection对象。
getConnection()方法中的3个参数:

  • String url: URL(Uniform Resource Locator,统一资源定位符) 是互联网上标准化的地址,用于定位和访问网络资源。让我们拆解一下url,看看其中含有哪些信息:
    • " jdbc:mysql:// "------> JDBC 连接 MySQL 的协议标识,连接不同的数据库要使用不同的固定标识
    • " 127.0.0.1:3306/test1? "------>数据库所在的主机 IP (127.0.0.1 是本地)+ MySQL 的默认端口 (3306)。test1是要连接的具体数据库名
    • ?号后面的是连接配置参数:
      • characterEncoding=utf8:设置数据库交互的字符编码为 UTF-8,避免中文乱码;
      • serverTimezone=Asia/Shanghai:设置时区为上海时区(MySQL 8.x 必填,否则会报时区错误);
      • allowPublicKeyRetrieval=true:允许获取 MySQL 的公钥(解决 8.x 的权限校验问题);
      • useSSL=false:开发环境关闭 SSL 加密(生产环境建议开启)。
  • String user: 数据库系统的登录用户名,这里使用的是root超级用户。
  • String passward: 数据库系统的登录密码,这里是"123456"。

总的来说,DriverManager 会根据 url 来识别我们要连接的是哪一个数据库系统(此处是 MySQL),然后从已注册的驱动列表中找到适配该数据库的驱动类实例 (此处是com.mysql.cj.jdbc.Driver),调用其connect()方法 完成与数据库的物理连接,最终返回代表该连接的Connection对象

b. 使用DataSource类

第一步:配置连接参数

所使用的代码如下:

java 复制代码
//1.1 定义mysql数据源对象
MysqlDataSource mysqlDataSource = new MysqlDataSource();

//1.2 设置数据库的连接串、用户名、密码
mysqlDataSource.setUrl("jdbc:mysql://127.0.0.1:3306/test1?characterEncoding=utf8" +
                "&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&useSSL=false");
mysqlDataSource.setUser("root");
mysqlDataSource.setPassword("123456");

//1.3 定义JDBC数据源对象(接口编程)
DataSource dataSource = mysqlDataSource;

这段代码做了什么?

  1. MysqlDataSource类是mysql实现的数据库源对象 ,该类是DataSource的实现类。我们要使用setUrl()、setUser()和setPassword() 这三个方法来配置(封装)数据库源,方便后续通过getConnection()方法直接连接数据库。
  • 【主流是使用第三方框架实现的 datasource(如 HikariCP 框架下的 HikariDataSource 类),它们会带有连接池机制。
  • 【这里的 MysqlDataSource 类并不支持连接池,一般来说连接池不会由第二方(数据库厂商)实现,而是由第三方框架提供。
  1. DataSource是 JDBC 规范(javax.sql包)定义的标准接口,其定位是数据库连接的统一获取入口

第二步:建立数据库连接

所使用的代码如下:

java 复制代码
//2.获取数据库连接 
connection = dataSource.getConnection();

这段代码做了什么?

getConnection()方法会根据配置的连接参数直接对数据库进行连接,最后返回连接对象Connection。

  • 【如果此处的dataSource的实例是由第三方框架实现的,那么它的getConnection()方法会从连接池中获取Connection对象;如果连接池中没有,那么会创建一个Connection对象存入连接池 并 把该对象的引用作为方法的返回。

补充:

  1. jdk17的JDBC是4.0之后的版本了, 而JDBC 4.0 引入了SPI(服务提供者接口)自动加载机制 ,已经无需forName()方法就能加载驱动类了。也就是说前面使用DriverManager 的方式连接数据库时,不需要显式使用forName()方法。
  2. 在 DataSource 连接数据库方式中第2步所使用的Connection()方法,它还是会使用到com.mysql.cj.jdbc.Driver 对象**(已被自动加载)**中connect()方法进行数据库连接。如下图所示:

3.4 创建Statement对象

所使用的代码如下:

java 复制代码
​Statement statement = connection.createStatement();

Statement对象的作用:

Statement 是 JDBC中用于向数据库发送 SQL 语句并获取执行结果 的核心接口,依赖已建立的Connection对象创建。

Statement中常用于发生SQL语句的方法有executeQuery()executeUpdate(),它们对应着不同类型的SQL语句。

executeQuery() 方法:

  • 查询语句(SELECT) :SELECT 语句通过executeQuery()方法执行,返回ResultSet对象。(封装查询结果集,供程序读取数据)

executeUpdate() 方法:

  • 数据定义语言(DDL) :如CREATE(建表)、ALTER(修改表)、DROP(删表)等。通过executeUpdate()方法执行,返回值通常为0(DDL 无 "影响行数");
  • 数据操作语言(DML) :如INSERT(插入)、UPDATE(更新)、DELETE(删除)等。通过executeUpdate()方法执行,返回值为 SQL 影响的行数;

3.5 执行SQL语句并接收结果集

如果使用的是查询语句,那么查询的结果需要ResultSet接收。所使用的代码如下:

java 复制代码
//4.定义sql语句     (注意不要用*, 因为修改表结构的时候可能会 改变列的顺序)
String sql = "SELECT id, name, sno, age, gender, enroll_date, class_id FROM student";

//5.执行sql并获取结果
ResultSet resultSet = statement.executeQuery(sql);

如果使用的DDL或DML语句,则不需要用到ResultSet。

建议查询列表不要使用通配符 * ,因为当表结构被修改时,查询出来的字段数量和顺序可能会不一样。而在Java业务层中对结果集的处理一般是不变的,无法应对查询字段的变化。

3.6 ResultSet对于结果集的后续处理

  • 默认滚动模式下,ResultSet对象只能用next()方法读取下一行数据只能向下滚动,不能向上滚动。
  • 到达下一行后,next()方法还会判断该行是否不为空: 如果不为空返回true,否则返回false。【该行为模式类似Scanner类中的++hasNext()方法 + nextLine()方法++的组合】

ResultSet通过以下方法读取当前行中的具体字段:

方法 作用 对应数据库类型
getInt(String/int) 读取整数类型字段 INT、TINYINT(无符号)等
getString(...) 读取字符串类型字段 VARCHAR、CHAR、TEXT 等
getLong(...) 读取长整数类型字段 BIGINT
getDouble(...) 读取浮点类型字段 DOUBLE、FLOAT
getBoolean(...) 读取布尔类型字段 BOOLEAN、TINYINT(1)
getDate(...) 读取日期字段(仅日期) DATE
getTime(...) 读取时间字段(仅时间) TIME
getTimestamp(...) 读取时间戳字段(日期 + 时间) DATETIME、TIMESTAMP
getObject(...) 通用读取(返回 Object 类型) 任意类型(需手动强转)

这些方法的参数有两种:

  • String类型参数:根据字段名称 读取指定的字段。如果查询时使用了别名,那么此处的参数必须也输入其别名。
  • int类型参数:根据字段序号 读取指定的字段。注意:字段下标从 1 开始,不是从 0 开始。

-- 示例代码:遍历并打印结果集:

java 复制代码
//每进一次循环读取下一行记录
while(resultSet.next()){   
   //根据列的数据类型 调用不同的获取方法
   long id = resultSet.getLong(1);     //第一列的下标是1
   String name = resultSet.getString(2);
   String sno = resultSet.getString(3);
   int age = resultSet.getInt(4);
   boolean gender = resultSet.getBoolean(5);
   Date enroll_date = resultSet.getDate(6);
   long class_id = resultSet.getLong(7);

   // 打印结果(占位符的索引从0开始)
   System.out.println(MessageFormat.format("学生编号={0},姓名={1},学号={2},年龄={3}," +
          "性别={4},入学时间={5},班级编号={6}", id, name, sno, age, gender, enroll_date
           , class_id));
}

MessageFormat的{}是索引式占位符:

  1. 占位符格式{数字} ,其中的数字是 "参数的索引"(从 0 开始)。比如:{0}对应参数列表的第 1 个字段,{1}对应第 2 个字段,以此类推。
  2. 作用 :按索引将后续传入的参数,替换到字符串中对应的{数字}位置,实现动态文本拼接;

String类的format()方法也支持占位符,但它的占位符是**%+格式说明符** (如**%s代表字符串** 、%d代表整数%f代表浮点数),而非{}。语法和MessageFormat完全不同。


额外补充

创建Statement对象的createStatement()方法最多有3个参数:

光标放在createStatement()方法,按crtl + p可以查看该方法的所有重写版本下所需要的参数:

可以看到,createStatement()方法有3个版本:无参、带2个参数、带3个参数。

参数 1:resultSetType(结果集类型)

控制 ResultSet 的滚动能力与对数据库数据变化的敏感度,可选值:

  • ResultSet.TYPE_FORWARD_ONLY:仅向前滚动,只能通过 next() 向下移动。[无参方法的默认值]
  • ResultSet.TYPE_SCROLL_INSENSITIVE可自由滚动,且结果集是 "快照"(数据库数据变化后,结果集不会同步更新)。
  • ResultSet.TYPE_SCROLL_SENSITIVE可自由滚动 ,且结果集会随数据库数据的变化动态更新(需数据库驱动支持)。
    • TYPE_SCROLL_SENSITIVE的结果集,本质是数据库端的敏感游标在 Java 中的映射。
    • 【ResultSet确实没有主动发送 SQL的能力,但它依赖已存在的数据库连接 + 数据库端的敏感游标,实现了实时读取最新数据的效果。

参数 2:resultSetConcurrency(结果集并发模式)

控制 ResultSet 是否能直接修改数据库数据,可选值:

  • ResultSet.CONCUR_READ_ONLY:结果集只读,仅能读取数据 ,无法修改。[无参方法的默认值]
  • ResultSet.CONCUR_UPDATABLE:结果集可更新,可通过 ResultSetupdateXxx() 方法修改数据并同步到数据库

参数 3:resultSetHoldability(结果集保持性)

控制事务提交后,ResultSet 是否保持打开状态,可选值:

  • ResultSet.HOLD_CURSORS_OVER_COMMIT:事务提交后,ResultSet 仍保持打开,可继续操作。[带两个参数的默认取值]
  • ResultSet.CLOSE_CURSORS_AT_COMMIT:事务提交后,ResultSet 会自动关闭,无法再操作(快照也无法读取)

想要自由滚动,只需保证第一个参数是 TYPE_SCROLL_INSENSITIVE 或 TYPE_SCROLL_SENSITIVE。

创造Statement对象时,createStatement()方法至少要使用带2个参数的版本。如下:

java 复制代码
Statement stmt = conn.createStatement(
    ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE
);

在可自由滚动模式下,除了next()方法外,用于滚动的方法还有:

方法 作用
previous() 游标回退到上一行 :返回true表示回退成功,false表示已在第一行之前
first() 游标直接跳到第一行 :返回true表示存在第一行,否则返回false
last() 游标直接跳到最后一行 :返回true表示存在最后一行,否则返回false
absolute(int row) 游标定位到指定行号返回true表示行存在 - 正数:从结果集开头数第row行。 - 负数:从结果集结尾数第(-row)行。
relative(int rows) 游标相对当前位置移动指定行数返回true表示移动后行存在 - 正数:向下移动rows行。 - 负数:向上移动(-row)行。
beforeFirst() 游标回到结果集开头(第一行之前),常用于重新遍历
afterLast() 游标跳到结果集结尾(最后一行之后)
getRow() 获取当前游标所在的行号(从 1 开始;若游标在开头 / 结尾,返回 0)

3.7 释放资源

Connection(数据库连接)、Statement(SQL 执行载体)、ResultSet(查询结果集)都属于外部资源 。它们依赖数据库服务,JVM 垃圾回收无法自动释放,若不主动释放会导致数据库连接泄漏、资源耗尽等问题。

释放资源最好遵循**"最晚获取的资源最早释放"**原则,避免一个资源关闭失败导致后续资源未释放。

  • 资源的释放肯定是在最后的,所以它的处理一般放在try-catch-finnaly语句中的finnaly代码块。
  • 使用close()方法即可释放资源。
  • close()方法可能导致SQLException异常,需要使用try-catch语句捕获。

代码如下:

java 复制代码
    try{
        //获取Connection资源
        //获取Statement资源
        //获取ResultSet资源    

    }catch (获取资源时产生的异常){
        //异常的处理....

    }finally{    //按顺序释放资源
        //Result的释放
        if(resultSet != null){
            try {
                resultSet.close();
            } catch (SQLException e) {
                //异常的处理...
            }
        }
        //Statement的释放
        if(statement != null){
            try {
                statement.close();
            } catch (SQLException e) {
                //异常的处理...
            }
        }
        //Connection的释放
        if(connection != null){
            try {
                connection.close();
            } catch (SQLException e) {
                //异常的处理...
            }
        }
    }

4. 完整示例演示

目前student表中有这些数据:

4.1 使用DriverManager的示例

-- 示例:获取数据库中每个学生的id, name, sno, age, gender, enroll_date, class_id,然后打印每个学生的信息

sql 复制代码
public static void main(String[] args) {

    Connection connection = null;
    Statement statement = null;
    ResultSet resultSet = null;

    try {
        //1.加载数据库厂商提供的驱动
        Class.forName("com.mysql.cj.jdbc.Driver"); 

        //2.使用数据库连接对象Connection, 建立数据库连接
        connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/test1?characterEncoding=utf8" +
                        "&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&useSSL=false",
                "root", "123456");

        //3.创建Statement对象,用于发送SQL语句给数据库
        statement = connection.createStatement();

        //4.定义sql语句     (注意不要用*, 因为修改表结构的时候可能会 改变列的顺序)
        String sql = "SELECT id, name, sno, age, gender, enroll_date, class_id FROM student;";

        //5.执行sql并获取结果
        resultSet = statement.executeQuery(sql);

        //6.遍历结果集
        while(resultSet.next()){
            //每进一次循环读取下一行记录
            long id = resultSet.getLong(1);     //第一列的下标是1
            String name = resultSet.getString(2);
            String sno = resultSet.getString(3);
            int age = resultSet.getInt(4);
            boolean gender = resultSet.getBoolean(5);
            Date enroll_date = resultSet.getDate(6);
            long class_id = resultSet.getLong(7);
            // 打印结果(占位符的索引从0开始)
            System.out.println(MessageFormat.format("学生编号={0},姓名={1},学号={2},年龄={3}," +
                    "性别={4},入学时间={5},班级编号={6}", id, name, sno, age, gender, enroll_date, class_id));
        }
    } catch (ClassNotFoundException e) {
        throw new RuntimeException(e);
    } catch (SQLException e) {
        throw new RuntimeException(e);
    }finally {

        //7.释放资源(从后往前释放)
        if(resultSet != null){
            try {
                resultSet.close();
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }
        if(statement != null){
            try {
                statement.close();
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }
        if(connection != null){
            try {
                connection.close();
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }
    }
}   

执行结果:

4.2 使用DataSource的示例

-- 示例:根据学生姓名查询数据库,然后打印学生信息

sql 复制代码
public static void main(String[] args) {
    //1.1 定义mysql数据源对象
    MysqlDataSource mysqlDataSource = new MysqlDataSource();
    //1.2 设置数据库的连接串、用户名、密码
    mysqlDataSource.setUrl("jdbc:mysql://127.0.0.1:3306/test1?characterEncoding=utf8" +
            "&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&useSSL=false");
    mysqlDataSource.setUser("root");
    mysqlDataSource.setPassword("123456");
    //1.3 定义JDBC数据源对象(接口编程)
    DataSource dataSource = mysqlDataSource;

    //对象声明
    Connection connection = null;
    PreparedStatement statement = null;     //预处理SQL注入
    ResultSet resultSet = null;

    try {
        //2.获取数据库连接
        connection = dataSource.getConnection();

        //3.1 定义SQL语句
        String sql = "SELECT id, name, sno, age, gender, enroll_date, class_id " +
                "FROM student WHERE name = ?";  //使用?占位符
        //3.2 接收用户请求
        System.out.println("输入要查询的姓名:");
        Scanner scanner = new Scanner(System.in);
        String inputName = scanner.next();

        //4. 用真实的姓名替换占位符【预处理阶段,自动会处理SQL注入问题】
        statement = connection.prepareStatement(sql);
        statement.setString(1, inputName);  //?占位符的下标从1开始

        //5.执行SQL语句并返回结果
        resultSet = statement.executeQuery();

        //6.遍历结果集
        while(resultSet.next()){
            //每进一次循环读取下一行记录
            //根据列的数据类型 调用不同的获取方法
            long id = resultSet.getLong("id");     //使用下标表示的话,查询列表下标从1开始
            String name = resultSet.getString("name");   //使用名称表示的话,如果查询时使用了别名,那么此处的输入也必须列的别名
            String sno = resultSet.getString("sno");
            int age = resultSet.getInt("age");
            boolean gender = resultSet.getBoolean("gender");
            Date enroll_date = resultSet.getDate("enroll_date");
            long class_id = resultSet.getLong("class_id");
            // 打印结果(占位符的索引从0开始)
            System.out.println(MessageFormat.format("学生编号={0},姓名={1},学号={2},年龄={3}," +
                    "性别={4},入学时间={5},班级编号={6}", id, name, sno, age, gender, enroll_date, class_id));
        }
    } catch (SQLException e) {
        throw new RuntimeException(e);
    } finally {
        //7.释放资源(从后往前释放)
        if(resultSet != null){
            try {
                resultSet.close();
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }
        if(statement != null){
            try {
                statement.close();
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }
        if(connection != null){
            try {
                connection.close();
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

执行结果:

5. SQL注入问题

5.1 什么是SQL注入?

根据学生姓名进行查询,可以写出一下代码:

这里只是单纯的字符串拼接,只有拼接后的字符串符合SQL语法,那么它就能执行。

例如输入:'/**/or/**/class_id=1;#

为什么能查出一个合法的结果集呢?我们把输入的数据与字符串拼接起来看看:

/**/ 和 # 在SQL语法中都表示注释,在这里你可以把它看作一个空格,执行上面拼接后的语句就相当于执行了下面的这条语句:

sql 复制代码
SELECT id, name, sno, age, gender, enroll_date, class_id 
FROM student WHERE name = '' or class_id=1;

可以看到,明明我们把程序设计成"根据学生姓名查询数据库",结果别人通过特殊的输入可以"随意查询数据库"。假如表中还存储着像"密码"这样的敏感数据,那么不法分子就可以通过这样的方法获取敏感数据,这就是SQL注入产生的问题!!

SQL 注入是一种恶意攻击手段:用户通过构造特殊的输入内容,篡改原有 SQL 语句的逻辑,让数据库执行非预期的操作(如绕过登录、删除数据等)。


5.2 SQL语句的预处理对象PreparedStatement

为了解决SQL注入问题,设计出了PreparedStatement类,它被用于SQL语句的预处理。

底层逻辑:

  1. 预编译 SQL 模板 :先把带**占位符 ?**的 SQL 模板(比如 SELECT * FROM user WHERE username = ? AND password = ?)发送给数据库,数据库先编译这个模板(解析语法、生成执行计划),并缓存起来。
  2. 传入参数 :再把用户输入的参数(比如用户名、密码)单独传给数据库。数据库会将参数作为 "纯数据" 处理,自动转义特殊字符(如 'OR 等),不会将其解析为 SQL 语法的一部分。
  3. 执行 SQL :复用预编译的执行计划,代入处理过的参数并执行,避免了参数参与 SQL 语法解析的过程。

示例代码:

java 复制代码
//1.完成Datasource的配置... 或 完成DriverManager的注册...

//2.完成Connection对象的创建...

//3.定义SQL语句,使用?占位符
String sql = "insert into student (name, sno, age, gender, enroll_date, class_id) 
values (?,?,?,?,?,?)";

//4.定义SQL预处理对象
PreparedStatement statement = connection.prepareStatement(sql);

//5.用户输入数据...

//6.填充数据并处理参数
statement.setString(1, inputName);
statement.setString(2, inputSno);
statement.setInt(3, inputAge);
statement.setByte(4, inputGender);
statement.setString(5, inputEnrollDate);
statement.setLong(6, inputClassId);

//7.执行SQL
int row = statement.executeUpdate();
//影响行数的判断
        if (row == 1) {
    System.out.println("插入成功");
} else {
    System.out.println("插入失败");
}

这三个方法分别做了什么?

  1. prepareStatement(sql) 方法
  • 核心作用:
    • ① 创建**PreparedStatement对象**;
    • ② 将传入的带占位符(?)的 SQL 模板 发送给数据库,由数据库对该 SQL 进行预编译(解析 SQL 语法、生成执行计划并缓存)。
  • 本质:让数据库先 "记住" SQL 的 "框架",后续只需填充参数,不用重复解析 SQL 语法。

  1. setXxx(索引, 参数值) 方法
  • 核心作用:给 SQL 模板中的?占位符按索引填充参数Xxx对应参数的数据类型(比如setString对应字符串、setInt对应整数)。
  • 注意事项:占位符的索引是从 1 开始的
  • 本质:setXxx ()方法会触发 安全处理,不过Java 端本身并不直接做 "转义" 操作,真正的转义 / 安全处理 是在数据库层面完成的。

  1. executeUpdate() 方法
  • 核心作用:执行当前PreparedStatement对应的 SQL 语句【复用预编译的 SQL】,返回值是SQL 执行后受影响的行数 (比如插入 1 条数据成功则返回1)。
  • 延伸:PreparedStatement 对象使用的executeQuery()方法,由于已经预编译了SQL模板,所以该方法也不需要输入参数sql。

总结:PreparedStatement与Statement的区别

维度 Statement PreparedStatement
使用流程 1. 拼接完整SQL 字符串(含参数) 2. connection.createStatement()方法 创建 Statement 对象 3. 执行时传入拼接好的 SQL (如statement.executeQuery(sql)) 1. 写?的SQL模板 2. connection.prepareStatement(sql) 方法创建 prepareStatement对象,绑定并预编译 SQL 3.setXxx()填参数 4. 执行时无需再传 SQL (如prepareStatement.executeQuery())
SQL 构造 字符串拼接 预编译模板 + 参数绑定
性能 每次执行都编译 预编译一次,多次执行
安全性 易受 SQL 注入攻击 防止 SQL 注入
可读性 差(字符串拼接混乱) 好(SQL 结构清晰)
数据类型 自动类型转换 强类型检查

注意:

  • PreparedStatement对象在最后也需要调用close()方法释放资源
  • PreparedStatement对象虽然可以向上转型成Statement类型,但这样的话就无法使用PreparedStatement类中的特定方法了(如setInt()、setString())!!!

所以此处不建议使用向上转型语法。

6. JDBC的步骤总结

  • 步骤一:连接到数据库服务
  • 步骤二:发送SQL语句
  • 步骤三:得到返回结果并显示
  • 步骤四:关闭连接

JDBC中在本博客涉及到的"事物",它们的下标起始位置分别是:

  • 用于预编译的String sql 与 PreparedStatement的setXxx()方法:占位符是问号 ?,下标从 1 开始
  • ResultSet的getXxx()读取字段:列的下标从 1 开始
  • MessageFormat.format()方法打印数据:占位符是**{数字},下标从 0 开始**。

本期分享完毕,感谢大家的支持Thanks♪(・ω・)ノ

相关推荐
爱笑的眼睛112 小时前
超越翻转与裁剪:面向生产级AI的数据增强深度实践与多模态演进
java·人工智能·python·ai
长孙阮柯2 小时前
Java进阶篇(五)
java·开发语言
小张快跑。2 小时前
Maven指定版本下载以及相关配置
java·maven
zhishidi2 小时前
Spring @Scheduled注解调度机制详解
java·python·spring
⑩-2 小时前
Blocked与Wati的区别
java·开发语言
AAA简单玩转程序设计2 小时前
救命!Java这3个小技巧,写起来爽到飞起✨
java
IManiy2 小时前
Java表达式引擎技术选型分析(SpEL、QLExpress)
java·开发语言
历程里程碑2 小时前
C++ 17异常处理:高效捕获与精准修复
java·c语言·开发语言·jvm·c++
雨雨雨雨雨别下啦2 小时前
ssm复习总结
java·开发语言