JDBC入门
JDBC的概述
JDBC的概述
JDBC(Java Database Connectivity)是 Java 语言操作关系型数据库的标准 API,它为 Java 程序提供了一套统一的接口来访问各种关系型数据库,屏蔽了不同数据库底层的实现差异,使开发者无需针对不同数据库编写不同的代码。
JDBC 是一套标准化的 Java 数据库接口规范
提供统一的 Java 类和接口,实现了对不同数据库的通用编程方式
通过数据库厂商提供的驱动程序(Driver)实现具体适配,屏蔽底层数据库差异,开发者无需关心底层数据库的差异
支持核心数据库操作功能包括执行 SQL 语句、处理结果集、管理事务等核心数据库操作。
数据库驱动是数据库厂商提供的实现类,作为数据传输的桥梁,用于建立应用程序与数据库之间的连接
要使用这些驱动类,需要导入对应数据库厂商提供的驱动jar包,常见的数据库驱动接口由各数据库厂商实现,例如MySQL的驱动实现类为com.mysql.cj.jdbc.Driver。
JDBC 的工作流程
加载并注册数据库驱动
目标:确保 JVM 能够识别并加载指定数据库的驱动程序(Driver),以便后续通过驱动建立与数据库的连接。
原理:数据库厂商提供的驱动类实现了 JDBC 标准接口,程序需将该类加载到内存并自动注册到 DriverManager(驱动管理器)中
两种实现方式:
- 显式加载驱动
java.sql.DriverManager 类提供了静态方法 registerDriver(Driver driver),用于手动注册驱动,需要传入一个具体的驱动实现类实例
创建MySQL驱动实例,显式注册驱动到DriverManager
import java.sql.Driver;
import java.sql.DriverManager;
import com.mysql.cj.jdbc.Driver; // MySQL 8.x驱动类
public class DriverLoad1 {
public static void main(String[] args) throws Exception {
// 创建MySQL驱动实例
Driver driver = new Driver();
// 显式注册驱动到DriverManager
DriverManager.registerDriver(driver);
// 后续可通过DriverManager获取连接
// Connection conn = DriverManager.getConnection(url, user, password);
}
}
- 通过反射加载驱动类(触发自动注册)
MySQL 驱动类的内部包含静态代码块,当类被加载时,静态代码块会自动调用 DriverManager.registerDriver() 完成注册,因此只需通过 Class.forName() 反射加载驱动类,即可间接完成注册。
import java.sql.DriverManager;
public class DriverLoad2 {
public static void main(String[] args) throws Exception {
// 反射加载MySQL驱动类,触发静态代码块自动注册
Class.forName("com.mysql.cj.jdbc.Driver");
// 后续可通过DriverManager获取连接
// Connection conn = DriverManager.getConnection(url, user, password);
}
}
建立数据库连接
使用驱动管理器 DriverManager 的 getConnection 方法获取连接:DriverManager.getConnection(url, username, password),该方法返回的 Connection 对象代表 Java 与数据库之间的会话
url参数格式说明:jdbc:mysql://localhost:3306/day07
jdbc 代表的主协议,mysql 代表子协议,localhost 是ip地址,3306 是默认的端口号,day07 是数据库名称
当访问本地数据库时,可省略主机和端口信息,简写为:jdbc:mysql:///day07
创建 SQL 执行载体
Connection 接口作为 Java 与数据库交互的桥梁,代表 Java 程序与数据库之间建立的会话连接,该连接使用完毕后必须及时关闭以释放资源。
Connection 接口主要提供两大功能:
- 创建用于执行 SQL 语句的载体
Statement createStatement():创建 Statement 接口实现对象
PreparedStatement prepareStatement(String sql):创建 Statement 接口的子接口 PreparedStatement 的实现对象,有效防止 SQL 注入漏洞 - 事务管理
void setAutoCommit(boolean autoCommit):设置事务的自动提交模式
void commit():提交当前事务
void rollback():回滚当前事务
执行 SQL 语句
Statement 接口用于向数据库发送静态 SQL 语句并获取执行结果
executeQuery(String sql):执行查询语句(如SELECT),返回ResultSet对象(包含查询结果);
executeUpdate(String sql):执行更新语句(如INSERT、UPDATE、DELETE),返回受影响的行数(int);
execute(String sql):执行任意 SQL 语句(可执行查询或更新),返回boolean值(true表示有结果集,false表示无结果集);
Statement 的子类 PreparedStatement,预编译 SQL 语句,支持动态参数(通过?占位符),解决了静态 SQL 的局限性,可防止 SQL 注入,执行效率更高(适合重复执行的 SQL)。
处理结果
ResultSet 接口用于封装 SQL 查询结果集,以表格形式存储查询数据,通过遍历该接口可获取查询结果
- 封装数据
内部维护游标,初始位置在第一行数据之前
调用 next() 方法使游标下移(返回 true 表示有下一行数据),通过 getXxx(列索引/列名) 方法逐行获取数据(默认仅支持向下移动) - 数据获取(根据字段类型调用对应方法)
整型(int/bigint):使用getInt()或者getLong()
字符串(varchar/char):使用 getString()
通用类型:getObject()(需自行强制转换) - 获取数据的方法是重载的
按列索引:getInt(int index)(索引从1开始)
按列名:getInt(String columnName) 通过字段的名称来取值(比较常用)
释放资源
关闭所有数据库相关资源(ResultSet、Statement、Connection),避免资源泄漏(数据库连接是稀缺资源,不关闭会导致连接耗尽)
注意关闭顺序:先开后关(ResultSet → Statement → Connection),且必须在finally块中关闭,确保无论是否发生异常,资源都会被释放。
finally{
if(resultSet != null){
try {
// 释放资源
resultSet.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if(statement != null){
try {
// 释放资源
statement.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if(connection != null){
try {
// 释放资源
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
数据库操作中加载驱动、获取连接、关闭资源等,这些操作的逻辑是通用的,可以进行向上提取
JDBC工具类三版本
1.0版本
package com.qcby.util;
import java.sql.*;
/**
* JDBC的工具类1.0版本
*/
public class JdbcUtil1 {
//加载驱动
public static void loadDriver() {
try {
Class.forName("com.mysql.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
// 获取连接
public static Connection getConnection() {
//加载驱动
loadDriver();
//获取连接对象
Connection conn = null;
try {
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/ssm", "root", "123456");
} catch (SQLException e) {
e.printStackTrace();
}
return conn;
}
/**
* 关闭资源,适用查询有结果集
* @param conn
* @param stmt
* @param rs
*/
public static void close(Connection conn, Statement stmt, ResultSet rs) {
if (rs != null) {
try { rs.close(); } catch (SQLException e) { e.printStackTrace(); }
}
if (stmt != null) {
try { stmt.close(); } catch (SQLException e) { e.printStackTrace(); }
}
if (conn != null) {
try { conn.close(); } catch (SQLException e) { e.printStackTrace(); }
}
}
/**
* 关闭资源,适用增删改无结果集
* @param conn
* @param stmt
*/
public static void close(Connection conn, Statement stmt) {
if (stmt != null) {
try { stmt.close(); } catch (SQLException e) { e.printStackTrace(); }
}
if (conn != null) {
try { conn.close(); } catch (SQLException e) { e.printStackTrace(); }
}
}
}
为什么在这个JdbcUtil1工具类中,所有方法都用static修饰?
如果不用static修饰,使用时必须先创建JDBCUtil1的实例:
JDBCUtil1 util = new JdbcUtil1(); // 多余的实例化
Connection conn = util.getConnection();
在 Java 中,static 关键字修饰方法表示该方法是类方法,属于类本身,而非类的某个具体实例,用 static 修饰后,可以直接通过类名调用,无需创建实例,更简洁高效:
Connection conn = JdbcUtil1.getConnection(); // 直接调用,无需new对象
2.0版本
其核心优化在于通过读取properties属性文件管理数据库连接参数而非硬编码,从而方便配置修改并减少代码冗余
package com.qcby.util;
import java.io.IOException;
import java.io.InputStream;
import java.sql.*;
import java.util.Properties;
/**
* JDBC的工具类2.0版本
*/
public class JdbcUtil2 {
private static final String driverclass;
private static final String url;
private static final String username;
private static final String password;
static {
//加载属性文件
Properties prop = new Properties();
InputStream inputStream = JdbcUtil2.class.getResourceAsStream("/db.properties");
try {
// 加载配置文件
prop.load(inputStream);
}catch (IOException e){
e.printStackTrace();
}
// 从配置文件获取参数
driverclass = prop.getProperty("driver");
url = prop.getProperty("url");
username = prop.getProperty("user");
password = prop.getProperty("password");
}
/**
* 加载驱动
*/
public static void loadDriver() {
try {
Class.forName(driverclass);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
// 获取连接
public static Connection getConnection() {
//加载驱动
loadDriver();
//获取连接对象,返回
Connection conn = null;
try {
// 获取到链接
conn = DriverManager.getConnection(url, username, password);
} catch (SQLException e) {
e.printStackTrace();
}
return conn;
}
// 关闭资源,适用增删改无结果集
public static void close(Connection conn, Statement stmt) {
if (stmt != null) {
try { stmt.close(); } catch (SQLException e) { e.printStackTrace(); }
}
if (conn != null) {
try { conn.close(); } catch (SQLException e) { e.printStackTrace(); }
}
}
// 关闭资源,适用查询有结果集
public static void close(Connection conn, Statement stmt, ResultSet rs) {
if (rs != null) {
try { rs.close(); } catch (SQLException e) { e.printStackTrace(); }
}
if (stmt != null) {
try { stmt.close(); } catch (SQLException e) { e.printStackTrace(); }
}
if (conn != null) {
try { conn.close(); } catch (SQLException e) { e.printStackTrace(); }
}
}
}
该类定义了四个 private static final 修饰的静态成员变量,这些变量由类加载时自动执行的静态代码块初始化,初始化过程为:当前类的类对象从项目的类路径中读取 db.properties 资源文件,并返回一个用于读取该文件内容的 InputStream 输入流对象,Properties 是 Java 中专门用于处理键值对形式配置文件的工具类(能方便地存储和获取配置项),通过调用 Properties 的load(InputStream inStream) 方法,从获取到的输入流中读取属性列表(键和元素对),并将其解析后存储到 prop 对象中,后续通过 getProperty(String key) 用指定的键在此属性列表中搜索属性,获取对应配置值,并赋值给静态变量,这样初始化静态成员变量就完成了。这样就可以通过读取 properties 配置文件获取数据库连接参数(而非硬编码在代码中),方便后期修改数据库配置。
该工具类中还提供了三个核心方法:loadDriver方法通过反射调用Class.forName(driverclass)加载数据库驱动;getConnection方法先调用loadDriver加载驱动,再通过DriverManager.getConnection(url, username, password)获取数据库连接并返回;此外还有两个重载的close方法,分别用于增删改(无结果集)和查询(有结果集)操作后的资源关闭,前者关闭Connection和Statement,后者按ResultSet→Statement→Connection的顺序关闭资源,且每个资源关闭前都会判断非空以避免空指针异常。
3.0版本
简化 Java 程序操作数据库的流程 —— 通过连接池复用数据库连接,避免频繁创建 / 关闭连接的性能损耗
导入坐标依赖
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.8</version>
</dependency>
</dependencies>
package com.qcby.util;
import com.alibaba.druid.pool.DruidDataSourceFactory;
import javax.sql.DataSource;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;
/**
* JDBC工具类 3.0 版本
* 加入数据库连接池
*/
public class JdbcUtil3 {
// 连接池对象
private static DataSource DATA_SOURCE;
static {
// 加载属性文件
Properties prop = new Properties();
InputStream inputStream = JdbcUtil3.class.getResourceAsStream("/druid.properties");
try {
// 加载配置文件
prop.load(inputStream);
// 创建连接池对象
DATA_SOURCE = DruidDataSourceFactory.createDataSource(prop);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 从连接池获取连接
*/
public static Connection getConnection() {
Connection conn = null;
try {
conn = DATA_SOURCE.getConnection(); // 从连接池拿连接
} catch (SQLException e) {
e.printStackTrace();
}
return conn;
}
/**
* 关闭资源(连接归还到池,而非真正关闭)
*/
public static void close(Connection conn, Statement stmt, ResultSet rs) {
if (rs != null) {
try { rs.close(); } catch (SQLException e) { e.printStackTrace(); }
}
if (stmt != null) {
try { stmt.close(); } catch (SQLException e) { e.printStackTrace(); }
}
if (conn != null) {
try {
// 连接池的连接close()是归还,不是关闭
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
// 重载:无结果集时归还
public static void close(Connection conn, Statement stmt) {
if (stmt != null) {
try { stmt.close(); } catch (SQLException e) { e.printStackTrace(); }
}
if (conn != null) {
try {
// 连接池的连接close()是归还,不是关闭
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
这是一个基于Druid连接池的JDBC工具类,其核心逻辑是通过数据库连接池高效管理数据库连接。在类加载时,静态代码块会加载druid.properties配置文件(该文件包含数据库驱动类、连接URL、用户名、密码以及连接池参数如初始连接数、最大活跃连接数等),并利用DruidDataSourceFactory创建连接池对象DARA_SOURCE;提供getConnection()方法从连接池获取数据库连接,而非直接创建新连接,以减少资源消耗;提供两个重载的close()方法,用于关闭Statement、ResultSet并将连接归还给连接池而非真正关闭连接),其中一个处理包含ResultSet的情况,另一个处理无ResultSet的情况,通过这种方式实现连接的复用,提升数据库操作效率。
SQL 注入漏洞
SQL 注入漏洞分析
当使用字符串拼接方式构造SQL语句时:String sql = "select * from t_user where username = '"+username+"' and password ='"+password+"'";
攻击者可通过特殊输入绕过验证:
输入用户名为 aaa’or’1=1(密码任意) 最终SQL语句变为: String sql = "select * from t_user where username = 'aaa'or'1=1' and password = 'sfsdfsds";
输入用户名为 aaa’-- '(密码任意) 最终SQL语句变为:String sql = "select * from t_user where username = 'aaa'‐‐ '' and password = 'sfsdfsdfs";
本质是拼接SQL语句,最终 SQL 会返回数据库中所有用户数据,攻击者无需正确密码即可登录
解决方案
“参数化查询”是一种旨在防御SQL注入的安全机制,其核心思想是通过严格分离SQL语句的结构与参数数据,确保用户输入仅作为“数据”参与数据库操作,而非被解析为SQL代码的一部分。
在Java语言中,PreparedStatement 接口( Statement 的子接口)是遵循JDBC规范、实现参数化查询机制的标准工具,也是Java开发者防御SQL注入的首选方案。其实现逻辑与核心功能如下:
基于“预编译+参数占位符”的结构锁定
PreparedStatement 通过以下流程实现SQL结构与参数的分离:- 使用 ? 作为参数占位符,替代SQL语句中需要动态传入的参数
- 调用 Connection.prepareStatement(String sql) 方法,将包含占位符的SQL模板发送至数据库服务器进行预编译,生成固定的执行计划,此时SQL语句的逻辑结构已被永久锁定,无法被后续传入的参数修改;
- 后续通过 setXxx() 系列方法(如setInt()、setString()、setObject()等)向占位符传入具体参数值,这些参数仅作为“数据”填充至预设位置,不会被数据库引擎解析为SQL代码。
setXxx() 方法内容
setXxx() 系列方法是参数化查询机制在代码层面的关键实现,通过双重机制强化安全性:- 强制类型匹配:根据方法名(如setInt()对应整数类型、setString()对应字符串类型)对传入参数进行类型校验,确保参数类型与SQL语句中对应字段的类型要求一致,从源头避免因类型不匹配导致的注入风险;
- 自动转义特殊字符:对于字符串等可能包含特殊符号的参数类型,数据库驱动会自动对单引号、分号、注释符等SQL注入常用字符进行转义处理(如将单引号转换为数据库可识别的转义形式),确保这些字符仅作为数据的一部分被处理,而非用于篡改SQL结构。