一、前言

数据库操作是 Java 后端开发的必备技能,而 JDBC(Java Database Connectivity)是 Java 程序与数据库交互的基础 API。本文将以一个完整的 “简易学生管理系统” 项目为目标,带大家从最基础的 JDBC 连接开始,一步步深入到核心 API 的使用、SQL 注入的防范,最终实现连接池的优化,让你真正掌握企业级开发中的数据库操作流程。


二、JDBC 连接数据库 6 步

2.1 核心 API 介绍

  • DriverManager:驱动管理类,用于加载数据库驱动并创建Connection对象。
  • Connection:连接对象,代表 Java 程序与数据库的一个会话。
  • Statement:执行 SQL 语句的对象。
  • ResultSet:结果集对象,用于存储 SQL 查询返回的数据。

2.2 连接代码示例

java

运行

import java.sql.*;

public class JdbcConnection {
    private static final String URL = "jdbc:mysql://localhost:3306/student_db?useSSL=false&serverTimezone=UTC";
    private static final String USER = "root";
    private static final String PASSWORD = "your_password";

    public static void main(String[] args) {
        Connection conn = null;
        Statement stmt = null;
        ResultSet rs = null;

        try {
            // 1. 加载数据库驱动
            Class.forName("com.mysql.cj.jdbc.Driver");
            // 2. 获取数据库连接
            conn = DriverManager.getConnection(URL, USER, PASSWORD);
            System.out.println("数据库连接成功!");

            // 3. 创建Statement对象
            stmt = conn.createStatement();
            // 4. 执行SQL查询
            String sql = "SELECT id, name, age FROM student";
            rs = stmt.executeQuery(sql);

            // 5. 处理查询结果
            while (rs.next()) {
                int id = rs.getInt("id");
                String name = rs.getString("name");
                int age = rs.getInt("age");
                System.out.printf("ID: %d, 姓名: %s, 年龄: %d%n", id, name, age);
            }
        } catch (ClassNotFoundException | SQLException e) {
            e.printStackTrace();
        } finally {
            // 6. 关闭资源(注意顺序)
            try { if (rs != null) rs.close(); } catch (SQLException e) { e.printStackTrace(); }
            try { if (stmt != null) stmt.close(); } catch (SQLException e) { e.printStackTrace(); }
            try { if (conn != null) conn.close(); } catch (SQLException e) { e.printStackTrace(); }
        }
    }
}

三、SQL 注入演示与防范

3.1 什么是 SQL 注入?

SQL 注入是一种常见的网络攻击手段,黑客通过在用户输入的数据中插入 SQL 语句片段,欺骗数据库执行非授权的操作。

3.2 注入风险演示(使用 Statement)

java

运行

// 假设这是一个登录验证方法
public static boolean login(String username, String password) {
    Connection conn = null;
    Statement stmt = null;
    ResultSet rs = null;
    boolean isLogin = false;

    try {
        conn = DriverManager.getConnection(URL, USER, PASSWORD);
        stmt = conn.createStatement();
        // 危险!直接拼接用户输入到SQL语句中
        String sql = "SELECT * FROM user WHERE username='" + username + "' AND password='" + password + "'";
        rs = stmt.executeQuery(sql);
        isLogin = rs.next(); // 如果有结果,说明登录成功
    } catch (SQLException e) {
        e.printStackTrace();
    } finally {
        // 关闭资源...
    }
    return isLogin;
}

攻击方式: 当黑客输入用户名:' OR '1'='1 --,密码任意。 拼接后的 SQL 语句变为: SELECT * FROM user WHERE username='' OR '1'='1 --' AND password='...' 这条 SQL 会查询出所有用户记录,导致登录绕过。

3.3 使用 PreparedStatement 防范注入

PreparedStatementStatement的子类,它支持参数化查询,可以有效防止 SQL 注入。

java

运行

public static boolean safeLogin(String username, String password) {
    Connection conn = null;
    PreparedStatement pstmt = null;
    ResultSet rs = null;
    boolean isLogin = false;

    try {
        conn = DriverManager.getConnection(URL, USER, PASSWORD);
        // 使用占位符?代替直接拼接
        String sql = "SELECT * FROM user WHERE username = ? AND password = ?";
        pstmt = conn.prepareStatement(sql);
        // 设置参数(索引从1开始)
        pstmt.setString(1, username);
        pstmt.setString(2, password);
        rs = pstmt.executeQuery();
        isLogin = rs.next();
    } catch (SQLException e) {
        e.printStackTrace();
    } finally {
        // 关闭资源...
    }
    return isLogin;
}

原理PreparedStatement会将 SQL 语句的结构和数据分开处理,数据库会先编译 SQL 模板,再将用户输入的数据作为纯文本参数传入,从而避免了注入风险。


四、数据库连接池配置

4.1 为什么需要连接池?

每次通过DriverManager获取新连接,都需要与数据库进行三次握手、认证等过程,开销巨大且效率低下。连接池通过预先创建并管理一组数据库连接,让应用程序可以复用这些连接,显著提高性能。

4.2 常用连接池

  • HikariCP:Spring Boot 默认推荐,以高性能著称。
  • Druid:阿里巴巴开源,监控功能强大,国内使用广泛。
  • C3P0:经典老牌连接池,配置简单。

4.3 Druid 连接池配置示例

1. 引入 Maven 依赖

xml

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.2.16</version>
</dependency>

2. 编写配置文件(druid.properties)

properties

# 数据库基本配置
driverClassName=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/student_db?useSSL=false&serverTimezone=UTC
username=root
password=your_password

# 连接池配置
initialSize=5      # 初始化连接数
minIdle=5          # 最小空闲连接数
maxActive=20       # 最大活跃连接数
maxWait=60000      # 获取连接时的最大等待时间(毫秒)
timeBetweenEvictionRunsMillis=60000  # 多久一次检测需要关闭的空闲连接
minEvictableIdleTimeMillis=300000    # 连接在池中最小生存的时间
validationQuery=SELECT 1              # 用来检测连接是否有效的SQL
testWhileIdle=true                     # 建议配置为true,不影响性能,并且保证安全性
testOnBorrow=false                     # 申请连接时执行validationQuery检测连接是否有效,会降低性能
testOnReturn=false                     # 归还连接时执行validationQuery检测连接是否有效,会降低性能
poolPreparedStatements=true            # 开启PSCache
maxPoolPreparedStatementPerConnectionSize=20  # 每个连接上PSCache的大小

3. 编写连接池工具类

java

运行

import com.alibaba.druid.pool.DruidDataSourceFactory;
import javax.sql.DataSource;
import java.io.InputStream;
import java.sql.Connection;
import java.util.Properties;

public class DruidUtils {
    private static DataSource dataSource;

    static {
        try {
            // 加载配置文件
            Properties props = new Properties();
            InputStream is = DruidUtils.class.getClassLoader().getResourceAsStream("druid.properties");
            props.load(is);
            // 初始化连接池
            dataSource = DruidDataSourceFactory.createDataSource(props);
        } catch (Exception e) {
            throw new RuntimeException("初始化Druid连接池失败", e);
        }
    }

    // 获取连接
    public static Connection getConnection() {
        try {
            return dataSource.getConnection();
        } catch (Exception e) {
            throw new RuntimeException("获取数据库连接失败", e);
        }
    }

    // 获取DataSource
    public static DataSource getDataSource() {
        return dataSource;
    }
}

五、简易学生管理系统实现

5.1 系统功能设计

我们将实现一个基于控制台的学生信息管理系统,包含以下功能:

  • 新增学生:向数据库中插入一条新的学生记录。
  • 查询学生:根据 ID 查询单个学生信息,或查询所有学生信息。
  • 更新学生:修改已有学生的信息(如姓名、年龄)。
  • 删除学生:根据 ID 删除一条学生记录。

5.2 数据库表设计

sql

CREATE DATABASE IF NOT EXISTS student_db;
USE student_db;

CREATE TABLE IF NOT EXISTS student (
    id INT PRIMARY KEY AUTO_INCREMENT COMMENT '学生ID',
    name VARCHAR(50) NOT NULL COMMENT '学生姓名',
    age INT COMMENT '学生年龄',
    gender CHAR(1) COMMENT '学生性别(M/F)',
    major VARCHAR(100) COMMENT '所属专业',
    create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间',
    update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

5.3 核心代码实现(以查询和新增为例)

1. 学生实体类(Student.java)

java

运行

public class Student {
    private Integer id;
    private String name;
    private Integer age;
    private String gender;
    private String major;

    // 构造方法、getter和setter省略...

    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                ", gender='" + gender + '\'' +
                ", major='" + major + '\'' +
                '}';
    }
}

2. 数据访问对象(StudentDAO.java)

java

运行

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;

public class StudentDAO {

    // 查询所有学生
    public List<Student> queryAllStudents() {
        List<Student> students = new ArrayList<>();
        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;

        try {
            conn = DruidUtils.getConnection();
            String sql = "SELECT id, name, age, gender, major FROM student";
            pstmt = conn.prepareStatement(sql);
            rs = pstmt.executeQuery();

            while (rs.next()) {
                Student student = new Student();
                student.setId(rs.getInt("id"));
                student.setName(rs.getString("name"));
                student.setAge(rs.getInt("age"));
                student.setGender(rs.getString("gender"));
                student.setMajor(rs.getString("major"));
                students.add(student);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 关闭资源...
        }
        return students;
    }

    // 新增学生
    public int addStudent(Student student) {
        Connection conn = null;
        PreparedStatement pstmt = null;
        int affectedRows = 0;

        try {
            conn = DruidUtils.getConnection();
            String sql = "INSERT INTO student(name, age, gender, major) VALUES (?, ?, ?, ?)";
            pstmt = conn.prepareStatement(sql);
            pstmt.setString(1, student.getName());
            pstmt.setInt(2, student.getAge());
            pstmt.setString(3, student.getGender());
            pstmt.setString(4, student.getMajor());
            affectedRows = pstmt.executeUpdate();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 关闭资源...
        }
        return affectedRows;
    }

    // 更新学生、删除学生等方法类似,省略...
}

3. 控制台交互界面(Main.java)

java

运行

import java.util.List;
import java.util.Scanner;

public class Main {
    private static StudentDAO studentDAO = new StudentDAO();
    private static Scanner scanner = new Scanner(System.in);

    public static void main(String[] args) {
        while (true) {
            System.out.println("===== 简易学生管理系统 =====");
            System.out.println("1. 新增学生");
            System.out.println("2. 查询所有学生");
            System.out.println("3. 更新学生信息");
            System.out.println("4. 删除学生");
            System.out.println("5. 退出系统");
            System.out.print("请输入操作编号:");
            int choice = scanner.nextInt();
            scanner.nextLine(); // 读取换行符

            switch (choice) {
                case 1:
                    addStudent();
                    break;
                case 2:
                    queryAllStudents();
                    break;
                case 3:
                    // updateStudent();
                    break;
                case 4:
                    // deleteStudent();
                    break;
                case 5:
                    System.out.println("退出系统...");
                    scanner.close();
                    return;
                default:
                    System.out.println("无效的操作编号,请重新输入!");
            }
        }
    }

    private static void addStudent() {
        System.out.print("请输入学生姓名:");
        String name = scanner.nextLine();
        System.out.print("请输入学生年龄:");
        int age = scanner.nextInt();
        scanner.nextLine();
        System.out.print("请输入学生性别(M/F):");
        String gender = scanner.nextLine();
        System.out.print("请输入学生专业:");
        String major = scanner.nextLine();

        Student student = new Student();
        student.setName(name);
        student.setAge(age);
        student.setGender(gender);
        student.setMajor(major);

        int result = studentDAO.addStudent(student);
        if (result > 0) {
            System.out.println("新增学生成功!");
        } else {
            System.out.println("新增学生失败!");
        }
    }

    private static void queryAllStudents() {
        List<Student> students = studentDAO.queryAllStudents();
        System.out.println("===== 学生列表 =====");
        for (Student s : students) {
            System.out.println(s);
        }
    }

    // updateStudent、deleteStudent等方法类似,省略...
}

六、总结

通过本文的学习和实战,我们从 JDBC 的基础 API 入手,深入理解了数据库连接、SQL 执行、结果处理的完整流程。我们重点学习了如何使用PreparedStatement防止 SQL 注入这一关键安全问题,并掌握了在生产环境中必须使用的数据库连接池技术(以 Druid 为例)。最后,我们通过一个简易学生管理系统的设计与实现,将所有知识点融会贯通,真正做到了学以致用。

希望通过这个项目,你能对 Java 数据库操作有一个全面而深入的理解,为后续学习 ORM 框架(如 MyBatis、Hibernate)打下坚实的基础。

更多推荐