1. JDBC
概述
JDBC
[Java Database Connectivity
]是 Java
语言中用于连接和操作数据库的一套标准 API
。它允许 Java
程序通过统一的方式与各种关系型数据库,如 MySQL、Oracle、SQL Server
等交互,执行 SQL
语句并处理结果。
1.1 JDBC
原理
JDBC
的核心原理是【通过统一的接口层屏蔽不同数据库的差异】,让Java
程序能以标准化的方式与各种数据库交互。其核心设计基于分层架构实现,如下:
- 接口层【
JDBC API
】:Java
定义了一套标准接口如Connection
、Statement
、ResultSet
,开发者只需调用这些接口的方法。这一套统一规范由SUN
公司提供,命名为JDBC
。 - 实现层【
JDBC Driver
】:JDBC
只是一堆接口,而JDBC
才是接口的实现,数据库驱动才能让应用程序连接和操作特定数据库,其原理就是将JDBC API
的调用转换为数据库能理解的指令。数据库驱动就是各个数据库厂商提供的JDBC
实现类而已。
1.2 JDBC
的作用
JDBC
接口是 Java
语言中用于连接和操作数据库的核心 API
,它通过标准化接口与实现分离的设计,在开发中起到【统一数据库操作、屏蔽底层差异】的关键作用。
- 提供统一的数据库操作
API
:JDBC
定义了一套标准接口,开发者只需学习这一套API
,即可操作任意支持JDBC
的数据库。 - 解耦业务逻辑与数据库实现
- 开发者面向接口编程:业务代码依赖
JDBC
接口,而非具体数据库的实现。 - 数据库切换透明化:更换数据库时,只需修改驱动和连接
URL
,无需重写业务逻辑。
- 开发者面向接口编程:业务代码依赖
- 管理数据库连接与资源:
JDBC
接口规范了资源的生命周期管理如连接的建立、关闭,并通过AutoCloseable
支持自动释放资源,避免内存泄漏。 - 支持安全、高效的数据库操作
- 防
SQL
注入:通过PreparedStatement
接口实现参数化查询。 - 批量处理优化:通过
addBatch()
和executeBatch()
提升批量操作性能。
- 防
- 事务管理:通过
Connection
接口管理事务,确保数据一致性。
1.3 模拟JDBC
接口
首先需要制定接口,模拟SUN
公司,如下:
package com.person;
interface JDBC {
// 负责连接数据库的方法
void getConnection();
}
其次就是接口的实现,即各个驱动,如下:
// Mysql驱动
package com.person;
public class MySQLDriver implements JDBC{
@Override
public void getConnection() {
System.out.println("你已连接MySQL数据库");
}
}
// Oracle驱动
package com.person;
public class OracleDriver implements JDBC{
@Override
public void getConnection() {
System.out.println("你已连接MySQL数据库");
}
}
在完成上述操作后就可以操作数据库了,如下:
package com.person;
public class Client {
public static void main(String[] args) {
// 操作MySQL数据库
JDBC jdbc = new MySQLDriver();
// 操作Oracle数据库
// JDBC jdbc = new OracleDriver();
jdbc.getConnection();
}
}
很明显上述代码违背开发原则中的OCP
原则,如果想要实现OCP
原则,可以将创建对象的任务交给反射机制,将类名配置到配置文件中即可,如下:
// jdbc.properties
jdbc.driver=MySQLDriver
import java.util.ResourceBundle;
public class Client{
public static void main(String[] args) throws Exception{
String driverClassName = ResourceBundle.getBundle("jdbc").getString("jdbc.driver");
Class c = Class.forName(driverClassName);
JDBC jdbc = (JDBC)c.newInstance();
jdbc.getConnection();
}
}
最终通过修改jdbc.properties
文件即可做到数据库的切换,这样就做到了驱动和实现的耦合。
1.4 MySQL
驱动的下载
驱动是各个厂商自己编写的JDBC
实现类,因此需要前往官网下载,以MySQL
为例,如下:
2. JDBC
编程的基本实现
2.1 实现步骤
JDBC
编程的步骤是很固定的,通常包含以下六步:
-
注册驱动
- 将
JDBC
驱动程序从硬盘上的文件系统中加载到内存中。 - 使得
DriverManager
可以通过一个统一的接口来管理该驱动程序的所有连接操作。
- 将
-
获取数据库连接:获取
java.sql.Connection
对象,该对象的创建标志着mysql
进程和jvm
进程之间的通道打开。 -
获取数据库操作对象:获取
java.sql.Statement
对象,该对象负责将SQL
语句发送给数据库,数据库负责执行该SQL
语句。 -
执行
SQL
语句:执行具体的SQL
语句,例如insert delete update select
等。 -
处理查询结果集
- 如果之前的操作是
SQL
查询语句,才会有处理查询结果集这一步。 - 执行
SQL
语句通常会返回查询结果集对象java.sql.ResultSet
。 - 对于
ResultSet
查询结果集来说,通常的操作是针对查询结果集进行结果集的遍历。
- 如果之前的操作是
-
释放资源
- 释放资源可以避免资源的浪费。在
JDBC
编程中,每次使用完Connection
、Statement
、ResultSet
等资源后,都需要显式地调用对应的close()
方法来释放资源,避免资源的浪费。 - 释放资源可以避免出现内存泄露问题。在
Java
中,当一个对象不再被引用时,会被JVM
的垃圾回收机制进行回收。但是在JDBC
编程中,如果不显式地释放资源,那么这些资源就不会被JVM
的垃圾回收机制自动回收,从而导致内存泄露问题。
- 释放资源可以避免资源的浪费。在
2.2 代码实现
package com.person;
import java.sql.*;
public class Client {
public static void main(String[] args){
Statement stmt = null;
Connection conn = null;
try {
// 注册驱动
Driver driver = new com.mysql.cj.jdbc.Driver();
DriverManager.registerDriver(driver);
// 获取连接
String url = "jdbc:mysql://localhost:3306/jdbc";
String user = "root";
String password = "20041104HT";
conn = DriverManager.getConnection(url, user, password); // com.mysql.cj.jdbc.ConnectionImpl@f79e
// 获取数据库操作对象 -- 可以创建多个
stmt = conn.createStatement(); // com.mysql.cj.jdbc.StatementImpl@4c309d4d
// 执行SQL语句
String sql = "INSERT INTO user(name, password, relname, gender, tel) VALUES('王五', '123654', '龙或', '男', '130')";
// boolean isSuccess = stmt.execute(sql); // 很少用
// System.out.println(isSuccess ? "插入数据成功" : "插入数据失败");
int count = stmt.executeUpdate(sql); // 凡是涉及 增、删、改的操作使用该方法,返回值为数据发生变化的条目
System.out.println(count == 1 ? "插入数据成功" : "插入数据失败");
} catch (SQLException e) {
e.printStackTrace();
} finally {
// 释放资源 从小到大进行释放
// 每一个都应该try-catch, 否则会导致后面的代码不执行
if (stmt != null) {
try{
stmt.close();
}catch (SQLException e){
e.printStackTrace();
}
}
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
}
2.3 URL
格式
JDBC URL
是在使用 JDBC
连接数据库时的一个 URL
字符串,它用来标识要连接的数据库的位置、认证信息和其他配置参数等。JDBC URL
的格式因数据库类型而异,完整格式如jdbc:mysql://<host>:<port>/<database_name>?<connection_parameters>
:
-
协议:表示要使用的数据库管理系统
DBMS
的类型,如jdbc:mysql
表示要使用MySQL
数据库,jdbc:postgresql
表示要使用PostgreSQL
数据库。 -
主机地址和端口号:表示要连接的数据库所在的服务器的
IP
地址或域名,以及数据库所在服务器监听的端口号。 -
数据库名称:表示要连接的数据库的名称。
-
其他可选参数:这些参数包括连接的超时时间、使用的字符集、连接池相关配置等。
-
serverTimezone
:设置服务器的时区,默认为UTC
,可以通过该参数来指定客户端和服务器的时区。jdbc:mysql://localhost:3306/jdbc?user=root&password=666&serverTimezone=America/Los_Angeles
-
useSSL
:是否使用SSL
进行连接,默认为true
。 -
useUnicode
:是否使用Unicode
编码进行数据传输,默认是true
。useUnicode
设置的是数据在传输过程中是否使用Unicode
编码方式。 -
characterEncoding
:连接使用的字符编码,默认为UTF-8
。characterEncoding
设置的是数据被传输到服务器之后,服务器采用哪一种字符集进行编码。jdbc:mysql://localhost:3306/jdbc?useUnicode=true&serverTimezone=Asia/Shanghai&useSSL=true&characterEncoding=utf-8
-
JDBC URL
是连接数据库的关键,通过 JDBC URL
,应用程序可以通过特定的JDBC
驱动程序与数据库服务器进行通信,从而实现与数据库的交互。在开发 Web
应用和桌面应用时,使用 JDBC URL
可以轻松地连接和操作各种类型的数据库,例如 MySQL、PostgreSQL、Oracle
等。
2.4 注册驱动的两种方式
在上述代码中,注册驱动的方式如下:
Driver driver = new com.mysql.cj.jdbc.Driver();
DriverManager.registerDriver(driver);
可以看一下Driver
类的源码,如下:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package com.mysql.cj.jdbc;
import java.sql.DriverManager;
import java.sql.SQLException;
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
public Driver() throws SQLException {
}
static {
try {
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException("Can't register driver!");
}
}
}
从源码中不难发现,Driver
类的静态代码块已经实现了注册驱动,也就是说,我们只需要让该代码块执行即可实现注册驱动,可以使用forName
实现,如下:
Class.forName("com.mysql.cj.jdbc.Driver");
【注意】从JDBC 4.0
即java 6
开始,驱动注册就不再需要手动完成了,由系统自动完成。但是推荐手动注册,因为有一些数据库驱动程序不支持自动注册。
2.5 动态配置连接数据库的信息
为了避免在程序中出现硬编码,符合OCP
原则,建议将数据库的配置信息配置到属性文件中,如下:
// jdbc.properties
driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/jdbc?
useUnicode=true&serverTimezone=Asia/Shanghai&useSSL=true&characterEncoding=utf-8
user=root
password=666
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Connection;
import java.sql.Statement;
import java.util.ResourceBundle;
public class JDBCTest04 {
public static void main(String[] args){
// 通过以下代码获取属性文件中的配置信息
ResourceBundle bundle = ResourceBundle.getBundle("jdbc");
String driver = bundle.getString("driver");
String url = bundle.getString("url");
String user = bundle.getString("user");
String password = bundle.getString("password");
Connection conn = null;
Statement stmt = null;
try {
// 1. 注册驱动
Class.forName(driver);
// 2. 获取连接
conn = DriverManager.getConnection(url, user, password);
// 3. 获取数据库操作对象
stmt = conn.createStatement();
// 4. 执行SQL语句
String sql = "insert into user(name,password,relname,gender,tel) values('bruce','123','布鲁斯','男','12566568956')";
int count = stmt.executeUpdate(sql);
System.out.println("添加数据成功");
} catch(SQLException | ClassNotFoundException e){
e.printStackTrace();
} finally {
// 6. 释放资源
if(stmt != null){
try{
stmt.close();
}catch(SQLException e){
e.printStackTrace();
}
}
if(conn != null){
try{
conn.close();
}catch(SQLException e){
e.printStackTrace();
}
}
}
}
}
2.6 获取连接的其它方式
在前面的案例中,是使用的如下方法连接的数据库:
Connection conn = DriverManager.getConnection(url, user, password);
当然还有其它方式获取数据库连接,如下:
-
getConnection(String url)
:参数仅需传递一个url
,用户名、密码等配置参数都需要配置在url
中。import java.sql.Driver; import java.sql.DriverManager; import java.sql.SQLException; import java.sql.Connection; public class JDBCTest05 { public static void main(String[] args){ try { // 1. 注册驱动 Class.forName("com.mysql.cj.jdbc.Driver"); // 2. 获取连接 String url = "jdbc:mysql://localhost:3306/jdbc?user=root&password=666"; Connection conn = DriverManager.getConnection(url); System.out.println(conn); } catch(SQLException|ClassNotFoundException e){ e.printStackTrace(); } } }
-
getConnetion(String url, Properties info)
:需传递两个参数,一个是url
,一个是Properties
对象。import java.sql.Driver; import java.sql.DriverManager; import java.sql.SQLException; import java.sql.Connection; import java.util.Properties; public class JDBCTest06 { public static void main(String[] args){ try { // 1. 注册驱动 Class.forName("com.mysql.cj.jdbc.Driver"); // 2. 获取连接 String url = "jdbc:mysql://localhost:3306/jdbc"; Properties info = new Properties(); info.setProperty("user", "root"); info.setProperty("password", "666"); info.setProperty("useUnicode", "true"); info.setProperty("serverTimezone", "Asia/Shanghai"); info.setProperty("useSSL", "true"); info.setProperty("characterEncoding", "utf-8"); Connection conn = DriverManager.getConnection(url, info); System.out.println(conn); } catch(SQLException|ClassNotFoundException e){ e.printStackTrace(); } } }
3. JDBC
常用操作
3.1 查询操作
查询数据信息可以通过列索引和列名两种方式,一般使用后者,通过列索引的方式代码可读性太差。在JDBC
中,索引都是从1
开始的。
package com.person;
import java.sql.*;
public class Client {
public static void main(String[] args){
Statement stmt = null;
Connection conn = null;
// 查询结果集
ResultSet result = null;
try {
// 注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
// 获取连接
String url = "jdbc:mysql://localhost:3306/jdbc";
String user = "root";
String password = "20041104HT";
conn = DriverManager.getConnection(url, user, password);
stmt = conn.createStatement();
// 查询操作
String sql = "SELECT id,name,password FROM user";
result = stmt.executeQuery(sql); // com.mysql.cj.jdbc.result.ResultSetImpl@3cc1435c
while(result.next()) {
// 以指定类型获取数据
// 如果以Date类型获取数据,返回的是java.sql.Date
int id = result.getInt("id");
String name = result.getString("name");
String pwd = result.getString("password");
System.out.println(id + name + pwd);
}
} catch (SQLException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
} finally {
if (result != null) {
try{
result.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();
}
}
}
}
}
3.2 查询元数据信息
ResultSetMetaData
是一个接口,用于描述 ResultSet
中的元数据信息,即查询结果集的结构信息,例如查询结果集中包含了哪些列,每个列的数据类型、长度、标识符等。ResultSetMetaData
可以通过 ResultSet
接口的 getMetaData()
方法获取,一般在对 ResultSet
进行元数据信息处理时使用。
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Connection;
import java.sql.Statement;
import java.util.ResourceBundle;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
public class JDBCTest12 {
public static void main(String[] args){
// 通过以下代码获取属性文件中的配置信息
ResourceBundle bundle = ResourceBundle.getBundle("jdbc");
String driver = bundle.getString("driver");
String url = bundle.getString("url");
String user = bundle.getString("user");
String password = bundle.getString("password");
Connection conn = null;
Statement stmt = null;
ResultSet rs = null;
try {
Class.forName(driver);
conn = DriverManager.getConnection(url, user, password);
stmt = conn.createStatement();
String sql = "select id,name,price,create_time as createTime from t_product";
rs = stmt.executeQuery(sql);
// 获取元数据信息
ResultSetMetaData rsmd = rs.getMetaData();
// 获取元数据的个数
int columnCount = rsmd.getColumnCount();
for (int i = 1; i <= columnCount; i++) {
System.out.println(rsmd.getColumnName(i)); // 列名
System.out.println(rsmd.getColumnTypeName(i)); // 列的数据类型
System.out.println(rsmd.getColumnDisplaySize(i)); // 列的显示长度
}
} catch(SQLException | ClassNotFoundException e){
e.printStackTrace();
} finally {
// 6. 释放资源
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();
}
}
}
}
}
3.3 获取新增行的主键值
大多数表的主键字段值都是自增的,在某些特殊的业务环境下,当我们插入了新数据后,希望能够获取到这条新数据的主键值,应该如何获取呢?在 JDBC
中,如果要获取插入数据后的主键值,可以使用 Statement
接口的 executeUpdate()
方法的重载版本,该方法接受一个额外的参数,用于指定是否需要获取自动生成的主键值。然后,通过以下两个步骤获取插入数据后的主键值:
-
在执行
executeUpdate()
方法时指定一个标志位,表示需要返回插入的主键值。 -
调用
Statement
对象的getGeneratedKeys()
方法,返回一个包含插入的主键值的ResultSet
对象。import java.sql.DriverManager; import java.sql.SQLException; import java.sql.Connection; import java.sql.Statement; import java.util.ResourceBundle; import java.sql.ResultSet; public class JDBCTest13 { public static void main(String[] args){ // 通过以下代码获取属性文件中的配置信息 ResourceBundle bundle = ResourceBundle.getBundle("jdbc"); String driver = bundle.getString("driver"); String url = bundle.getString("url"); String user = bundle.getString("user"); String password = bundle.getString("password"); Connection conn = null; Statement stmt = null; ResultSet rs = null; try { // 1. 注册驱动 Class.forName(driver); // 2. 获取连接 conn = DriverManager.getConnection(url, user, password); // 3. 获取数据库操作对象 stmt = conn.createStatement(); // 4. 执行SQL语句 String sql = "insert into user(name,password,realname,gender,tel) values('zhangsan','111','张三','男','19856525352')"; // 第一步 int count = stmt.executeUpdate(sql, Statement.RETURN_GENERATED_KEYS); // 第二步 rs = stmt.getGeneratedKeys(); if(rs.next()){ int id = rs.getInt(1); System.out.println("新增数据行的主键值: " + id); } } catch(SQLException | ClassNotFoundException e){ e.printStackTrace(); } finally { // 6. 释放资源 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(); } } } } }
3.4 封装DbUtils
将 JDBC
封装为类似 DbUtils
的工具类可以大幅简化数据库操作代码,减少重复性工作,如资源释放、异常处理等。
package com.powernode.jdbc;
import java.sql.*;
import java.util.ResourceBundle;
public class DbUtils {
private static String url;
private static String user;
private static String password;
static {
// 读取属性资源文件
ResourceBundle bundle = ResourceBundle.getBundle("com.powernode.jdbc.jdbc");
String driver = bundle.getString("driver");
url = bundle.getString("url");
user = bundle.getString("user");
password = bundle.getString("password");
// 注册驱动
try {
Class.forName(driver);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
/**
* 获取数据库连接
* @return
* @throws SQLException
*/
public static Connection getConnection() throws SQLException {
Connection conn = DriverManager.getConnection(url, user, password);
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) {
throw new RuntimeException(e);
}
}
if (stmt != null) {
try {
stmt.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
}
4. SQL
注入
4.1 原理
SQL
注入【SQL Injection
】 是一种针对数据库的安全漏洞攻击技术。攻击者通过向应用程序的输入字段如表单、URL
参数等注入恶意构造的 SQL
代码,从而操纵后端数据库执行非预期的 SQL
命令。这种攻击可能导致数据泄露、数据篡改、账户越权,甚至数据库完全被控制。其原理如下:
- 输入拼接漏洞:如果应用程序直接将用户输入拼接到
SQL
语句中,未做任何过滤或转义,攻击者可通过输入特殊字符如'
、"
、;
、--
改变SQL
语句的逻辑。 - 恶意代码执行:攻击者注入的 SQL 代码会被数据库解析执行,例如绕过登录验证、删除或修改数据库表、窃听敏感数据。
如下代码用来模拟SQL
注入:
package com.person;
import java.sql.*;
import java.util.ResourceBundle;
import java.util.Scanner;
/**
* 用户登录案例演示SQL注入问题
*/
public class Client {
public static void main(String[] args) {
// 输出欢迎页面
System.out.println("欢迎使用用户管理系统, 请登录!");
// 接收用户名和密码
Scanner scanner = new Scanner(System.in);
System.out.print("用户名:");
String loginName = scanner.nextLine();
System.out.print("密码:");
String loginPwd = scanner.nextLine();
// 读取属性配置文件,获取连接数据库的信息。
ResourceBundle bundle = ResourceBundle.getBundle("jdbc");
String driver = bundle.getString("jdbc.driver");
String url = bundle.getString("url");
String user = bundle.getString("user");
String password = bundle.getString("password");
// JDBC程序验证用户名和密码是否正确
Connection conn = null;
Statement stmt = null;
ResultSet rs = null;
try {
// 1.注册驱动
Class.forName(driver);
// 2.获取连接
conn = DriverManager.getConnection(url, user, password);
// 3.获取数据库操作对象
stmt = conn.createStatement();
// 4.执行SQL语句
String sql = "select relname from user where name = '"+loginName+"' and password = '"+loginPwd+"'";
rs = stmt.executeQuery(sql);
// 5.处理查询结果集
if (rs.next()) {
String relname = rs.getString("relname");
System.out.println("登录成功, 欢迎您, " + relname);
} else {
System.out.println("登录失败, 用户名不存在或者密码错误");
}
} catch (ClassNotFoundException | SQLException e) {
throw new RuntimeException(e);
} finally {
// 6.释放资源
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
if (stmt != null) {
try {
stmt.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
}
}
当我输入如下指令时,发现也能成功登录,即使数据库中并不存在该用户信息。
欢迎使用用户管理系统, 请登录!
用户名:王五
密码:aaa' or '1==1 --
登录成功, 欢迎您, 吉吉
4.2 解决SQL
注入
从上述代码中不难发现,导致SQL
注入的根本原因就是Statement
造成的,Statement
先进行字符串的拼接,然后将拼接好的字符串发送给SQL
服务器编译执行。
因此JDBC
为了解决这个问题,引入了PreparedStatement
接口,即预编译的数据库操作对象。PreparedStatement
先对SQL
语句进行预编译,确定其语法结构,然后再向SQL
语句的指定位置传值,这样就算用户传递的信息中包括SQL
关键字,也只会被当作一个值传递给SQL
语句,而不会再参与SQL
语句的编译。
package com.person;
import java.sql.*;
import java.util.ResourceBundle;
import java.util.Scanner;
/**
* 用户登录案例演示SQL注入问题
*/
public class Client {
public static void main(String[] args) {
System.out.println("欢迎使用用户管理系统, 请登录!");
Scanner scanner = new Scanner(System.in);
System.out.print("用户名:");
String loginName = scanner.nextLine();
System.out.print("密码:");
String loginPwd = scanner.nextLine();
ResourceBundle bundle = ResourceBundle.getBundle("jdbc");
String driver = bundle.getString("jdbc.driver");
String url = bundle.getString("url");
String user = bundle.getString("user");
String password = bundle.getString("password");
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
Class.forName(driver);
conn = DriverManager.getConnection(url, user, password);
String sql = "SELECT * FROM user WHERE name=? AND password=?";
pstmt = conn.prepareStatement(sql);
// setXXX限制传值的类型
pstmt.setString(1, loginName);
pstmt.setString(2, loginPwd);
rs = pstmt.executeQuery();
if (rs.next()) {
String relname = rs.getString("relname");
System.out.println("登录成功, 欢迎您, " + relname);
} else {
System.out.println("登录失败, 用户名不存在或者密码错误");
}
} catch (ClassNotFoundException | SQLException e) {
throw new RuntimeException(e);
} finally {
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
if (pstmt != null) {
try {
pstmt.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
}
}
PreparedStatement
和Statement
都是用来执行SQL
语句的接口,但是PreparedStatement
预编译SQL
语句,执行速度也更快,可以避免SQL
注入。
4.3 PreparedStatement
的使用
PreparedStatement
是Statement
的子类,自然也可以实现增删改查的操作,如下:
-
新增操作
Class.forName(driver); conn = DriverManager.getConnection(url, user, password); String sql = "INSERT INTO user(name, password, relname, gender, tel) VALUES(?, ?, ?, ?, ?)"; pstmt = conn.prepareStatement(sql); pstmt.setString(1, "迪迦"); pstmt.setString(2, "111"); pstmt.setString(3, "李天"); pstmt.setString(4, "女"); pstmt.setString(5, "666"); int count = pstmt.executeUpdate(); if (count == 1) { System.out.println("插入数据成功"); } else { System.out.println("插入数据失败"); }
-
修改操作
Class.forName(driver); conn = DriverManager.getConnection(url, user, password); String sql = "UPDATE user SET name=?, tel=? WHERE name=?"; pstmt = conn.prepareStatement(sql); pstmt.setString(1, "哈哈"); pstmt.setString(2, "111sssssss"); pstmt.setString(3, "迪迦"); int count = pstmt.executeUpdate(); if (count == 1) { System.out.println("修改数据成功"); } else { System.out.println("修改数据失败"); }
-
删除操作
Class.forName(driver); conn = DriverManager.getConnection(url, user, password); String sql = "DELETE FROM user WHERE name=?"; pstmt = conn.prepareStatement(sql); pstmt.setString(1, "王五"); int count = pstmt.executeUpdate(); if (count != 0) { System.out.println("删除数据成功"); } else { System.out.println("删除数据失败"); }
-
查询操作
Class.forName(driver); conn = DriverManager.getConnection(url, user, password); String sql = "SELECT * FROM user WHERE name like ?"; pstmt = conn.prepareStatement(sql); pstmt.setString(1, "_四"); rs = pstmt.executeQuery(); while (rs.next()) { String name = rs.getString("name"); System.out.println(name); }
4.4 JDBC
批处理
在 JDBC
中,批处理是一种用于高效执行多个 SQL
操作的机制,特别适合需要批量插入、更新或删除数据的场景。以下是批处理的完整使用方法和注意事项:首先是不使用批处理,所用时间为36931ms
,如下:
// 记录时间
long begin = System.currentTimeMillis();
Class.forName(driver);
conn = DriverManager.getConnection(url, user, password);
String sql = "INSERT INTO user(name, password, relname, gender, tel) VALUES(?, ?, ?, ?, ?)";
for (int i = 0; i <= 10000; i++) {
pstmt = conn.prepareStatement(sql);
pstmt.setString(1, "迪迦");
pstmt.setString(2, "111");
pstmt.setString(3, "李天");
pstmt.setString(4, "女");
pstmt.setString(5, "666");
// 逐条提交
pstmt.executeUpdate();
}
System.out.println("不使用批处理插入10000条记录成功");
long end = System.currentTimeMillis();
System.out.println("使用时间" + (end - begin) + "ms"); // 使用时间36931ms
再使用批处理,使用时间为2235ms
如下:
// 记录时间
long begin = System.currentTimeMillis();
Class.forName(driver);
conn = DriverManager.getConnection(url, user, password);
// 关闭自动提交,开启事务
conn.setAutoCommit(false);
String sql = "INSERT INTO user(name, password, relname, gender, tel) VALUES(?, ?, ?, ?, ?)";
for (int i = 0; i <= 10000; i++) {
pstmt = conn.prepareStatement(sql);
pstmt.setString(1, "迪迦");
pstmt.setString(2, "111");
pstmt.setString(3, "李天");
pstmt.setString(4, "女");
pstmt.setString(5, "666");
// 将参数添加到批量处理的队列
pstmt.addBatch();
if (i % 1000 == 0) {
// 执行批量处理队列,返回值为int[],表示每个SQL操作影响的行数
pstmt.executeBatch();
// 清空批处理队列,防止内存溢出
pstmt.clearBatch();
}
}
// 执行剩余批处理
pstmt.executeBatch();
// 手动提交事务
conn.commit();
System.out.println("使用批处理插入10000条记录成功");
long end = System.currentTimeMillis();
System.out.println("使用时间" + (end - begin) + "ms"); // 使用时间717ms
5. 事务 &
存储过程
事务简单来说就是一个完整的业务,在这个业务中需要多条DML
语句共同联合才能完成。事务可以保证多条DML
语句同时成功和同时失败,从而保证数据的安全。DML
语句即修改数据的语句,如INSERT
、DELETE
、UPDATE
。下面模拟一个简单的业务场景:
package com.person;
import java.sql.*;
import java.util.ResourceBundle;
import java.util.Scanner;
/**
* 用户登录案例演示SQL注入问题
*/
public class Client {
public static void main(String[] args) {
// 转账金额
double money = 10000.0;
Connection conn = null;
PreparedStatement ps1 = null;
PreparedStatement ps2 = null;
try {
conn = DbUtils.getConnection();
// 更新 001 账户
String sql1 = "update act set balance = balance - ? where actno = ?";
ps1 = conn.prepareStatement(sql1);
ps1.setDouble(1, money);
ps1.setString(2, "001");
int count1 = ps1.executeUpdate();
// 模拟异常
String str = null;
str.toString();
// 更新 002账户
String sql2 = "update act set balance = balance + ? where actno = ?";
ps2 = conn.prepareStatement(sql2);
ps2.setDouble(1, money);
ps2.setString(2, "002");
int count2 = ps2.executeUpdate();
System.out.println("转账成功");
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
DbUtils.close(null, ps1, null);
DbUtils.close(conn, ps1, null);
}
}
}
5.1 事务
上述代码发生异常时,001
账户的钱会减少,但是002
账户并不会增加,这在实际开发中是很危险的。我们想要的情况时发生异常时能够回滚到发生异常前的状态,这时就需要用到回滚,步骤如下:
-
关闭自动提交,即开启事务。
-
当整个业务结束后,手动提交业务。
-
如果处理业务过程中发生异常,手动回滚事务。
package com.person; import java.sql.*; import java.util.ResourceBundle; import java.util.Scanner; /** * 用户登录案例演示SQL注入问题 */ public class Client { public static void main(String[] args) { // 转账金额 double money = 10000.0; Connection conn = null; PreparedStatement ps1 = null; PreparedStatement ps2 = null; try { conn = DbUtils.getConnection(); // 开启事务 conn.setAutoCommit(false); // 更新 001 账户 String sql1 = "update act set balance = balance - ? where actno = ?"; ps1 = conn.prepareStatement(sql1); ps1.setDouble(1, money); ps1.setString(2, "001"); int count1 = ps1.executeUpdate(); // 模拟异常 String str = null; str.toString(); // 更新 002账户 String sql2 = "update act set balance = balance + ? where actno = ?"; ps2 = conn.prepareStatement(sql2); ps2.setDouble(1, money); ps2.setString(2, "002"); int count2 = ps2.executeUpdate(); System.out.println("转账成功"); // 手动提交 conn.commit(); } catch (SQLException e) { // 回滚事务 try { conn.rollback(); } catch (SQLException ex) { throw new RuntimeException(ex); } } finally { DbUtils.close(null, ps1, null); DbUtils.close(conn, ps1, null); } } }
// 设置事务的隔离级别 conn.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);
5.2 存储过程
首先在MySQL
中创建存储过程,如下:
create procedure mypro(in n int, out sum int)
begin
set sum := 0;
repeat
if n % 2 = 0 then
set sum := sum + n;
end if;
set n := n - 1;
until n <= 0
end repeat;
end;
然后就可以使用JDBC
去调用存储过程,如下:
package com.powernode.jdbc;
import com.powernode.jdbc.utils.DbUtils;
import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Types;
public class Client {
public static void main(String[] args) {
Connection conn = null;
CallableStatement cs = null;
try {
conn = DbUtils.getConnection();
String sql = "{call mypro(?, ?)}";
// 预编译SQL语句
cs = conn.prepareCall(sql);
// 传递参数
cs.setInt(1, 100);
// 注册出参
cs.registerOutParameter(2, Types.INTEGER);
// 执行存储过程
cs.execute();
// 通过出参获取结果
int result = cs.getInt(2);
System.out.println("计算结果:" + result);
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
DbUtils.close(conn, cs, null);
}
}
}
系统的大致功能如下:
public class Client {
static Connection conn = null;
static PreparedStatement pstm = null;
static ResultSet res = null;
static Scanner sc = new Scanner(System.in);
static {
// 数据库连接
try {
ResourceBundle resourceBundle = ResourceBundle.getBundle("jdbc");
String driver = resourceBundle.getString("jdbc.driver");
String user = resourceBundle.getString("user");
String pwd = resourceBundle.getString("password");
String url = resourceBundle.getString("url");
// 注册驱动
Class.forName(driver);
// 获取连接
conn = DriverManager.getConnection(url, user, pwd);
System.out.println(conn);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws SQLException {
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.println("[0]退出系统");
while (true) {
System.out.println("请输入您的选择: ");
int select = sc.nextInt();
switch (select) {
case 1:
// 查看员工列表
doList();
break;
case 2:
// 查看某员工详细信息
System.out.println("请输入员工姓名: ");
String name = sc.next();
doDetail(name);
break;
case 3:
// 新增员工
doSave();
break;
case 4:
// 修改员工信息
doModify();
break;
case 5:
// 删除员工
doDelete();
break;
case 0:
// 退出系统
System.out.println("感谢您的使用!!!");
return;
default:
System.out.println("您的输入有误,请重新输入!!!");
}
}
}
6. 员工信息管理
首先导入如下员工信息:
drop table if exists t_employee;
create table t_employee(
id bigint primary key auto_increment,
name varchar(255),
job varchar(255),
hiredate char(10),
salary decimal(10,2),
address varchar(255)
);
insert into t_employee(name,job,hiredate,salary,address) values('张三','销售员','1999-10-11',5000.0,'北京朝阳');
insert into t_employee(name,job,hiredate,salary,address) values('李四','编码人员','1998-02-12',5000.0,'北京海淀');
insert into t_employee(name,job,hiredate,salary,address) values('王五','项目经理','2000-08-11',5000.0,'北京大兴');
insert into t_employee(name,job,hiredate,salary,address) values('赵六','产品经理','2022-09-11',5000.0,'北京东城');
insert into t_employee(name,job,hiredate,salary,address) values('钱七','测试员','2024-12-11',5000.0,'北京西城');
commit;
select * from t_employee;
6.1 查看员工信息
static void doList() throws SQLException {
// 查看所有员工信息
String sql = "SELECT name, job FROM t_employee";
pstm = conn.prepareStatement(sql);
res = pstm.executeQuery();
while (res.next()) {
String name = res.getString("name");
String job = res.getString("job");
System.out.println(name + "--" + job);
}
}
6.2 查看员工详情
static void doDetail(String name) throws SQLException {
String sql = "SELECT * from t_employee WHERE name=?";
pstm = conn.prepareStatement(sql);
pstm.setString(1, name);
res = pstm.executeQuery();
if (res.next()) {
String id = res.getString("id");
String tname = res.getString("name");
String job = res.getString("job");
String hireDate = res.getString("hireDate");
double salary = res.getDouble("salary");
String address = res.getString("address");
System.out.println(String.format("%s %s %s %s %f %s", id, tname, job, hireDate, salary, address));
} else {
System.out.println("很抱歉, 该员工信息不存在!!!");
}
}
6.3 新增员工
static void doSave() throws SQLException {
String sql = "INSERT INTO t_employee(name, salary, job, hireDate, address) VALUES(?, ?, ?, ?, ?)";
pstm = conn.prepareStatement(sql);
System.out.println("请输入员工姓名: ");
String name = sc.next();
System.out.println("请输入员工职位 ");
String job = sc.next();
System.out.println("请输入员工薪资: ");
double salary = sc.nextDouble();
System.out.println("请输入员工入职时间: ");
String hireDate = sc.next();
System.out.println("请输入员工家庭地址: ");
String address = sc.next();
pstm.setString(1, name);
pstm.setDouble(2, salary);
pstm.setString(3, job);
pstm.setString(4, hireDate);
pstm.setString(5, address);
pstm.executeUpdate();
System.out.println("员工信息录入成功");
}
6.4 修改员工信息
static void doModify() throws SQLException {
// 修改员工信息
String sql = "UPDATE t_employee set salary=?, job=? WHERE name=?";
pstm =conn.prepareStatement(sql);
System.out.println("请输入需要修改的员工姓名: ");
String tname = sc.next();
System.out.println("请输入修改后的薪资: ");
double salary = sc.nextDouble();
System.out.println("请输入修改后的职位: ");
String job = sc.next();
pstm.setDouble(1, salary);
pstm.setString(2, job);
pstm.setString(3, tname);
pstm.executeUpdate();
System.out.println("员工信息修改成功!!!");
}
6.5 删除员工信息
static void doDelete() throws SQLException {
// 删除员工
String sql = "DELETE FROM t_employee WHERE name=?";
System.out.println("请输入要删除的员工姓名: ");
String t_name = sc.next();
pstm= conn.prepareStatement(sql);
pstm.setString(1, t_name);
pstm.executeUpdate();
System.out.println(t_name + "的员工信息已成功删除");
}
7. DAO
DAO
即Data Access Object
,翻译为数据访问对象,是一种用于将业务逻辑与数据访问逻辑分离的设计模式,核心目的是通过抽象接口对数据库的操作,提高代码的可维护性、可扩展性以及可测试性。DAO
的核心作用如下:
- 解耦业务与数据访问:业务层
Service
通过调用DAO
接口操作数据,无需关心底层是JDBC
、Hibernate
还是其他技术实现。 - 统一数据操作规范:定义标准
CRUD
接口,确保不同数据源如MySQL
、MongoDB
的操作方式一致。 - 提高代码复用性:同一
DAO
可被多个业务模块复用,减少重复代码。 - 简化单元测试:通过
Mock DAO
实现,无需真实数据库即可测试业务逻辑。
DAO
的典型分层结构为:表现层调用业务逻辑层,业务逻辑层调用DAO
接口层,DAO
接口层实现DAO
实现层。DAO
实现层操作数据源。
- 表现层:负责用户交互,如网页、
APP
界面,展示数据并收集用户输入。 - 业务逻辑层:处理业务规则、流程控制、调用
DAO
接口操作数据。 DAO
接口层:定义数据操作的抽象接口,屏蔽底层实现细节。DAO
实现层:实现接口,具体操作数据库或其它数据源。- 数据源:实际存储或提供数据的位置。
7.1 javaBean
和pojo
JavaBean
和POJO
是 Java
中用于描述简单数据对象的术语,它们都用于封装数据,但在设计规范和用途上有所不同。
-
pojo
:指普通的java
对象,不依赖于任何框架或接口,仅通过简单的字段和方法实现数据封装。其特点就是无强制规范、无侵入式。public class User { private String name; private int age; public User() {} public User(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public boolean isAdult() { return age >= 18; } }
-
javaBean
:JavaBean
是一种符合特定规范的POJO
,主要用于GUI
开发、工具类库或需要反射操作的场景。无参构造器必须存在,字段私有化,通过属性访问器访问字段。import java.io.Serializable; public class UserBean implements Serializable { // 实现 Serializable // 字段私有 private String name; private int age; private boolean active; // 无参构造器 public UserBean() {} // Getter 和 Setter public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } // 布尔属性可用 isXxx() public boolean isActive() { return active; } public void setActive(boolean active) { this.active = active; } }
特性 POJO
JavaBean
规范要求 无 必须遵守无参构造器、 Getter/Setter
等框架依赖 无 无,但常用于需要内省的工具场景 序列化 可选 通常实现 Serializable
方法逻辑 可包含自定义业务逻辑 通常仅限 Getter/Setter 典型用途 领域模型、 Spring Bean
网路传输、缓存存储
7.2 实现步骤
首先封装Employee
类,一般将普通类称为javaBean
或pojo
如下:
package com.person;
public class Employee {
private Long id;
private String name;
private String job;
private Double salary;
private String hireDate;
private String address;
public Employee(){
}
public Employee(Long id, String name, String job, Double salary, String hireDate, String address) {
this.id = id;
this.name = name;
this.job = job;
this.salary = salary;
this.hireDate = hireDate;
this.address = address;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getJob() {
return job;
}
public void setJob(String job) {
this.job = job;
}
public Double getSalary() {
return salary;
}
public void setSalary(Double salary) {
this.salary = salary;
}
public String getHireDate() {
return hireDate;
}
public void setHireDate(String hireDate) {
this.hireDate = hireDate;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
@Override
public String toString() {
return "Employee{" +
"id=" + id +
", name='" + name + '\'' +
", job='" + job + '\'' +
", salary=" + salary +
", hireDate='" + hireDate + '\'' +
", address='" + address + '\'' +
'}';
}
}
然后就是定义EmployeeDao
,DAO
不负责任何业务逻辑的处理,只负责CRUD
操作。
package com.person.jdbc.dao;
import com.powernode.jdbc.beans.Employee;
import com.powernode.jdbc.utils.DbUtils;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
public class EmployeeDao {
/**
* 新增员工
*/
public int insert(Employee employee) {
Connection conn = null;
PreparedStatement ps = null;
int count = 0;
try {
conn = DbUtils.getConnection();
String sql = "insert into t_employee(name,job,salary,hiredate,address) values(?,?,?,?,?)";
ps = conn.prepareStatement(sql);
ps.setString(1, employee.getName());
ps.setString(2, employee.getJob());
ps.setDouble(3, employee.getSalary());
ps.setString(4, employee.getHiredate());
ps.setString(5, employee.getAddress());
count = ps.executeUpdate();
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
DbUtils.close(conn, ps, null);
}
return count;
}
/**
* 修改员工
*/
public int update(Employee employee){
Connection conn = null;
PreparedStatement ps = null;
int count = 0;
try {
conn = DbUtils.getConnection();
String sql = "update t_employee set name=?, job=?, salary=?, hiredate=?, address=? where id=?";
ps = conn.prepareStatement(sql);
ps.setString(1, employee.getName());
ps.setString(2, employee.getJob());
ps.setDouble(3, employee.getSalary());
ps.setString(4, employee.getHiredate());
ps.setString(5, employee.getAddress());
ps.setLong(6, employee.getId());
count = ps.executeUpdate();
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
DbUtils.close(conn, ps, null);
}
return count;
}
/**
* 根据id删除员工信息
*/
public int deleteById(Long id){
Connection conn = null;
PreparedStatement ps = null;
int count = 0;
try {
conn = DbUtils.getConnection();
String sql = "delete from t_employee where id = ?";
ps = conn.prepareStatement(sql);
ps.setLong(1, id);
count = ps.executeUpdate();
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
DbUtils.close(conn, ps, null);
}
return count;
}
/**
* 根据id查询所有员工
*/
public Employee selectById(Long id){
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
Employee employee = null;
try {
conn = DbUtils.getConnection();
String sql = "select * from t_employee where id = ?";
ps = conn.prepareStatement(sql);
ps.setLong(1, id);
rs = ps.executeQuery();
if(rs.next()){
employee = new Employee();
employee.setId(id);
employee.setName(rs.getString("name"));
employee.setJob(rs.getString("job"));
employee.setSalary(rs.getDouble("salary"));
employee.setHiredate(rs.getString("hiredate"));
employee.setAddress(rs.getString("address"));
}
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
DbUtils.close(conn, ps, rs);
}
return employee;
}
/**
* 查询所有员工信息
*/
public List<Employee> selectAll(){
List<Employee> employees = new ArrayList<>();
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
conn = DbUtils.getConnection();
String sql = "select * from t_employee";
ps = conn.prepareStatement(sql);
rs = ps.executeQuery();
while(rs.next()){
Employee employee = new Employee();
employee.setId(rs.getLong("id"));
employee.setName(rs.getString("name"));
employee.setJob(rs.getString("job"));
employee.setSalary(rs.getDouble("salary"));
employee.setHiredate(rs.getString("hiredate"));
employee.setAddress(rs.getString("address"));
employees.add(employee);
}
} catch (SQLException e) {
throw new RuntimeException(e);
} finally {
DbUtils.close(conn, ps, rs);
}
return employees;
}
}
8. 连接池
Connetion
对象是重量级对象,创建一个Connection
对象就是建立两个进程之间的通信,非常消耗资源,一次完整的数据库操作,90%
的时间都花在了连接对象的创建。除了效率低下,还有一个问题就是连接对象的数量无法限制,如果连接对象的数量过高,可能会导致Mysql
数据库服务器崩溃。
如果使用数据库连接池,就会提前创建好N
个连接对象,将其存放到缓存中。用户请求时,连接对象就可以直接从连接池中获取,而不需要再创建新的连接。并且,当连接池中没有空闲的连接对象时,用户请求就只能等待,就可以避免无法控制连接对象的数量。
连接池有很多,不过所有的连接池都实现了javax.sql.DataSource
接口。因此,不管使用哪家的连接池产品,都直接调用javax.sql.DataSource
接口的方法即可。当然,我们也可以通过实现该接口编写自己的连接池。
对于一个基本的连接池,一般都包含以下属性:
initialSize
:初始化连接数,连接池初始化时创建的连接数。maxActive
:连接池中的最大连接数。当连接池中的连接数量达到此值时,后续请求被阻塞并等待连接池中有连接被释放后再处理。minidle
: 指连接池中最小的空闲连接数,也就是即使当前没有请求,连接池中至少也要保持一定数量的空闲连接,以便应对高并发请求或突发连接请求的情况。maxidle
: 指连接池中最大的空闲连接数,也就是连接池中最多允许保持的空闲连接数量。当连接池中的空闲连接数量达到了maxIdle
设定的值后,多余的空闲连接将会被连接池释放掉。maxWait
:最大等待时间,当连接池中的连接数量达到最大值时,后续请求需要等待的最大时间,如果超过这个时间,则会抛出异常。testOnBorrow、testOnReturn
:为了确保连接池中只有可用的连接,一些连接池会定期对连接进行有效性检查,这里的属性就是配置这些检查的选项。driver
、url
、password
、user
等。
常用的连接池有Druid
、HikariCP
等连接池,如下:
-
Druid
jdbc.driver=com.mysql.cj.jdbc.Driver url=jdbc:mysql://localhost:3306/jdbc user=root password=asdf initialSize=5 minIdle=10 maxActive=20
// 读取属性配置文件 InputStream in = DruidConfig.class.getClassLoader().getResourceAsStream("jdbc.properties"); Properties props = new Properties(); props.load(in); // 创建连接池 DataSource dataSource = DruidDataSourceFactory.createDataSource(props); Connection conn = dataSource.getConnection(); // 关闭连接 并不是真正的关闭连接, 只是将连接归还连接池 conn.close()
-
HikariCP
jdbcUrl=jdbc:mysql://localhost:3306/jdbc username=root password=asdf driverClassName=com.mysql.cj.jdbc.Driver minimumIdle=5 maximumPoolSize=20
InputStream in = HikariConfig.class.getClassLoader().getResourceAsStream("config.properties"); props.load(in); HikariConfig config = new HikariConfig(props); DataSource dataSource = new HikariDataSource(config); Connection conn = dataSource.getConnection(); conn
量过高,可能会导致Mysql
数据库服务器崩溃。
如果使用数据库连接池,就会提前创建好N
个连接对象,将其存放到缓存中。用户请求时,连接对象就可以直接从连接池中获取,而不需要再创建新的连接。并且,当连接池中没有空闲的连接对象时,用户请求就只能等待,就可以避免无法控制连接对象的数量。
连接池有很多,不过所有的连接池都实现了javax.sql.DataSource
接口。因此,不管使用哪家的连接池产品,都直接调用javax.sql.DataSource
接口的方法即可。当然,我们也可以通过实现该接口编写自己的连接池。
对于一个基本的连接池,一般都包含以下属性:
initialSize
:初始化连接数,连接池初始化时创建的连接数。maxActive
:连接池中的最大连接数。当连接池中的连接数量达到此值时,后续请求被阻塞并等待连接池中有连接被释放后再处理。minidle
: 指连接池中最小的空闲连接数,也就是即使当前没有请求,连接池中至少也要保持一定数量的空闲连接,以便应对高并发请求或突发连接请求的情况。maxidle
: 指连接池中最大的空闲连接数,也就是连接池中最多允许保持的空闲连接数量。当连接池中的空闲连接数量达到了maxIdle
设定的值后,多余的空闲连接将会被连接池释放掉。maxWait
:最大等待时间,当连接池中的连接数量达到最大值时,后续请求需要等待的最大时间,如果超过这个时间,则会抛出异常。testOnBorrow、testOnReturn
:为了确保连接池中只有可用的连接,一些连接池会定期对连接进行有效性检查,这里的属性就是配置这些检查的选项。driver
、url
、password
、user
等。
常用的连接池有Druid
、HikariCP
等连接池,如下:
-
Druid
jdbc.driver=com.mysql.cj.jdbc.Driver url=jdbc:mysql://localhost:3306/jdbc user=root password=asdf initialSize=5 minIdle=10 maxActive=20
// 读取属性配置文件 InputStream in = DruidConfig.class.getClassLoader().getResourceAsStream("jdbc.properties"); Properties props = new Properties(); props.load(in); // 创建连接池 DataSource dataSource = DruidDataSourceFactory.createDataSource(props); Connection conn = dataSource.getConnection(); // 关闭连接 并不是真正的关闭连接, 只是将连接归还连接池 conn.close()
-
HikariCP
jdbcUrl=jdbc:mysql://localhost:3306/jdbc username=root password=asdf driverClassName=com.mysql.cj.jdbc.Driver minimumIdle=5 maximumPoolSize=20
InputStream in = HikariConfig.class.getClassLoader().getResourceAsStream("config.properties"); props.load(in); HikariConfig config = new HikariConfig(props); DataSource dataSource = new HikariDataSource(config); Connection conn = dataSource.getConnection(); conn