JDBC的概述
JDBC 的工作流程
加载并注册数据库驱动
1.显式加载驱动
public class DriverLoad1 {public static void main(String[] args) throws Exception {// 创建MySQL驱动实例Driver driver = new Driver();// 显式注册驱动到DriverManagerDriverManager.registerDriver(driver);// 后续可通过DriverManager获取连接// Connection conn = DriverManager.getConnection(url, user, password);}
}
2.通过反射加载驱动类(触发自动注册)
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);}
}
建立数据库连接
创建 SQL 执行载体
- 创建用于执行 SQL 语句的载体Statement createStatement():创建 Statement 接口实现对象PreparedStatement prepareStatement(String sql):创建 Statement 接口的子接口 PreparedStatement 的实现对象,有效防止 SQL 注入漏洞
- 事务管理void setAutoCommit(boolean autoCommit):设置事务的自动提交模式 void commit():提交当前事务 void rollback():回滚当前事务
执行 SQL 语句
处理结果
- 封装数据内部维护游标,初始位置在第一行数据之前调用 next() 方法使游标下移(返回 true 表示有下一行数据),通过 getXxx(列索引/列名) 方法逐行获取数据(默认仅支持向下移动)
- 数据获取(根据字段类型调用对应方法)整型(int/bigint):使用getInt()或者getLong()字符串(varchar/char):使用 getString()通用类型:getObject()(需自行强制转换)
- 获取数据的方法是重载的按列索引:getInt(int index)(索引从1开始)按列名:getInt(String columnName) 通过字段的名称来取值(比较常用)
释放资源
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版本
/*** 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属性文件管理数据库连接参数而非硬编码,从而方便配置修改并减少代码冗余
/*** 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 程序操作数据库的流程 —— 通过连接池复用数据库连接,避免频繁创建 / 关闭连接的性能损耗
导入坐标依赖
com.alibaba druid 1.2.8
/*** 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();}}}
}

SQL 注入漏洞
SQL 注入漏洞分析
解决方案
- 基于“预编译+参数占位符”的结构锁定
- 使用 ? 作为参数占位符,替代SQL语句中需要动态传入的参数
- 调用 Connection.prepareStatement(String sql) 方法,将包含占位符的SQL模板发送至数据库服务器进行预编译,生成固定的执行计划,此时SQL语句的逻辑结构已被永久锁定,无法被后续传入的参数修改;
- 后续通过 setXxx() 系列方法(如setInt()、setString()、setObject()等)向占位符传入具体参数值,这些参数仅作为“数据”填充至预设位置,不会被数据库引擎解析为SQL代码。
- setXxx() 方法内容
- 强制类型匹配:根据方法名(如setInt()对应整数类型、setString()对应字符串类型)对传入参数进行类型校验,确保参数类型与SQL语句中对应字段的类型要求一致,从源头避免因类型不匹配导致的注入风险;
- 自动转义特殊字符:对于字符串等可能包含特殊符号的参数类型,数据库驱动会自动对单引号、分号、注释符等SQL注入常用字符进行转义处理(如将单引号转换为数据库可识别的转义形式),确保这些字符仅作为数据的一部分被处理,而非用于篡改SQL结构。