学习地址\](【尚硅谷2024最新JDBC教程 \| jdbc基础到高级一套通关!】 [www.bilibili.com/video/BV1Tx...](https://link.juejin.cn?target=https%3A%2F%2Fwww.bilibili.com%2Fvideo%2FBV1Tx421S7HZ%2F%3Fp%3D34%26share_source%3Dcopy_web%26vd_source%3D1a47fe5dfa3ae49f76d25e6238dd7d03 "https://www.bilibili.com/video/BV1Tx421S7HZ/?p=34&share_source=copy_web&vd_source=1a47fe5dfa3ae49f76d25e6238dd7d03"))
建立项目如图: 
并执行sql语句:
```sql
CREATE DATABASE rainsoul;
use rainsoul;
create table t_emp
(
emp_id int auto_increment comment '员工编号' primary key,
emp_name varchar(100) not null comment '员工姓名',
emp_salary double(10, 5) not null comment '员工薪资',
emp_age int not null comment '员工年龄'
);
insert into t_emp (emp_name, emp_salary, emp_age)
values ('andy', 777.77, 32),
('大风哥', 666.66, 41),
('康师傅', 111, 23),
('Gavin', 123, 26),
('小鱼儿', 123, 28);
```
# 2.快速入门
查询数据库所有信息:
```java
package com.rainsoul.base;
import java.sql.*;
public class JDBCQuick {
/**
* 主函数:演示从MySQL数据库中查询员工信息并打印。
* @param args 命令行参数(未使用)
* @throws ClassNotFoundException 如果MySQL JDBC驱动未找到,则抛出此异常
* @throws SQLException 如果数据库操作出现错误,则抛出此异常
*/
public static void main(String[] args) throws ClassNotFoundException, SQLException {
// 注册MySQL JDBC驱动
Class.forName("com.mysql.cj.jdbc.Driver");
// 获取到数据库的连接
Connection connection = DriverManager
.getConnection("jdbc:mysql://localhost:3306/rainsoul", "root", "root");
// 创建PreparedStatement对象,用于执行预编译的SQL查询
PreparedStatement preparedStatement = connection
.prepareStatement("select emp_id,emp_name,emp_salary,emp_age from t_emp");
// 执行查询语句,获取结果集
ResultSet resultSet = preparedStatement.executeQuery();
// 处理查询结果,将每条员工信息打印出来
while (resultSet.next()) {
int empId = resultSet.getInt("emp_id");
String empName = resultSet.getString("emp_name");
String empSalary = resultSet.getString("emp_salary");
int empAge = resultSet.getInt("emp_age");
System.out.println(empId + "\t" + empName + "\t" + empSalary + "\t" + empAge);
}
// 释放资源,关闭ResultSet、PreparedStatement以及Connection
resultSet.close();
preparedStatement.close();
connection.close();
}
}
```
查询结果: 
# 3.核心API流程的理解
## 3.1注册驱动
```java
Class.forName("com.mysql.cj.jdbc.Driver");
```
* 在 Java 中,当使用 JDBC(Java Database Connectivity)连接数据库时,需要加载数据库特定的驱动程序,以便与数据库进行通信。加载驱动程序的目的是为了注册驱动程序,使得 JDBC API 能够识别并与特定的数据库进行交互。
* 从JDK6开始,不再需要显式地调用 `Class.forName()` 来加载 JDBC 驱动程序,只要在类路径中集成了对应的jar文件,会自动在初始化时注册驱动程序。
在Java程序中执行 Class.forName("com.mysql.cj.jdbc.Driver") 这一行代码时,以下底层过程会依次发生: 类加载:
1. JVM接收到这个字符串字面量,根据它来查找并加载指定名称的类。类加载是通过类加载器(ClassLoader)体系实现的。通常情况下,系统类加载器(System ClassLoader)负责加载用户类路径(classpath)上的类。类加载包括三个阶段:加载(Loading)、验证(Verification)、准备(Preparation)。在这个过程中,JVM会查找指定类的.class文件(对于MySQL JDBC驱动来说,通常位于jar包中),将其字节码数据读入内存,并进行必要的安全性和正确性检查。
2. 类初始化:在类加载完成后,如果该类尚未被初始化,JVM会触发类的初始化过程。类初始化包括分配静态变量内存、执行静态初始化块(static {})中的代码等步骤。对于 com.mysql.cj.jdbc.Driver 类,其内部通常包含一个静态初始化块或静态方法,在类初次被加载时会被自动执行。这个初始化逻辑的核心任务是向Java的JDBC驱动管理器(java.sql.DriverManager)注册该驱动。
3. 驱动注册:com.mysql.cj.jdbc.Driver 类(或者其他MySQL JDBC驱动类,如旧版的 com.mysql.jdbc.Driver)会实现 java.sql.Driver 接口。按照JDBC规范,实现该接口的类需要在初始化时向 DriverManager 注册自己。注册通常是通过调用 DriverManager.registerDriver() 方法完成的,但在MySQL JDBC驱动中,可能采用更现代的Service Provider Interface (SPI)机制,即在类路径下的 META-INF/services/java.sql.Driver 文件中声明该驱动类。这样,当JDBC API尝试加载驱动时,会自动发现并加载这个文件中列出的实现类。注册过程实质上是将驱动类的一个实例(通常是一个单例或无状态对象)添加到 DriverManager 内部的驱动列表中。这样,当后续调用 DriverManager.getConnection() 方法时,DriverManager 就知道有哪些可用的驱动可以用来尝试建立数据库连接。

我用个生活中的比喻解释一下:
想象你奶奶想打个电话,但是家里没有电话机。为了能打电话,首先需要去买一个电话机,把它正确安装好并插到电话线上。这相当于加载MySQL的驱动程序类。
家里买来的电话机不会自动连到电话线上,必须手动把它安装插好才能用。同样地,刚加载的MySQL驱动也不会自动可用,必须手动将它"注册"到管理所有电话机的系统中。
这个"注册"的过程,就好比你需要拨一个特殊的号码,把新买的电话机报给电话公司,告诉他们:"嘿,我家新装了一个电话机,请把它加入你们的系统里"。
一旦电话公司的系统里有了你家的新电话机,将来就可以用它打电话了。同理,注册后的MySQL驱动也可以被Java程序使用,连接数据库了。
所以那行`Class.forName`代码,相当于你先买回家一个电话机(加载驱动);而在它的静态代码块中,则自动帮你打了个内线电话给电话公司(注册驱动),"嘿,我是新的MySQL电话机,请记录我"。
## 3.2Connection
* Connection接口是JDBC API的重要接口,用于建立与数据库的通信通道。换而言之,Connection对象不为空,则代表一次数据库连接。
* 在建立连接时,需要指定数据库URL、用户名、密码参数。
* *URL:jdbc:mysql://localhost:3306/atguigu*
* *jdbc:mysql://IP地址:端口号/数据库名称?参数键值对1\&参数键值对2*
* `Connection` 接口还负责管理事务,`Connection` 接口提供了 `commit` 和 `rollback` 方法,用于提交事务和回滚事务。
* 可以创建 `Statement` 对象,用于执行 SQL 语句并与数据库进行交互。
* 在使用JDBC技术时,必须要先获取Connection对象,在使用完毕后,要释放资源,避免资源占用浪费及泄漏。
## 3.3Statement
* `Statement` 接口用于执行 SQL 语句并与数据库进行交互。它是 JDBC API 中的一个重要接口。通过 `Statement` 对象,可以向数据库发送 SQL 语句并获取执行结果。
* 结果可以是一个或多个结果。
* 增删改:受影响行数单个结果。
* 查询:单行单列、多行多列、单行多列等结果。
* 但是`Statement` 接口在执行SQL语句时,会产生`SQL注入攻击问题`:
* 当使用 `Statement` 执行动态构建的 SQL 查询时,往往需要将查询条件与 SQL 语句拼接在一起,直接将参数和SQL语句一并生成,让SQL的查询条件始终为true得到结果。
示例代码:
```java
String username = "' OR '1'='1"; // 用户输入的非法用户名
String sql = "SELECT * FROM users WHERE username='" + username + "'";
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);
// 结果集将包含整个 users 表的所有记录
```
在这段代码中,我们动态构建了一个 SQL 查询语句,并将用户输入的`username`直接拼接到 SQL 语句中。但是,用户输入的用户名是一个非法的字符串 `' OR '1'='1`。
当执行 SQL 语句时,实际执行的是:
```sql
SELECT * FROM users WHERE username='' OR '1'='1'
```
由于`'1'='1'`这个条件永远为真,因此该查询会返回 users 表中的所有记录,而不是预期的基于用户名进行过滤的结果。
这种注入攻击发生的根本原因是,用户的输入数据被直接拼接到 SQL 语句中,而没有经过任何过滤和检查。攻击者可以构造特殊的字符串,影响原本的 SQL 语句执行逻辑,从而绕过认证或获取不应有的数据。
这种 SQL 注入攻击不仅可能导致数据泄露,还可能被利用执行任意的 SQL 语句,如删除或修改数据库数据等,造成严重的安全隐患。
因此,在使用 JDBC 时,应当避免使用`Statement`执行动态构建的 SQL 语句,而是使用`PreparedStatement`。`PreparedStatement`允许使用参数占位符,将参数数据与 SQL 语句分离,有效防止 SQL 注入攻击的发生。
dubug演示: 
代码:
```java
package com.rainsoul.base;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.Scanner;
public class JDBCInjection {
public static void main(String[] args) throws Exception {
//1.注册驱动 (可以省略)
//2.获取连接对象
Connection connection = DriverManager.getConnection("jdbc:mysql:///rainsoul", "root", "root");
//3.获取执行SQL语句对象
Statement statement = connection.createStatement();
System.out.println("请输入员工姓名:");
Scanner scanner = new Scanner(System.in);
String name = scanner.nextLine();
//4.编写SQL语句,并执行,接受返回的结果
String sql = "SELECT emp_id,emp_name,emp_salary,emp_age FROM t_emp WHERE emp_name = '"+name+"'";
ResultSet resultSet = statement.executeQuery(sql);
//5.处理结果:遍历resultSet
while(resultSet.next()){
int empId = resultSet.getInt("emp_id");
String empName = resultSet.getString("emp_name");
double empSalary = resultSet.getDouble("emp_salary");
int empAge = resultSet.getInt("emp_age");
System.out.println(empId+"\t"+empName+"\t"+empSalary+"\t"+empAge);
}
//6.释放资源
resultSet.close();
statement.close();
connection.close();
}
}
```
## 3.4 PreparedStatement
* `PreparedStatement`是 `Statement` 接口的子接口,用于执行`预编译`的 SQL 查询,作用如下:
1. 预编译SQL语句:在创建PreparedStatement时,就会预编译SQL语句,也就是SQL语句已经固定。
2. 防止SQL注入:`PreparedStatement` 支持参数化查询,将数据作为参数传递到SQL语句中,采用?占位符的方式,将传入的参数用一对单引号包裹起来'',无论传递什么都作为值。有效防止传入关键字或值导致SQL注入问题。
3. 性能提升:PreparedStatement是预编译SQL语句,同一SQL语句多次执行的情况下,可以复用,不必每次重新编译和解析。

代码:
```java
package com.rainsoul.base;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.Scanner;
public class JDBCPrepared {
public static void main(String[] args) throws Exception{
//1.注册驱动 (可以省略)
//2.获取连接对象
Connection connection = DriverManager
.getConnection("jdbc:mysql:///rainsoul", "root", "root");
//3.获取执行SQL语句对象
PreparedStatement preparedStatement = connection
.prepareStatement("SELECT emp_id,emp_name,emp_salary,emp_age FROM t_emp WHERE emp_name = ?");
System.out.println("请输入员工姓名:");
Scanner scanner = new Scanner(System.in);
String name = scanner.nextLine();
//4.为?占位符复制,并执行SQL语句,接受返回的结果
preparedStatement.setString(1, name);
ResultSet resultSet = preparedStatement.executeQuery();
//5.处理结果:遍历resultSet
while(resultSet.next()){
int empId = resultSet.getInt("emp_id");
String empName = resultSet.getString("emp_name");
double empSalary = resultSet.getDouble("emp_salary");
int empAge = resultSet.getInt("emp_age");
System.out.println(empId+"\t"+empName+"\t"+empSalary+"\t"+empAge);
}
//6.释放资源
resultSet.close();
preparedStatement.close();
connection.close();
}
}
```
## 3.5ResultSet
* `ResultSet`是 JDBC API 中的一个接口,用于表示从数据库中`执行查询语句所返回的结果集`。它提供了一种用于遍历和访问查询结果的方式。
* 遍历结果:ResultSet可以使用 `next()` 方法将游标移动到结果集的下一行,逐行遍历数据库查询的结果,返回值为boolean类型,true代表有下一行结果,false则代表没有。
* 获取单列结果:可以通过getXxx的方法获取单列的数据,该方法为重载方法,支持索引和列名进行获取。
# 4. 基于PreparedStatement实现CRUD
## 4.1查询单行单列
```java
@Test
public void testQuerySingleRowAndCol() throws SQLException {
//1.注册驱动 (可以省略)
//2.获取连接
Connection connection = DriverManager.getConnection("jdbc:mysql:///rainsoul", "root", "root");
//3.预编译SQL语句得到PreparedStatement对象
PreparedStatement preparedStatement = connection.prepareStatement("SELECT COUNT(*) as count FROM t_emp");
//4.执行SQL语句,获取结果
ResultSet resultSet = preparedStatement.executeQuery();
//5.处理结果(如果自己明确一定只有一个结果,那么resultSet最少要做一次next的判断,才能拿到我们要的列的结果)
if(resultSet.next()){
int count = resultSet.getInt("count");
System.out.println(count);
}
//6.释放资源
resultSet.close();
preparedStatement.close();
connection.close();
}
```
## 4.2查询单行多列
```java
@Test
public void testQuerySingleRow()throws Exception{
//1.注册驱动
//2.获取连接
Connection connection = DriverManager.getConnection("jdbc:mysql:///rainsoul", "root", "root");
//3.预编译SQL语句获得PreparedStatement对象
PreparedStatement preparedStatement = connection.prepareStatement("SELECT emp_id,emp_name,emp_salary,emp_age FROM t_emp WHERE emp_id = ?");
//4.为占位符赋值,然后执行,并接受结果
preparedStatement.setInt(1,5);
ResultSet resultSet = preparedStatement.executeQuery();
//5.处理结果
while(resultSet.next()){
int empId = resultSet.getInt("emp_id");
String empName = resultSet.getString("emp_name");
double empSalary = resultSet.getDouble("emp_salary");
int empAge = resultSet.getInt("emp_age");
System.out.println(empId+"\t"+empName+"\t"+empSalary+"\t"+empAge);
}
//6.资源释放
resultSet.close();
preparedStatement.close();
connection.close();
}
```
## 4.3查询多行多列
```java
@Test
public void testQueryMoreRow()throws Exception{
Connection connection = DriverManager.getConnection("jdbc:mysql:///rainsoul", "root", "root");
PreparedStatement preparedStatement = connection.prepareStatement("SELECT emp_id,emp_name,emp_salary,emp_age FROM t_emp WHERE emp_age > ?");
//为占位符赋值,执行SQL语句,接受结果
preparedStatement.setInt(1, 25);
ResultSet resultSet = preparedStatement.executeQuery();
while(resultSet.next()){
int empId = resultSet.getInt("emp_id");
String empName = resultSet.getString("emp_name");
double empSalary = resultSet.getDouble("emp_salary");
int empAge = resultSet.getInt("emp_age");
System.out.println(empId+"\t"+empName+"\t"+empSalary+"\t"+empAge);
}
resultSet.close();
preparedStatement.close();
connection.close();
}
```
## 4.4新增
```java
@Test
public void testInsert() throws SQLException {
Connection connection = DriverManager.getConnection("jdbc:mysql:///rainsoul", "root", "root");
PreparedStatement preparedStatement = connection.prepareStatement("INSERT INTO t_emp(emp_name,emp_salary,emp_age) VALUES (?,?,?)");
preparedStatement.setString(1, "rose");
preparedStatement.setDouble(2,345.67);
preparedStatement.setInt(3,28);
int result = preparedStatement.executeUpdate();
//根据受影响行数,做判断,得到成功或失败
if(result > 0){
System.out.println("成功!");
}else{
System.out.println("失败!");
}
preparedStatement.close();
connection.close();
}
```
## 4.5修改
```java
@Test
public void testUpdate() throws SQLException {
Connection connection = DriverManager.getConnection("jdbc:mysql:///rainsoul", "root", "root");
PreparedStatement preparedStatement = connection.prepareStatement("UPDATE t_emp SET emp_salary = ? WHERE emp_id = ?");
preparedStatement.setDouble(1, 888.88);
preparedStatement.setInt(2, 6);
int result = preparedStatement.executeUpdate();
if(result > 0){
System.out.println("成功!");
}else{
System.out.println("失败!");
}
preparedStatement.close();
connection.close();
}
```
## 4.6删除
```java
@Test
public void testDelete() throws SQLException {
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/rainsoul", "root", "root");
PreparedStatement preparedStatement = connection.prepareStatement("DELETE FROM t_emp WHERE emp_id = ?");
preparedStatement.setDouble(1, 6);
int result = preparedStatement.executeUpdate();
if(result > 0){
System.out.println("成功!");
}else{
System.out.println("失败!");
}
preparedStatement.close();
connection.close();
}
```
# 5.JDBC扩展
## 5.1实体类和ORM
* 在使用JDBC操作数据库时,我们会发现数据都是零散的,明明在数据库中是一行完整的数据,到了Java中变成了一个一个的变量,不利于维护和管理。而我们Java是面向对象的,一个表对应的是一个类,一行数据就对应的是Java中的一个对象,一个列对应的是对象的属性,所以我们要把数据存储在一个载体里,这个载体就是实体类!
* ORM(Object Relational Mapping)思想,**对象到关系数据库的映射**,作用是在编程中,把面向对象的概念跟数据库中表的概念对应起来,以面向对象的角度操作数据库中的数据,即一张表对应一个类,一行数据对应一个对象,一个列对应一个属性!
* 当下JDBC中这种过程我们称其为手动ORM。后续我们也会学习ORM框架,比如MyBatis、JPA等。
新建POJO类: 
```java
//类名和数据库名对应,但是表名一般缩写,类名要全写!
public class Employee {
private Integer empId;//emp_id = empId 数据库中列名用下划线分隔,属性名用驼峰!
private String empName;//emp_name = empName
private Double empSalary;//emp_salary = empSalary
private Integer empAge;//emp_age = empAge
get set...
}
```
封装代码:
```java
@Test
public void testORM() throws SQLException {
// 获取数据库连接
Connection connection = DriverManager
.getConnection("jdbc:mysql:///rainsoul", "root", "root");
// 准备执行SQL语句的预编译语句,用于提高查询效率和防止SQL注入
PreparedStatement preparedStatement = connection
.prepareStatement("select emp_id,emp_name,emp_salary,emp_age from t_emp where emp_id = ?");
// 设置预编译语句中的参数
preparedStatement.setInt(1,1);
// 执行查询并获取结果集
ResultSet resultSet = preparedStatement.executeQuery();
// 初始化Employee对象,用于存放查询结果
Employee employee = null;
// 遍历结果集并将数据赋值给Employee对象
if(resultSet.next()){
employee = new Employee();
int empId = resultSet.getInt("emp_id");
String empName = resultSet.getString("emp_name");
double empSalary = resultSet.getDouble("emp_salary");
int empAge = resultSet.getInt("emp_age");
// 将查询结果映射到Employee对象的属性上
employee.setEmpId(empId);
employee.setEmpName(empName);
employee.setEmpSalary(empSalary);
employee.setEmpAge(empAge);
}
// 打印Employee对象,验证查询和映射结果
System.out.println(employee);
// 关闭结果集、预编译语句和数据库连接,释放资源
resultSet.close();
preparedStatement.close();
connection.close();
}
```
## 5.2主键回显
在数据中,执行新增操作时,主键列为自动增长,可以在表中直观的看到,但是在Java程序中,我们执行完新增后,只能得到受影响行数,无法得知当前新增数据的主键值。在Java程序中获取数据库中插入新数据后的主键值,并赋值给Java对象,此操作为主键回显。
```java
@Test
public void testReturnPK()throws Exception{
//获取连接
Connection connection = DriverManager
.getConnection("jdbc:mysql:///rainsoul", "root", "root");
// 预编译SQL语句,告知preparedStatement,返回新增数据的主键列的值
String sql = "INSERT INTO t_emp(emp_name,emp_salary,emp_age) VALUES (?,?,?)";
PreparedStatement preparedStatement = connection.prepareStatement(sql,Statement.RETURN_GENERATED_KEYS);
// 创建对象,将对象的属性值,填充在?占位符上 (ORM)
Employee employee = new Employee(null, "jack", 123.45, 29);
preparedStatement.setString(1, employee.getEmpName());
preparedStatement.setDouble(2, employee.getEmpSalary());
preparedStatement.setInt(3, employee.getEmpAge());
//执行SQL,并获取返回的结果
int result = preparedStatement.executeUpdate();
ResultSet resultSet = null;
//处理结果
if(result > 0){
System.out.println("成功!");
//获取当前新增数据的主键列,回显到Java中employee对象的empId属性上。
//返回的主键值,是一个单行单列的结果存储在ResultSet里
resultSet = preparedStatement.getGeneratedKeys();
if(resultSet.next()){
int empId = resultSet.getInt(1);
employee.setEmpId(empId);
}
System.out.println(employee);
}else{
System.out.println("失败!");
}
//释放资源
if(resultSet!=null){
resultSet.close();
}
preparedStatement.close();
connection.close();
}
```
## 5.3批量操作
一条一条操作很耗时间:
```java
@Test
public void testReturnPK()throws Exception{
//获取连接
Connection connection = DriverManager
.getConnection("jdbc:mysql:///rainsoul", "root", "root");
// 预编译SQL语句,告知preparedStatement,返回新增数据的主键列的值
String sql = "INSERT INTO t_emp(emp_name,emp_salary,emp_age) VALUES (?,?,?)";
PreparedStatement preparedStatement = connection.prepareStatement(sql,Statement.RETURN_GENERATED_KEYS);
// 创建对象,将对象的属性值,填充在?占位符上 (ORM)
Employee employee = new Employee(null, "jack", 123.45, 29);
preparedStatement.setString(1, employee.getEmpName());
preparedStatement.setDouble(2, employee.getEmpSalary());
preparedStatement.setInt(3, employee.getEmpAge());
//执行SQL,并获取返回的结果
int result = preparedStatement.executeUpdate();
ResultSet resultSet = null;
//处理结果
if(result > 0){
System.out.println("成功!");
//获取当前新增数据的主键列,回显到Java中employee对象的empId属性上。
//返回的主键值,是一个单行单列的结果存储在ResultSet里
resultSet = preparedStatement.getGeneratedKeys();
if(resultSet.next()){
int empId = resultSet.getInt(1);
employee.setEmpId(empId);
}
System.out.println(employee);
}else{
System.out.println("失败!");
}
//释放资源
if(resultSet!=null){
resultSet.close();
}
preparedStatement.close();
connection.close();
}
```
优化后:
```java
@Test
public void testBatch() throws Exception {
//1.注册驱动
// Class.forName("com.mysql.cj.jdbc.Driver");
//2.获取连接
Connection connection = DriverManager.getConnection("jdbc:mysql:///rainsoul?rewriteBatchedStatements=true", "root", "root");
//3.编写SQL语句
/*
注意:1、必须在连接数据库的URL后面追加?rewriteBatchedStatements=true,允许批量操作
2、新增SQL必须用values。且语句最后不要追加;结束
3、调用addBatch()方法,将SQL语句进行批量添加操作
4、统一执行批量操作,调用executeBatch()
*/ String sql = "insert into t_emp (emp_name,emp_salary,emp_age) values (?,?,?)";
//4.创建预编译的PreparedStatement,传入SQL语句
PreparedStatement preparedStatement = connection.prepareStatement(sql);
//获取当前行代码执行的时间。毫秒值
long start = System.currentTimeMillis();
for(int i = 0;i<10000;i++){
//5.为占位符赋值
preparedStatement.setString(1, "marry"+i);
preparedStatement.setDouble(2, 100.0+i);
preparedStatement.setInt(3, 20+i);
preparedStatement.addBatch();
}
//执行批量操作
preparedStatement.executeBatch();
long end = System.currentTimeMillis();
System.out.println("消耗时间:"+(end - start));
preparedStatement.close();
connection.close();
}
```
# 6.连接池
我们每次操作数据库都要获取新连接,使用完毕后就close释放,频繁的创建和销毁造成资源浪费。连接的数量无法把控,对服务器来说压力巨大。
> 连接池就是数据库连接对象的缓冲区,通过配置,由连接池负责创建连接、管理连接、释放连接等操作。
>
> 预先创建数据库连接放入连接池,用户在请求时,通过池直接获取连接,使用完毕后,将连接放回池中,避免了频繁的创建和销毁,同时解决了创建的效率。
>
> 当池中无连接可用,且未达到上限时,连接池会新建连接。
>
> 池中连接达到上限,用户请求会等待,可以设置超时时间。
好的,下面是常见的几种连接池及其简要讲解:
| 连接池 | 描述 |
|-----------------------------|---------------------------------------------------------------------------------------------|
| Apache DBCP | Apache 开发的数据库连接池,实现了连接池的基本管理功能,并对获取连接、使用连接、释放连接等过程进行了简单的封装,是一个老牌、经典的连接池。 |
| C3P0 | 一个开源的JDBC连接池产品,实现了数据源和连接池的功能。它支持JDBC3规范和JDBC2的扩展,包括连接分组、强制断线重连、统计和扩展语句级别等功能。 |
| Tomcat JDBC Connection Pool | Tomcat服务器自带的数据库连接池,可以直接使用或集成到其他服务器环境中。相比Apache DBCP和C3P0,它配置更简单,且内置于Tomcat中,占用资源较少。 |
| HikariCP | 一个轻量级的高性能连接池,它是为了解决Apache DBCP、C3P0等连接池存在的并发性能问题和内存泄漏问题而设计的。相比其他连接池,HikariCP更快、更稳定,资源利用率更高。 |
| Druid | Alibaba开源的一个数据库连接池项目,它包含一个高效的连接池和监控组件。Druid能够提供强大的监控和扩展功能,帮助开发人员快速发现系统中存在的问题。 |
| BoneCP | 一个极简连接池实现,它在性能和低内存占用方面表现良好。BoneCP设计为轻量级并且容易使用,但它缺乏其他连接池提供的一些高级功能。 |
选择合适的连接池要考虑项目的实际需求、性能要求、可维护性等因素。一般来说,HikariCP和Druid因为性能和功能方面的出色表现,使用较为广泛。Apache DBCP和C3P0作为传统的连接池也有一定使用。不同场景下,可以根据具体情况选择适合的连接池产品。
## 6.1Druid连接池使用
首先要引入jar包。

```java
/**
* 测试通过硬编码方式使用Druid连接池。
* 该方法展示了如何直接在代码中配置Druid连接池,并使用该连接池获取数据库连接,
* 进行操作后,再将连接归还给连接池。
*
* @throws SQLException 如果操作数据库连接时发生错误,则抛出SQLException。
*/
@Test
public void testHardCodeDruid() throws SQLException {
/*
硬编码:将连接池的配置信息和Java代码耦合在一起。
1、创建DruidDataSource连接池对象。
2、设置连接池的配置信息【必须 | 非必须】
3、通过连接池获取连接对象
4、回收连接【不是释放连接,而是将连接归还给连接池,给其他线程进行复用】
*/
//1.创建DruidDataSource连接池对象。
DruidDataSource druidDataSource = new DruidDataSource();
//2.设置连接池的配置信息【必须 | 非必须】
//2.1 必须设置的配置
druidDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
druidDataSource.setUrl("jdbc:mysql:///rainsoul");
druidDataSource.setUsername("root");
druidDataSource.setPassword("root");
//2.2 非必须设置的配置,例如初始化大小和最大活动连接数
druidDataSource.setInitialSize(10);
druidDataSource.setMaxActive(20);
//3.通过连接池获取连接对象
Connection connection = druidDataSource.getConnection();
System.out.println(connection);
//基于connection进行数据库操作,例如CRUD
//4.回收连接,将使用完毕的连接归还给连接池
connection.close();
}
```
使用配置文件: 
```java
/**
* 测试通过Druid连接池获取数据库连接的功能
* 本测试方法不接受参数,也不返回任何值
* @throws Exception 抛出异常的条件:读取配置文件或获取数据库连接时可能发生的任何异常
*/
@Test
public void testResourcesDruid() throws Exception {
// 创建Properties对象用于存放配置信息
Properties properties = new Properties();
// 从类路径下读取db.properties配置文件,并加载到Properties对象中
InputStream inputStream = DruidTest.class.getClassLoader()
.getResourceAsStream("db.properties");
properties.load(inputStream);
// 使用DruidDataSourceFactory和配置信息创建DruidDataSource连接池
DataSource dataSource = DruidDataSourceFactory.createDataSource(properties);
// 从连接池中获取一个数据库连接
Connection connection = dataSource.getConnection();
System.out.println(connection);
// 此处是开发CRUD操作的代码位置,当前代码未实现具体操作
// 使用完毕后关闭数据库连接,释放资源
connection.close();
}
```
## 6.2Hikari连接池的使用
```java
@Test
public void testHardCodeHikari()throws Exception{
/*
硬编码:将连接池的配置信息和Java代码耦合在一起
1、创建HikariDataSource连接池对象
2、设置连接池的配置信息【必须 | 非必须】
3、通过连接池获取连接对象
4、回收连接
*/ //1.创建HikariDataSource连接池对象
HikariDataSource hikariDataSource = new HikariDataSource();
//2.设置连接池的配置信息【必须 | 非必须】
//2.1 必须设置的配置
hikariDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
hikariDataSource.setJdbcUrl("jdbc:mysql:///rainsoul");
hikariDataSource.setUsername("root");
hikariDataSource.setPassword("root");
//2.2 非必须设置的配置
hikariDataSource.setMinimumIdle(10);
hikariDataSource.setMaximumPoolSize(20);
//3.通过连接池获取连接对象
Connection connection = hikariDataSource.getConnection();
System.out.println(connection);
//4.回收连接
connection.close();
}
```
使用配置文件: 
```java
@Test
public void testResourcesHikari() throws Exception {
//1.创建Properties集合,用于存储外部配置文件的key和value值。
Properties properties = new Properties();
//2.读取外部配置文件,获取输入流,加载到Properties集合里。
InputStream inputStream = HikariTest.class.getClassLoader()
.getResourceAsStream("hikari.properties");
properties.load(inputStream);
//3.创建HikariConfig连接池配置对象,将Properties集合传进去。
HikariConfig hikariConfig = new HikariConfig(properties);
//4.基于HikariConfig连接池配置对象,构建HikariDataSource
HikariDataSource hikariDataSource = new HikariDataSource(hikariConfig);
//5.获取连接
Connection connection = hikariDataSource.getConnection();
System.out.println(connection);
//6.回收连接
connection.close();
}
```
## 6.3常见的参数配置
| 参数名称 | 描述 |
|-------------------------------|------------------------------------------|
| initialSize | 连接池初始连接数量,默认值通常为0 |
| maxActive | 连接池在同一时间能够分配的最大活跃连接数,使用负值表示不限制 |
| maxIdle | 连接池中最大空闲连接数,控制池中有多少空闲连接可以存活 |
| minIdle | 连接池中最小空闲连接数,低于这个数量时,连接池会创建新的连接 |
| maxWait | 当没有可用连接时,连接池等待连接被归还的最大时间(以毫秒计),超过时间则抛出异常 |
| maxAge | 连接池中连接能够存活的最长时间(以毫秒计),超过时间将被释放 |
| testOnBorrow | 在将连接借出时是否测试连接的有效性,可避免将无效连接分配出去 |
| testOnReturn | 在将连接归还到池中时是否测试连接的有效性,可避免将无效连接存入池中 |
| testWhileIdle | 是否对空闲连接进行有效性检测,可避免连接由于长期空闲而失效 |
| timeBetweenEvictionRunsMillis | 空闲连接检测线程的运行周期时间,用于控制空闲连接检测的频率 |
| validationQuery | 用于检测连接是否有效的SQL查询语句,如 "SELECT 1" |
| removeAbandonedTimeout | 连接长时间无操作时,被视为已经被废弃的超时时间(以秒计) |
| logAbandoned | 是否记录长时间无操作而被废弃的连接信息 |
根据不同的连接池实现,可配置参数的名称可能有所不同,但基本含义是相似的。正确配置这些参数可以合理控制连接池的大小、连接的生命周期、资源利用率等,从而优化连接池的性能和资源占用。
# 7.JDBC优化及工具类封装
## 7.1JDBC工具类(V1.0):
1. 维护一个连接池对象。
2. 对外提供在连接池中获取连接的方法
3. 对外提供回收连接的方法 注意:**工具类仅对外提供共性的功能代码,所以方法均为静态方法!**
这段代码实现了一个JDBC工具类 `JDBCUtil`,用于管理数据库连接池和获取/释放连接。我将为每部分代码添加注释进行解释。
```java
public class JDBCUtil {
// 创建连接池引用,因为要提供给当前项目的全局使用,所以创建为静态的。
private static DataSource dataSource;
// 在项目启动时,即创建连接池对象,赋值给dataSource
static {
try {
// 创建一个Properties对象,用于加载配置文件
Properties properties = new Properties();
// 获取配置文件的输入流
InputStream inputStream = JDBCUtil.class.getClassLoader().getResourceAsStream("db.properties");
// 加载配置文件
properties.load(inputStream);
// 使用Druid连接池工厂类创建连接池对象,并传入配置文件参数
dataSource = DruidDataSourceFactory.createDataSource(properties);
} catch (Exception e) {
// 如果创建连接池出现异常,则抛出运行时异常
throw new RuntimeException(e);
}
}
// 对外提供在连接池中获取连接的方法
public static Connection getConnection() {
try {
// 从连接池中获取一个连接
return dataSource.getConnection();
} catch (SQLException e) {
// 如果获取连接出现异常,则抛出运行时异常
throw new RuntimeException(e);
}
}
// 对外提供回收连接的方法
public static void release(Connection connection) {
try {
// 关闭连接,将其归还到连接池中
connection.close();
} catch (SQLException e) {
// 如果关闭连接出现异常,则抛出运行时异常
throw new RuntimeException(e);
}
}
}
```
这个工具类主要实现了以下功能:
1. 在静态代码块中,通过读取配置文件 `db.properties` 获取数据库连接参数,并使用 Druid 连接池工厂类创建一个连接池对象 `dataSource`。
2. 提供一个静态方法 `getConnection()`,用于从连接池中获取一个数据库连接。
3. 提供一个静态方法 `release(Connection connection)`,用于关闭指定的数据库连接,将其归还到连接池中。
使用这个工具类,可以方便地获取和释放数据库连接,同时利用连接池技术提高连接的复用率和性能。需要注意的是,在项目启动时就创建了连接池对象,并在静态代码块中加载了配置文件,这种方式确保了连接池的初始化只执行一次。
## 7.2ThreadLocal
用一个生活中的例子来解释什么是`ThreadLocal`以及它的使用场景。
我们可以把`ThreadLocal`想象成一个小柜子,每个人都有自己专属的小柜子。这个小柜子就相当于每个线程内部的存储空间。
假设你奶奶有几个孙子孙女,他们都很顽皮,经常在家里到处乱跑。为了防止他们弄乱房间,你给每个孩子准备了一个专属的小柜子,让他们把自己的东西放进去。
这样一来,每个孩子拿自己柜子里的东西就不会影响其他孩子,也不会把房间弄乱了。这就是`ThreadLocal`的作用,它为每个"线程"提供了一个独立的存储空间,可以在里面放自己的东西,而不会影响到其他"线程"。
在程序中,多个线程就像你奶奶家的这些孩子一样,他们并发地运行,有可能会争夺一些共享的资源(比如数据库连接对象)。如果多个线程使用同一个资源,就可能会导致线程安全问题。
通过`ThreadLocal`,每个线程就拥有了自己独立的"小柜子",可以放置自己的连接对象。当线程需要使用连接时,从自己的"小柜子"里取出来用;用完后,又放回到"小柜子"中,不会影响其他线程。
这种做法避免了线程之间相互影响和频繁地从连接池中获取连接,提高了程序的运行效率和线程安全性。
当然,我们要记得在线程结束后,将"小柜子"里的东西清理干净,避免浪费资源。否则就像孩子长大后忘记把自己的旧东西清理出柜子一样,会导致资源的浪费和积累。
总之,`ThreadLocal`就像是为每个线程准备了一个"独立的小空间",保证了线程之间使用共享资源时的隔离性和安全性。只要用好了,就可以让程序运行得更高效和可靠。
ThreadLocal的相关知识点整理如下:
1. **ThreadLocal概念**
* ThreadLocal是JDK包中的一个类,可以在同一个线程内创建独立的副本变量。
* 每个线程都会有一个自己独立的副本变量,线程之间彼此不会相互影响。
* ThreadLocal提供get()、set(value)、remove()等方法访问和修改副本变量。
2. **ThreadLocal原理**
* 每个Thread内部都有一个ThreadLocalMap类型的成员变量,存储当前线程的副本变量。
* ThreadLocal作为Map的键(key),副本变量作为Map的值(value)存储。
* 每个线程读写自己所属线程的变量副本,线程之间相互隔离。
3. **ThreadLocal使用场景**
* 线程本地存储(每个线程内部有自己独立的变量副本)
* 事务管理(绑定事务上下文)
* 日志记录(MDC机制存储日志跟踪信息)
* 数据库连接池(每个线程有独立的连接对象)
4. **ThreadLocal使用注意事项**
* 每个线程自己使用后,需要调用remove()清除副本,避免内存泄漏。
* 副本变量是存储在线程对象内部的Map中,生存周期随线程终止而销毁。
* 减小副本变量的范围,使用后再强制设为null,避免副本变量带来的额外内存占用。
* 只有在状态确实需要与线程相关联时,才使用ThreadLocal存储状态。
5. **ThreadLocal应用实例**
* JDBC中使用ThreadLocal获取线程独立的Connection对象
* Servlet组件绑定和传递Request、Response等对象
* 实现简化的线程上下文管理器(TransmittableThreadLocal)
6. **ThreadLocal内存泄漏风险**
* 由于ThreadLocalMap的生存周期是与线程一直存在的,若不手动remove,就会导致内存泄漏。
* 使用弱引用或定期删除无效线程变量,防止内存泄漏。
* 在线程池中注意每次运行结束后,清理ThreadLocal存储。
## JDBCUtilV2工具类(V2.0)
这个 `JDBCUtilV2` 类是在原有 `JDBCUtil` 类的基础上进行了改进,增加了 `ThreadLocal` 的支持。我将为这部分新增的代码添加注释进行解释。
```java
public class JDBCUtilV2 {
// ...
// 创建一个ThreadLocal对象,用于存储线程级别的Connection对象
private static ThreadLocal
万字JDBC教程
Hiramrain2024-04-14 18:04
相关推荐
铲子Zzz10 分钟前
Java使用接口AES进行加密+微信小程序接收解密霖檬ing15 分钟前
K8s——配置管理(1)float_六七35 分钟前
SQL六大核心类别全解析Vic101011 小时前
Java 开发笔记:多线程查询逻辑的抽象与优化Biaobiaone1 小时前
Java中的生产消费模型解析特立独行的猫a1 小时前
11款常用C++在线编译与运行平台推荐与对比louisgeek2 小时前
Java 位运算Code季风2 小时前
将 gRPC 服务注册到 Consul:从配置到服务发现的完整实践(上)hweiyu002 小时前
Maven 私库Boilermaker19922 小时前
【Java EE】SpringIoC