一、JDBC到底是什么?
1.1 官方定义与我的理解
JDBC的全称是Java DataBase Connectivity,翻译过来就是Java数据库连接。它是一套由Sun公司定义的接口规范,注意,是"接口",不是实现。为什么要设计成接口?因为数据库产品太多了,MySQL、Oracle、SQL Server、PostgreSQL,每个都有自己的通信协议和底层实现。如果Java直接依赖某个数据库的实现类,那代码就写死了,换数据库就得重写。
所以JDBC只定义标准,而具体的实现类(也就是我们常说的"驱动")由各个数据库厂商提供。我们开发者只需要学会调用JDBC接口里的方法就行了,至于底层怎么和数据库通信,那是驱动的事。这其实就是面向接口编程的一个典型应用。
1.2 驱动是个啥?
驱动就像是数据库和Java程序之间的"翻译官"。Java程序发出去的SQL指令,驱动负责把它翻译成数据库能听懂的语言,然后传给数据库执行;执行完的结果,驱动再翻译回Java能识别的格式。所以,在使用JDBC之前,第一步就是把对应数据库的驱动jar包导入到项目中。比如我们用的是MySQL,就需要导入mysql-connector-java这个jar包,版本号根据你的MySQL版本来选。
二、JDBC快速入门
2.1 准备一张测试表
先创建一个数据库和一张用户表。
CREATE DATABASE jdbcdemo;
USE jdbcdemo;
CREATE TABLE t_user (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(30),
password VARCHAR(30),
email VARCHAR(30)
);
INSERT INTO t_user VALUES (NULL, 'aaa', '123', 'aaa@163.com');
INSERT INTO t_user VALUES (NULL, 'bbb', '456', 'bb@163.com');
INSERT INTO t_user VALUES (NULL, 'ccc', '789', 'ccc@163.com');
2.2 第一个JDBC程序:查询所有用户
目标是查出t_user表里的所有数据,并在控制台打印出来。按照下面五个步骤来:
第一步:加载驱动
第二步:获取数据库连接
第三步:创建执行SQL语句的对象
第四步:执行查询,拿到结果集
第五步:遍历结果集并输出
第六步:释放资源
直接上代码
public class JdbcDemo1 {
public static void main(String[] args) throws Exception {
// 1. 加载驱动(这里利用Class.forName反射加载)
Class.forName("com.mysql.jdbc.Driver");
// 2. 获取连接
// url格式:jdbc:mysql://主机:端口/数据库名
// 如果连接本机且端口是3306,可以简写为 jdbc:mysql:///jdbcdemo
String url = "jdbc:mysql:///jdbcdemo";
String user = "root";
String password = "root";
Connection conn = DriverManager.getConnection(url, user, password);
// 3. 创建Statement对象,用来执行SQL
Statement stmt = conn.createStatement();
// 4. 执行查询,返回ResultSet结果集
String sql = "SELECT * FROM t_user";
ResultSet rs = stmt.executeQuery(sql);
// 5. 遍历结果集
// rs.next() 移动游标到下一行,有数据返回true,否则false
while (rs.next()) {
// 根据字段类型取值,可以用字段名或列索引(从1开始)
int id = rs.getInt("id");
String username = rs.getString("username");
String passwordVal = rs.getString("password");
String email = rs.getString("email");
System.out.println(id + " -- " + username + " -- " + passwordVal + " -- " + email);
}
// 6. 释放资源(注意顺序:先开的后关)
rs.close();
stmt.close();
conn.close();
}
}
运行这个程序,控制台就会打印出三条用户记录,但这已经是一个完整的JDBC查询流程了,后面的所有操作都是在这个骨架上的扩展。
三、核心API详解
3.1 DriverManager:驱动的管理者
DriverManager主要负责两件事:加载驱动和获取连接。
关于加载驱动,早期写法是 DriverManager.registerDriver(new Driver()),但这种方式有两个问题:一是硬编码依赖了MySQL的Driver类,换数据库得改代码;二是会导致驱动被注册两次(因为Driver类自己静态块里也注册了一次)。所以现在通用的做法是 Class.forName("com.mysql.jdbc.Driver"),利用类加载机制触发驱动类的静态代码块完成注册,解耦又干净。
获取连接用 getConnection(url, user, password) 方法,三个参数分别是:
URL:格式为 jdbc:mysql://主机:端口/数据库名。如果连本地且默认端口,可以缩写成 jdbc:mysql:///jdbcdemo,非常方便。
用户名和密码:就是你的数据库登录账号。
3.2 Connection:数据库连接对象
Connection代表和数据库之间的物理连接,这个对象非常"昂贵",因为每次创建都要经过TCP握手、认证等过程,所以用完必须立即关闭,否则会造成资源泄露。它有两个核心作用:
创建执行SQL的对象:createStatement() 返回普通的Statement;prepareStatement(String sql) 返回预编译的PreparedStatement(后面会重点讲,它能防SQL注入)。
管理事务:通过 setAutoCommit(false) 开启事务,然后用 commit() 和 rollback() 来控制提交或回滚。
3.3 Statement:执行SQL的"传令兵"
Statement对象用来发送SQL语句到数据库执行。最常用的方法就两个:
executeQuery(String sql):执行查询语句(SELECT),返回ResultSet。
executeUpdate(String sql):执行增删改语句(INSERT/UPDATE/DELETE),返回受影响的行数(int)。
另外,Statement还支持批处理,用 addBatch() 和 executeBatch() 可以一次发送多条SQL,提升效率,不过这个在实际项目中用得不多,因为PreparedStatement也能支持批处理。
3.4 ResultSet:查询结果集
执行查询后,数据库返回的数据会被封装进ResultSet对象。你可以把它想象成一个二维表格,内部有个游标(指针),一开始指向第一行数据之前。调用 next() 方法会让游标下移一行,如果指向了有效行,就返回true,否则false。
取数据的时候,根据字段类型调用对应的getXxx方法,比如 getInt()、getString()、getDouble() 等。这些方法都有重载版本,既可以通过列索引(从1开始)取,也可以通过列名取。我个人强烈建议使用列名,因为可读性更好,而且即使SQL调整了字段顺序,代码也不会受影响。
3.5 释放资源
数据库连接、Statement、ResultSet都持有底层资源,如果不释放,最终会导致连接耗尽、内存溢出。所以释放资源的代码一定要写在 finally 块里,确保无论程序是否发生异常,都能执行。
标准释放模板如下:
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(); }
}
注意关闭顺序:先关ResultSet,再关Statement,最后关Connection。如果先关Connection,后面再关Statement可能会报错,因为连接已经没了。
四、封装工具类
每次写重复的加载驱动、获取连接、释放资源太烦了,而且硬编码URL和密码也不安全。通常我们会把JDBC的公共操作提取到一个工具类里,同时把数据库配置放到外部的properties文件中,方便修改。
4.1 创建配置文件 db.properties
在src目录下创建这个文件,内容如下:
driver=com.mysql.jdbc.Driver
url=jdbc:mysql:///jdbcdemo
user=root
password=root
4.2 编写工具类 JdbcUtils
这个类负责加载配置文件、注册驱动、提供获取连接的方法,以及释放资源的方法。
import java.io.InputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;
public class JdbcUtils {
private static String driver;
private static String url;
private static String user;
private static String password;
// 静态代码块,类加载时执行一次,加载配置文件并注册驱动
static {
try {
Properties props = new Properties();
// 从类路径下加载配置文件
InputStream is = JdbcUtils.class.getResourceAsStream("/db.properties");
props.load(is);
driver = props.getProperty("driver");
url = props.getProperty("url");
user = props.getProperty("user");
password = props.getProperty("password");
// 注册驱动
Class.forName(driver);
} catch (Exception e) {
throw new ExceptionInInitializerError("JDBC初始化失败,请检查配置文件");
}
}
// 获取连接
public static Connection getConnection() throws SQLException {
return DriverManager.getConnection(url, user, password);
}
// 释放资源(重载方法,针对查询场景)
public static void release(ResultSet rs, Statement stmt, Connection conn) {
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(); }
}
}
// 释放资源(针对增删改,没有ResultSet)
public static void release(Statement stmt, Connection conn) {
release(null, stmt, conn);
}
}
有了这个工具类,前面的查询代码可以简化为:
Connection conn = JdbcUtils.getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM t_user");
while (rs.next()) {
// 处理数据...
}
JdbcUtils.release(rs, stmt, conn);
配置统一管理,以后改密码不用动代码。
五、控制台登录案例
需求很简单:从控制台输入用户名和密码,去数据库校验,如果存在则登录成功,否则失败。
先定义一个User实体类(JavaBean),对应t_user表:
public class User {
private int id;
private String username;
private String password;
private String email;
// 构造方法、getter/setter 省略...
}
然后是登录逻辑:
public class LoginDemo {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.print("请输入用户名:");
String username = sc.nextLine();
System.out.print("请输入密码:");
String password = sc.nextLine();
Connection conn = null;
Statement stmt = null;
ResultSet rs = null;
try {
conn = JdbcUtils.getConnection();
stmt = conn.createStatement();
// 注意:这里使用了字符串拼接,存在SQL注入风险!
String sql = "SELECT * FROM t_user WHERE username = '" + username + "' AND password = '" + password + "'";
rs = stmt.executeQuery(sql);
if (rs.next()) {
System.out.println("登录成功,欢迎 " + rs.getString("username"));
} else {
System.out.println("用户名或密码错误");
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
JdbcUtils.release(rs, stmt, conn);
}
}
}
这个功能看起来没问题,但如果你输入用户名 aaa,密码随便输一个比如 123456,它能正常工作吗?试一下,你会发现它仍然能登录成功,这就是经典的SQL注入漏洞。
六、SQL注入
6.1 漏洞是怎么产生的?
问题出在SQL语句的拼接上。当我们把用户输入直接拼到SQL里时,用户就可以构造特殊的字符串来改变SQL的语义。
比如输入用户名 aaa' or '1'='1,密码随便输(比如 123),那么拼接后的SQL变成了:
SELECT * FROM t_user WHERE username = 'aaa' or '1'='1' AND password = '123'
因为 '1'='1' 恒成立,所以整个WHERE条件变成真,就相当于查询了所有用户,而且只取第一条,自然就能绕过密码验证了。
再比如输入用户名 aaa' ,密码任意,拼接后:
SELECT * FROM t_user WHERE username = 'aaa' -- ' AND password = '123'
在SQL里是注释符,后面的 AND 条件被注释掉了,所以同样能查出aaa的记录。
这就是SQL注入的本质:用户输入被当成了SQL代码的一部分,而不是单纯的数据。
6.2 怎么解决?用PreparedStatement
PreparedStatement 是 Statement 的子接口,它有两个核心优势:
预编译:SQL语句先发送到数据库进行编译,编译后的结构固定,参数部分用 ? 占位。
参数化查询:执行时再把具体参数传入,参数会被当作纯文本处理,不会再次被解释为SQL指令,从而彻底杜绝注入。
把上面的登录代码改成PreparedStatement版本:
String sql = "SELECT * FROM t_user WHERE username = ? AND password = ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
// 设置参数,第一个参数是占位符的索引(从1开始)
pstmt.setString(1, username);
pstmt.setString(2, password);
rs = pstmt.executeQuery(); // 注意这里不需要再传sql了
即使用户名输入 aaa' or '1'='1,它也会被当作一个普通的字符串值去匹配,而不会被解释成SQL关键字,所以查询结果一定是空,登录失败。
除了安全,PreparedStatement还有性能优势,因为预编译的SQL可以在数据库端缓存,多次执行只需要传不同参数,解析开销小。所以,在实际开发中,**永远不要使用Statement拼接SQL,一律使用PreparedStatement**,这应该成为你的肌肉记忆。
七、总结与建议
到这里,JDBC的入门核心知识就讲完了。我们从头梳理一下:
JDBC是Java操作数据库的标准接口,驱动由数据库厂商提供。
开发步骤:加载驱动 -> 获取连接 -> 创建Statement/PreparedStatement -> 执行SQL -> 处理结果集 -> 释放资源。
核心API:DriverManager、Connection、Statement(及PreparedStatement)、ResultSet。
一定要封装工具类,把配置抽离到properties文件。
登录案例暴露了SQL注入问题,必须用PreparedStatement解决。
最后给新手几个小建议:
-
刚开始写JDBC代码时,不要急着用框架,先把原生API用熟,这样以后看框架源码才不会懵。
-
释放资源的代码一定写在finally里,或者用try-with-resources(Java 7+),总之不能忘。
-
开发中尽量使用PreparedStatement,既安全又高效。
-
生产环境建议配合连接池使用(比如Druid),但那是进阶内容,先把基础打牢。
JDBC是Java后端开发的必修课,虽然现在很多项目都用ORM框架,但底层原理永远不会过时。希望这篇文章能帮你顺利入门,如果有什么疑问,欢迎在评论区留言讨论。我们下篇见!