JDBC PreparedStatement的作用机制

26 阅读3分钟

为什么说jdbc的PreparedStatement可以完全避免SQL注入的问题,还能高效利用数据库本身对查询的缓存?它的机制是什么,是数据库层面做的吗?


1 为什么能完全防范 SQL 注入?

JDBC Statement 中,SQL 语句是通过字符串拼接完成的。数据库拿到的是一串分不清哪里是“命令”、哪里是“数据”的字符。

  • 普通 Statement 的漏洞
    如果你拼接 SELECT * FROM users WHERE username = ' + admin' OR '1'='1 + ',数据库看到的指令是: “选出所有用户,只要 1 等于 1 就行” 。数据篡改了逻辑。

  • PreparedStatement :
    它采用了参数化查询(Parameterized Query)

    1. 预编译阶段:驱动程序先向数据库发送一个带占位符的模版:SELECT * FROM users WHERE username = ?
    2. 数据绑定阶段:再把具体的参数(如 admin' OR '1'='1)传过去。

关键点:由于数据库已经提前完成了 SQL 的词法和语法分析,它已经认定这个 ? 位置只能是纯文本数据。哪怕你传入的数据里包含 DROP TABLE 或者 OR 1=1,数据库也只会把它当成一个“名字特别奇怪的用户”,而不会将其当作指令执行。


2 为什么能高效利用缓存?

这就涉及到数据库执行 SQL 的过程:解析(Parse) -> 优化(Optimize) -> 执行(Execute)

  • 硬解析(Hard Parse) :如果每次 SQL 字符串都不同(比如 ID 一直变),数据库就得针对每一条 SQL 重新计算执行计划(怎么查最快、用哪个索引),这非常耗 CPU。
  • 软解析(Soft Parse)PreparedStatement 允许数据库缓存执行计划

因为你发送的 SQL 模版始终是 SELECT * FROM items WHERE id = ?,数据库会发现:“诶,这个模版我见过,执行计划已经算好了,直接拿来用吧!”。

注意:这种缓存主要发生在数据库的 SGA(系统全局区)/ Library Cache 中,而不是在 Java 程序里。


3 它的机制是什么?是数据库层面做的吗?

这是一个端到端(客户端 + 服务端) 的配合过程,其核心协议通常被称为 二进制协议(Binary Protocol)

3.1 宏观流程:

  1. 客户端(JDBC Driver) :将带有 ? 的 SQL 发给数据库,并请求一个 Statement ID

  2. 服务端(Database)

    • 校验 SQL 语法。
    • 生成执行计划。
    • 将该计划与 Statement ID 绑定并存入缓存。
    • 返回 ID 给客户端。
  3. 客户端:调用 setInt()setString(),将纯数据(不含 SQL 关键字)和 Statement ID 打包发送。

  4. 服务端:根据 ID 找到缓存好的计划,直接把数据塞进去运行。

3.2 是不是所有数据库都支持?

  • 数据库支持:大多数现代关系型数据库(MySQL, PostgreSQL, Oracle, SQL Server)都在内核层面支持预编译。

  • 驱动模拟:如果某些老旧数据库不支持,JDBC 驱动会在本地进行“模拟(Client-side emulation)”,即在本地安全地转义字符串再发过去。

    • 冷知识:在 MySQL 中,默认情况下 JDBC 是在本地模拟预编译的。如果你想让它在数据库层面真正“硬核”执行,需要在连接字符串里加上 useServerPrepStmts=true

4 总结

PreparedStatement 的本质是把 SQL 变成了“带参数的函数”

  • 安全性:数据永远不会被误认为指令(类型安全)。
  • 性能:一次编译,多次运行(避免重复计算执行计划)。

稍微多说一句: 虽然它能防住 99% 的注入,但如果你在 SQL 模版里依然用字符串拼接表名(比如 SELECT * FROM ? 是不支持的,表名必须拼接),那还是会面临注入风险。这时候就需要你手动做严格的白名单校验了。