JDBC入门全攻略

一、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解决。

最后给新手几个小建议:

  1. 刚开始写JDBC代码时,不要急着用框架,先把原生API用熟,这样以后看框架源码才不会懵。

  2. 释放资源的代码一定写在finally里,或者用try-with-resources(Java 7+),总之不能忘。

  3. 开发中尽量使用PreparedStatement,既安全又高效。

  4. 生产环境建议配合连接池使用(比如Druid),但那是进阶内容,先把基础打牢。

JDBC是Java后端开发的必修课,虽然现在很多项目都用ORM框架,但底层原理永远不会过时。希望这篇文章能帮你顺利入门,如果有什么疑问,欢迎在评论区留言讨论。我们下篇见!

相关推荐
云水一下2 天前
零基础玩转bWAPP靶场(二十三):SQL 注入——存储型(SQLite)
web安全·sql注入·bwapp
云水一下6 天前
零基础玩转bWAPP靶场(十四):SQL 注入(GET/选择型)
web安全·sql注入·bwapp
洵有兮6 天前
sql注入通关笔记
数据库·笔记·sql·sql注入
云水一下7 天前
零基础玩转bWAPP靶场(十三):SQL 注入(GET/搜索型)
web安全·sql注入·bwapp
Sagittarius_A*10 天前
SQL 注入漏洞:SQL 语句拼接缺陷与全类型注入利用技术
数据库·sql·安全·sql注入·dvwa
祁白_11 天前
sqlmap工具
web安全·网络安全·sql注入·sqlmap
lcreek2 个月前
SQL盲注漏洞详解 DVWA High
网络安全·sql注入
lcreek2 个月前
SQL 注入漏洞详解:从原理到防御的完整学习指南
网络安全·sql注入
三垣网安2 个月前
记一次补天公益SRC漏洞挖掘:弱口令+SQL注入+信息泄露
sql注入·信息泄露·弱口令