文章目录
JDBC
1、JDBC概述
Java数据库连接,(Java Database Connectivity,简称JDBC)是Java语言中用来规范客户端程序如何来访问数据库的应用程序接口,提供了诸如查询和更新数据库中数据的方法。JDBC也是Sun Microsystems的商标。我们通常说的JDBC是面向关系型数据库的。
简而言之:JDBC就是一种数据库连接技术,是Java语言操作关系型数据库的一套接口,本质是一种规范。目的是规范其他关系型数据库的实现类,其他数据库想被Java操作就需要去实现这一套接口(Java就是大爹😆),即设计接口的实现类(也叫做驱动),这些驱动在项目中一般使用相关jar包引入(这就是所谓的面向接口编程)
JDBC的优点:
- 降低冗余。Java只需要提供接口,具体实现由各数据库厂商完成,Java不需要再去针对不同的数据库进行分别开发实现类
- 降低代码和数据库的耦合性。可随时替换底层数据库,访问数据库的Java代码只需要少量改动
2、JDBC快速入门
巧记:”贾琏欲执事“
备注:MySQL5.0及其以后的版本可以省略加载驱动这一步,原因看这里
示例:
前期准备工作:
- 下载jar包,下载地址:👉jdbc驱动jar包下载
- 导入项目中的,同时加入依赖库
注意:驱动jar包要和你的MySQL版本要配套使用,否则会报错!
<!--mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.16</version>
</dependency>
package com.hhxy.dao.imp;
import com.hhxy.dao.BookDao;
import com.hhxy.pojo.Book;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
/**
* @author ghp
* @date 2022/9/21
*/
public class BookDaoImp implements BookDao {
/**
* 查询所有
*/
@Override
public List<Book> selectAll() {
try {
//1、加载驱动
Class.forName("com.mysql.cj.jdbc.Driver");
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
//2.0 用于存储数据库的连接信息
String jdbcUrl = null;
String jdbcName = null;
String jdbcPassword = null;
try {
//2.1 获取字符输入流对象,用于获取配置文件的连接信息
FileReader fr = new FileReader("src/main/resources/jdbc.properties");
//2.2 获取properties对象,用于读取字符输入流对象中的连接信息
Properties properties = new Properties();
//2.3 将fr读取配置文件的数据加载到properties对象中
properties.load(fr);
//2.4 获取连接信息
jdbcUrl = properties.getProperty("jdbc.url");
jdbcName = properties.getProperty("jdbc.username");
jdbcPassword = properties.getProperty("jdbc.password");
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
//用于存储从数据库中查询到的数据
List<Book> list = new ArrayList<>();
try (
//2、获取连接对象
//2.5 获取连接对象
Connection conn = DriverManager.getConnection(jdbcUrl, jdbcName, jdbcPassword);
//3.2 获取SQL执行对象
Statement stat = conn.createStatement();
)
{
//3、执行SQL
//3.1 编写SQL
String sql = "select * from t_book";
//3.3 执行SQL,并获取结果集
ResultSet rs = stat.executeQuery(sql);
//4、对结果集进行处理
while(rs.next()){
//4.1 获取结果集中的数据
int id = rs.getInt("book_id");
String bookName = rs.getString("book_name");
double price = rs.getDouble("price");
int stock = rs.getInt("stock");
//4.2 将结果集中的数据封装到List集合中
list.add(new Book(id,bookName,price,stock));
}
} catch (SQLException e) {
throw new RuntimeException(e);
}
return list;
}
}
3、JDBC相关API详解
3.1 DriverManager
-
registerDriver
:加载驱动DriverManager.registerDriver(new Driver());
拓展:
-
为什么加载驱动只需要直接将
com.mysql.cj.jdbc.Driver
类加载到内存中就可以了呢?原因:是因为Driver类中有一个静态代码块,该静态代码块中存在一条
DriverManager.registerDriver()
语句进行加载驱动,一旦Driver类被加载就会执行该静态代码块进而执行DriverManager.registerDriver()
语句进行加载驱动。
-
MySQL5.0版本后加载驱动的语句
Class.forName("com.mysql.cj.jdbc.Driver")
可以省略不写(这里值的一提的是MySQL5.0版本加载驱动的语句应该是
Class.forName("com.mysql.jdbc.Driver")
,我用的是MySQL8.0版本,所以需要加.cj,但是一般都是直接省略了,所以无需在乎这些细节🐶)原因:因为
MySQL5.0
版本后,在驱动jar包中META-INF
文件夹下的services
文件夹下存在一个java.sql.Driver
文件,里面存放了驱动类的全类名,当我们将jar包导入依赖库中时,系统就会自动将java.sql.Driver
文件的信息加载到Class.forName
方法中,从而直接实现驱动加载。
-
-
getConnection
:该方法用于连接数据库,连接成功后返回一个连接对象Connection conn = DriverManager.getConnection(url,userName,password);
1)url:数据库连接路径
语法: String url = "jdbc:mysql://ip地址(域名):端口号/数据库名称?参数键值对1&参数键值对2…" 示例: String url = "jdbc:mysql://localhost:3306/ghp_database?characterEncoding=utf8&useSSL=false";
拓展:
-
如果连接的是本机mysql服务器,并且mysql服务默认端口是3306,则url可以简写为:
String url = "jdbc:mysql:///数据库名称?参数键值对1&&参数键值对2…"
-
characterEncoding=utf8
:表示所处理字符的解码和编码的格式。当数据库和项目的编码格式不同时,能够避免出现乱码;当数据库和项目的编码格式相同使可以不使用(一般都会选择加上,提高容错) -
useSSL=false
:表示禁用安全连接方式,解决警告提示。在使用高版本的驱动时,不使用安全连接会弹出警告提示,使用安全连接SSL会降低20%的性能 -
useServerPrepStmts=ture
:开启预编译模式,一般是搭配PrepareStatement对象使用 -
serverTimezone=UTC
:指定MySQL服务器的时间标准,常见的参数有:UTC表示协调世界时,GMT表示世界时。这个参数一般用于我们使用IDEA连接MySQL服务器时配置,防止时间不统一,因为MySQL服务器默认是美国时间/(ㄒoㄒ)/~~ -
useUnicode=true
:指定编码解码格式使用万国码,一般是默认值(可以直接使用characterEncoding=utf8)
2)userName:数据库用户名
3)password:数据库登录密码
-
3.2 Connection
-
事务管理
//开启事务 setAutoCommit(boolean autoCommit);//autoCommit为true表示自动提交,为false表示手动提交 //提交事务 commit(); //回滚事务 rollback();
-
获取SQL执行对象
1)
createStatement
:该方法用于获取一个普通的SQL执行对象StatementStatement stmt = conn.createStatement();
2)
prepareStatement
:该方法用于获取一个预编译的SQL执行对象prepareStatementPrepareStatement stmt = conn.prepareStatement(sql);
作用:防止SQL注入
3)
prepareCall
:该方法用于获取一个执行存储过程的对象PrepareCallPrepareCall stmt = conn.prepareCall();
备注:
- 存储过程就是一个已经预编译的SQL执行对象,使用它去执行SQL语句会比预编译的要快
- 存储过程在MySQL中并不常用,了解二即可_
3.3 Statement
-
executeUpdata
:执行DML、DDL语句,执行成功后返回结果是一个int类型的值int count = stmt.executeUpdate(sql);
备注:
1)如果执行DML语句,该值表示表中受DML语句影响的行数
2)如果执行DDL语句,该值在SQL语句执行成功后也可能为0(比如删除数据库时,删除成功,但是count为0)
-
executeQuery
:执行DQL语句,执行成功后返回一个结果集对象ResultSetResultSet rs = stmt.executeQuery(sql);
3.4 ResultSet
-
next
:将光标向下移动一行,并判断当前行是否为有效行1,返回一个Boolean类型的值1)true:表示该行为有效行
2)false:表示该行为无效行
-
getXxx
:获取对应的数据类型
示例:
package com.hhxy.jdbc;
import org.junit.Test;
import java.sql.*;
public class ResultSetTest {
@Test
public void ResultSetTest() throws Exception {
/*1、连接数据库*/
//String url = "jdbc:mysql://127.0.0.1:3306/ghp?characterEncoding=utf8&useSSL=false";
String url = "jdbc:mysql:///ghp?characterEncoding=utf8&useSSL=false";
String userName = "root";
String password = "32345678";
Connection conn = DriverManager.getConnection(url,userName,password);
/*2、定义SQL语句*/
String sql = "select * from student";
/*3、获取SQL语句执行对象*/
Statement stmt = conn.createStatement();
/*4、执行DQL语句*/
ResultSet rs = stmt.executeQuery(sql);
/*5、处理结果*/
//使用结果集对象rs中的next方法遍历查询结果
while(rs.next()){
//获取遍历的数据
int Sno = rs.getInt(1);
String Sname = rs.getString(2);
String Ssex = rs.getString(3);
/*
也可以使用字段名,效果是一样的
int Sno = rs.getInt("Sno");
String Sname = rs.getString("Sname");
String Ssex = rs.getString("Ssex");
*/
System.out.println(Sno+" "+Sname+" "+" "+Ssex);
}
/*6、释放资源*/
rs.close();
stmt.close();
rs.close();
}
}
Student表:
输出结果:
3.5 PrepareStatement
预编译SQL执行对象(PrepareStatement)作用:预编译SQL语句并执行,并且防止SQL注入。
SQL注入:是通过操作输入来修改事先定义好的SQL语句,用以达到执行代码对服务器进行攻击的方法。
我对SQL注入的理解:当我们在系统登录界面时,人为的输入一些特殊的字符串,即使这些特殊的字符串对应的账户不存在,也能成功登录到系统中。SQL注入这个bug存在的核心原因是字符串的拼接。
-
占位符:
?
:代替字符串,可以避免使用字符串拼接,从而解决SQL注入PrepareStatement对象可以通过使用占位符来解决SQL注入
String sql = "select from tb_user where userName=? and password=?"; PrepareStatement pstmt = conn.prepareStatement(sql); //备注:该执行SQL对象在创建对象的时候就已经传进了SQL语句,所以在调用执行方法的时候就不需要再次传SQL语句了
-
setXxx
:给占位符设置值setXxx(int parameterIndex,Xxx 需要设置的值) 示例: name = "张三"; setString(1,name);//1表示第一个占位符,name表示设置的值
1)parameterIndex:表示第几个占位符
2)Xxx:表示想给该占位符设置的值的数据类型
-
useServerPrepStmts=ture
:开启PrepareStatment预编译功能//在getConnection方法中使用 String url = "jdbc:mysql://127.0.0.1:3306/ghp?useServerPrepStmts=ture";
注意:使用PrepareStatement对象执行SQL语句时,对于敏感字符,无论是否开启预编译都会进行转义(这是PrepareStatement对象自带的预编译功能),开启预编译只是让代码能够不重复编译,详情请看拓展
示例:
package com.hhxy.jdbc;
import org.junit.Test;
import java.sql.*;
public class PrepareStatement_UserLogin {
/**
* 正常登录示范
* @throws SQLException
*/
@Test
public void Test1() throws SQLException {
/*1、连接数据库*/
//String url = "jdbc:mysql://127.0.0.1:3306/ghp?characterEncoding=utf8&useSSL=false";
String url = "jdbc:mysql:///ghp?characterEncoding=utf8&useSSL=false";
String userName = "root";
String password = "32345678";
Connection conn = DriverManager.getConnection(url, userName, password);
//接收用户输入:用户名和密码
String name = "张三";
String pwd = "123";
/*定义SQL语句*/
String sql = "select * from tb_user where userName='"+name+"' and password = '"+pwd+"'";
/*获取SQL语句执行对象Statement*/
Statement stmt = conn.createStatement();
/*执行SQL语句*/
ResultSet rs = stmt.executeQuery(sql);
//判断登录是否成功
if(rs.next()){
System.out.println("登录成功");
}else{
System.out.println("登录失败");
}
/*释放资源*/
rs.close();
stmt.close();
rs.close();
}
/**
* 演示SQL注入
* @throws SQLException
*/
@Test
public void Test2() throws SQLException {
/*1、连接数据库*/
//String url = "jdbc:mysql://127.0.0.1:3306/ghp?characterEncoding=utf8&useSSL=false";
String url = "jdbc:mysql:///ghp?characterEncoding=utf8&useSSL=false";
String userName = "root";
String password = "32345678";
Connection conn = DriverManager.getConnection(url, userName, password);
/*-----------------------SQL注入核心代码------------------------------------------*/
//接收用户输入:用户名和密码
String name = "张三";//用户名乱写
String pwd = "'or'1'='1";//用户密码写特定的字符串
/*定义SQL语句*/
String sql = "select * from tb_user where userName='"+name+"' and password = '"+pwd+"'";
System.out.println(sql);
/* sql语句输出:
select * from tb_user where userName='张三' and password = ''or'1'='1'
where条件永远为真,直接查询tb_user表的所有信息,那么rs肯定是有数据的
*/
/*-------------------------------------------------------------------------------*/
/*获取SQL语句执行对象Statement*/
Statement stmt = conn.createStatement();
/*执行SQL语句*/
ResultSet rs = stmt.executeQuery(sql);
//判断登录是否成功
if(rs.next()){
//rs中有数据就进来
System.out.println("登录成功");
}else{
System.out.println("登录失败");
}
/*释放资源*/
rs.close();
stmt.close();
rs.close();
}
/**
* 使用PrepareStatement对象解决SQL注入问题
*/
@Test
public void Test3() throws SQLException {
/*1、连接数据库*/
//String url = "jdbc:mysql://127.0.0.1:3306/ghp?characterEncoding=utf8&useSSL=false";
String url = "jdbc:mysql:///ghp?characterEncoding=utf8&useSSL=false";
String userName = "root";
String password = "32345678";
Connection conn = DriverManager.getConnection(url, userName, password);
/*-----------------------核心代码------------------------------------------*/
//接收用户输入:用户名和密码
String name = "张三";//用户名乱写
String pwd = "'or'1'='1";//用户密码写特定的字符串
/*定义SQL语句*/
String sql = "select * from tb_user where userName=? and password=?";
/*获取SQL语句执行对象PrepareStatement*/
PreparedStatement pstmt = conn.prepareStatement(sql);
//设置占位符的值
pstmt.setString(1,name);
pstmt.setString(2,pwd);
/**
预编译会导致,会让SQL语句变成:
select * from tb_user where userName='张三' and password = '\\'or\\'1\\'=\\'1'
从这里我们可以知道,前面使用Statement对象执行SQL语句时,也可以通过转义解决SQL注入
*/
/*执行SQL语句*/
ResultSet rs = pstmt.executeQuery();
/*-------------------------------------------------------------------------------*/
//判断登录是否成功
if(rs.next()){
//rs中有数据就进来
System.out.println("登录成功");
}else{
System.out.println("登录失败");
}
/*释放资源*/
rs.close();
pstmt.close();
rs.close();
}
}
拓展:预编译
预编译的优点:
- 使代码性能更高。原因:预编译对于同样功能的代码只编译一次
- 防止SQL注入。原因:预编译会对特殊的字符进行转义,防止字符串拼接
PrepareStatement原理:
- 在获取PrepareStatement对象时,将SQL语句发送给MySQL服务器进行检查,编译(这一步很耗时)
- 执行SQL语句时就不用再对SQL语句进行检查、编译了(节约时间)
- 如果SQL语句都是一样的模板,则只进行一次检查、编译
示意图:
4、数据库连接池
4.1 数据库连接池基本概念
-
数据库连接池是什么?
数据库连接池是个容器,负责分配、管理数据库连接。
-
为什么需要数据库连接池?
数据库连接是一种关键的、有限的、昂贵的资源,这一点在多用户的网页应用程序中体现得尤为突出。对数据库连接的管理能显著影响到整个应用程序的伸缩性和健壮性,影响到程序的性能指标。数据库连接池正是针对这个问题提出来的。
-
数据库连接池的原理:
连接池是在一开始就创建好了一些连接(Connection)对象存储起来。用户需要连接数据库时,不需要自己创建连接,而只需要从连接池中获取一个连接进行使用,使用完毕后再将连接对象归还给连接池;这样就可以起到资源重用,也节省了频繁创建连接销毁连接所花费的时间,从而提升了系统响应的速度。
-
数据库连接池的特点:
1)它允许应用程序重复使用一个现有的数据库连接,而不是再重新建立一个;
2)释放空闲时间超过最大空闲时间的数据库连接来避免因为没有释放数据库连接而引起的数据库连接遗漏。
-
数据库连接池的优点:
1)让资源可重复使用
2)提升系统响应速度
3)避免数据连接遗漏
4.2 数据库连接池的使用
Java提供了数据库连接池标准接口DataSource,该接口一般由第三方实现(Java1.5新增了一个MiniConnectionPoolManager2连接池)。一般常用的数据库连接池有:DBCP3、C3P04、Druid5
getConnection
:该方法用于获取连接池对象
Druid(德鲁伊)的基本使用:
druid配置详解:
属性 | 说明 | 建议值 |
---|---|---|
url | 数据库的jdbc连接地址。一般为连接oracle/mysql。示例如下: | |
mysql : jdbc:mysql://ip:port/dbname?option1&option2&… | ||
oracle : jdbc:oracle:thin:@ip:port:oracle_sid | ||
username | 登录数据库的用户名 | |
password | 登录数据库的用户密码 | |
initialSize | 启动程序时,在连接池中初始化多少个连接(默认是0) | 10-50已足够 |
maxActive | 连接池中最多支持多少个活动会话(默认值是8) | |
maxWait | 程序向连接池中请求连接时,超过maxWait的值后,认为本次请求失败,即连接池 | 100 |
没有可用连接,单位毫秒,设置-1时表示无限等待 | ||
minEvictableIdleTimeMillis | 池中某个连接的空闲时长达到 N 毫秒后, 连接池在下次检查空闲连接时,将 | 见说明部分 |
回收该连接,要小于防火墙超时设置 | ||
net.netfilter.nf_conntrack_tcp_timeout_established的设置 | ||
timeBetweenEvictionRunsMillis | 检查空闲连接的频率,单位毫秒, 非正整数时表示不进行检查 | |
keepAlive | 程序没有close连接且空闲时长超过 minEvictableIdleTimeMillis,则会执 | true |
行validationQuery指定的SQL,以保证该程序连接不会池kill掉,其范围不超 | ||
过minIdle指定的连接个数。 | ||
minIdle | 回收空闲连接时,将保证至少有minIdle个连接(默认是8) | 与initialSize相同 |
removeAbandoned | 要求程序从池中get到连接后, N 秒后必须close,否则druid 会强制回收该 | false,当发现程序有未 |
连接,不管该连接中是活动还是空闲, 以防止进程不会进行close而霸占连接。 | 正常close连接时设置为true | |
removeAbandonedTimeout | 设置druid 强制回收连接的时限,当程序从池中get到连接开始算起,超过此 | 应大于业务运行最长时间 |
值后,druid将强制回收该连接,单位秒。 | ||
logAbandoned | 当druid强制回收连接后,是否将stack trace 记录到日志中 | true |
testWhileIdle | 当程序请求连接,池在分配连接时,是否先检查该连接是否有效。(高效) | true |
validationQuery | 检查池中的连接是否仍可用的 SQL 语句,drui会连接到数据库执行该SQL, 如果 | |
正常返回,则表示连接可用,否则表示连接不可用 | ||
testOnBorrow | 程序 申请 连接时,进行连接有效性检查(低效,影响性能) | false |
testOnReturn | 程序 返还 连接时,进行连接有效性检查(低效,影响性能) | false |
poolPreparedStatements | 缓存通过以下两个方法发起的SQL(默认是false) | true |
public PreparedStatement prepareStatement(String sql) | ||
public PreparedStatement prepareStatement(String sql, | ||
int resultSetType, int resultSetConcurrency) | ||
maxPoolPrepareStatementPerConnectionSize | 每个连接最多缓存多少个SQL(默认是-1) | 20 |
filters | 这里配置的是插件,常用的插件有: | stat,wall,slf4j |
监控统计: filter:stat | ||
日志监控: filter:log4j 或者 slf4j | ||
防御SQL注入: filter:wall | ||
connectProperties | 连接属性。比如设置一些连接池统计方面的配置。 | |
druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000 | ||
比如设置一些数据库连接属性: |
package com.hhxy.Druid;
import com.alibaba.druid.pool.DruidDataSourceFactory;
import javax.sql.DataSource;
import java.io.FileInputStream;
import java.sql.Connection;
import java.util.Map;
import java.util.Properties;
public class DruidTest {
public static void main(String[] args) throws Exception {
//1、导入数据库连接池jar包
//2、定义配置文件
//3、加载配置文件,获取配置文件对象
Properties prop = new Properties();
prop.load(new FileInputStream("day1_jdbc_demo\\src\\druid.properties"));
// prop.load(DruidTest.class.getResourceAsStream("/druid.properties"));//方法2,使用类对象方法定位文件
//4、获取连接池对象
DataSource ds = DruidDataSourceFactory.createDataSource(prop);
//5、获取连接对象
Connection conn = ds.getConnection();
System.out.println(conn);//连接成功输出:com.mysql.cj.jdbc.ConnectionImpl@4a83a74a
//System.out.println(System.getProperty("user.dir"));//打印当前文件路径,用于定位配置文件(这是一个小技巧)
}
}
druid.properties:
driverClassName=com.mysql.cj.jdbc.Driver
url=jdbc:mysql:///ghp?
useSSL=false&useServerPrepStmts=true
username=root
password=32345678
# 初始化连接数量
initialSize=5
# 最大连接数
maxActive=10
# 最大等待时间
maxWait=3000
有效行就是这一行必须存在,这一行可以为空,可以为0,但是一定要存在 ↩︎
MiniConnectionPoolManager:是一个轻量级JDBC数据库连接池。它只需要Java1.5(或更高)并且没有依赖第三方包。 ↩︎
DBCP是一个依赖Jakartacommons-pool对象池机制的数据库连接池。DBCP可以直接的在应用程序中使用。 ↩︎
C3P0是一个开放源代码的JDBC连接池,它在lib目录中与Hibernate一起发布,包括了实现jdbc3和jdbc2扩展规范说明的Connection 和Statement 池的DataSources 对象。 ↩︎
Druid连接池是阿里巴巴开源的数据库连接池项目 功能强大,性能优秀,是Java语言最好的数据库连接池之一。Druid不仅是一个数据库连接池,还包含一个ProxyDriver、一系列内置的JDBC组件库、一个SQL Parser。支持所有JDBC兼容的数据库,包括Oracle、MySql、Derby、Postgresql、SQL Server、H2等。 ↩︎