JDBC
JDBC就是使用Java语言操作关系型数据库的一套API,全称(Java DataBase Connectivity)Java数据库连接。
JDBC本质:
- 官方(sun公司)定义的一套操作所有关系型数据库的规则,即接口
- 各个数据库厂商去实现这套接口,提供数据库驱动jar包
- 我们可以使用这套接口(JDBC) 编程,真正执行的代码是驱动jar包中的实现类
Idea添加MySQL驱动包
1、先从Maven仓库下载合适的mysqljar包
2、建立lib文件夹,添加MySQL的驱动jar包1
3、将lib文件夹设置库
JDBC使用
使用步骤
- 创建工程,导入jar包
- 注册驱动
Class.forName("com.mysql.jdbc.Driver");
- 获取连接
Connection conn = DriverManager.getConnection(url,username,password);
- 定义SQL语句
String sql ="update...";
- 获取执行SQL对象
Statement stmt =conn.createStatement();
- 执行SQL
stmt.executeUpdate(sql);
- 处理返回结果
- 释放资源
\
使用示例
import java.sql.Connection;
import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.Statement;
/**
* JDBC快速入门
*/
public class JDBC_Test {
public static void main(String[] args) throws Exception {
//注册驱动
Class.forName("com.mysql.jdbc.Driver");//固定的类名
//获取连接
String url = "jdbc:mysql://127.0.0.1:3306/db1";//jdbc:mysql://+ IP + Port /数据库名
String username = "root";
String password = "123";
Connection conn = DriverManager.getConnection(url, username, password);
//定义sql语句
String sql = "update account set money =20300 where id =1";
//获取执行sql的对象 statement
Statement stmt = conn.createStatement();
//执行sql
int count = stmt.executeUpdate(sql);//返回受影响的行数
//受处理结果
System.out.println(count);
//释放资源
stmt.close();
conn.close();
}
}
复制代码
测试数据库
create database db1;
create table account(
`id` int primary key,
`name` varchar(4) not null,
`money` int not null
);
Insert into account values (1,"ning",10000);
复制代码
JDBC常用API
Drivermanager(驱动管理类)
作用:
1.注册驱动
Class.forName("com.mysql.jdbc.Driver");
复制代码
com.mysql.jdbc.Driver源码
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!");
}
}
}
复制代码
MySQL5之后的驱动包可以省略注册驱动的步骤,其会自动加载jar包中的META->INF/services/java.sql.Driver文件中的驱动类。
2.获取数据库连接
static Connection getConnection(String url, String user, String password)
复制代码
参数:
细节演示:
String url = "jdbc:mysql:///db1";//省略127.0.0.1:3306
复制代码
当版本达到 5.7及以上,MySQL会建议你使用更为安全的 SSL 连接,这种连接需要复杂的配置,而且会降低20%的性能,所以我们通常不使用这种配置,并设置参数关闭其警告提示。
String url = "jdbc:mysql://127.0.0.1:3306/db1?useSSL=false";
复制代码
Connection(数据库连接对象)
作用:
1.获取执行SQL的对象
有三种方法:(主要使用前两种,第三种很少用)
- 普通执行SQL对象
Statement createStatement()
复制代码
- 预编译SQL的执行SQL对象
PreparedStatement prepareStatement(String sql)
复制代码
- 执行存储过程的对象
CallableStatement prepareCall(String sql)
复制代码
2.管理事务
import java.sql.*;
public class JDBC_Test {
public static void main(String[] args) throws Exception {
//获取连接
String url = "jdbc:mysql://127.0.0.1:3306/db1";//jdbc:mysql://+ IP + Port /数据库名
String username = "root";
String password = "123";
Connection conn = DriverManager.getConnection(url, username, password);
//定义sql语句
String sql1 = "update account set money =money+500 where id =1";
String sql2 = "update account set money =money-500 where id =2";
Statement stmt = null;
//获取执行sql的对象 statement
stmt = conn.createStatement();
try {
//开启事务
conn.setAutoCommit(false);//将自动提交事务关闭
//执行sql
int count1 = stmt.executeUpdate(sql1);//返回受影响的行数
System.out.println(count1);
//出现意外
// System.out.println(3 / 0);
int count2 = stmt.executeUpdate(sql2);//返回受影响的行数
System.out.println(count2);
//提交事务
conn.commit();
} catch (SQLException throwables) {
//回滚事务
conn.rollback();
throwables.printStackTrace();
}
//释放资源
stmt.close();
conn.close();
}
}
复制代码
数据库语句
CREATE DATABASE IF NOT EXISTS db1;
USE db1;
CREATE TABLE account ( `id` INT, `name` VARCHAR ( 10 ), `money` INT, PRIMARY KEY ( id ) );
INSERT INTO account VALUES (1,"阿画",20);
INSERT INTO account VALUES (2,"苗苗",5000);
复制代码
Statement(执行SQL语句)
1.执行DML、DDL语句
int executeUpdate(sql)
复制代码
返回值: DML语句影响的行数 (DDL语句执行后,执行成功也可能返回0)
实际使用:不返回影响的行数,而是返回执行是否成功,对于一些DDL语句(比如定义数据库),不出现异常就算成功。
String sql = "update account set money =20300 where id =1";
Statement stmt = conn.createStatement();
int count = stmt.executeUpdate(sql);
if (count > 0) {
System.out.println("执行成功");
} else {
System.out.println("执行失败");
}
复制代码
2.执行DQL语句
ResultSet executeQuery(sql)
复制代码
返回值: ResultSet 结果集对象
ResultSet(结果集对象)
封装DQL语句的查询结果
其封装了二维表,内部存有游标,游标默认指向数据的上一行(即表头行)
获取查询结果:
boolen next()
复制代码
将光标从当前位置向下移动一行 ,判断当前行是否为有效行(即有数据的行) 返回值:
- true: 有效行,当前行有数据
- false:无效行,当前行没有数据
XXx getXxx(参数):获取数据
复制代码
Xxx
:数据类型;如: int getInt(参数) ; String getString(参数) 参数:
- int:列的编号,从1开始
- String:列的名称
使用步骤
String sql = "select * from account ";
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);
while (rs.next()) {
int id = rs.getInt(1);//ID
String name = rs.getString(2);//name
int money = rs.getInt(3);//money
/*和上面的输出结果一样
int id = rs.getInt(“id”);//ID
String name = rs.getString("name");//name
int money = rs.getInt("money");//money
*/
System.out.println(id);
System.out.println(name);
System.out.println(money);
}
rs.close();
复制代码
使用案例:将ResultSet封装进ArrayList中
定义需要封装的类
class customers {
String CustomerID;
String CompanyName;
String Address;
String City;
public String getCustomerID() {
return CustomerID;
}
public void setCustomerID(String customerID) {
CustomerID = customerID;
}
public String getCompanyName() {
return CompanyName;
}
public void setCompanyName(String companyName) {
CompanyName = companyName;
}
public String getAddress() {
return Address;
}
public void setAddress(String address) {
Address = address;
}
public String getCity() {
return City;
}
public void setCity(String city) {
City = city;
}
@Override
public String toString() {
return "customers{" +
"CustomerID='" + CustomerID + '\'' +
", CompanyName='" + CompanyName + '\'' +
", Address='" + Address + '\'' +
", City='" + City + '\'' +
'}';
}
}
复制代码
将查询到的结果装入集合容器中
Collection<customers> rsl = new ArrayList<>();
String sql = "select * from customers ";
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);
while (rs.next()) {
customers customers = new customers();
customers.setAddress(rs.getString("Address"));
customers.setCity(rs.getString("city"));
customers.setCustomerID(rs.getString("CustomerID"));
customers.setCompanyName(rs.getString("CompanyName"));
rsl.add(customers);
}
rs.close();
rsl.forEach(new Consumer<customers>() {
@Override
public void accept(customers customers) {
System.out.println(customers);
}
});
复制代码
PreparedStatement(预编译SQL语句)
继承自Statement
,作用是预编译SQL语句并执行,预防SQL注入问题。
什么是SQL注入
SQL注入是对用户输入数据的合法性没有判断或过滤不严,通过操作输入来修改事先定义好的SQL语句,用以达到执行代码对服务器进行攻击的方法。
@Test
public void testLogin_Inject() throws Exception {
//获取连接
String url = "jdbc:mysql:///db1?serverTimezone=UTC";//jdbc:mysql://+ IP + Port /数据库名
String username = "root";
String password = "123";
Connection conn = DriverManager.getConnection(url, username, password);
String name = "dadsadadad";
String pwd = "' or '1' = '1";
String sql = "select * from tb_user where username='" + name + "'and password ='" + pwd + "'";
System.out.println(sql);
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);
if (rs.next()) {
System.out.println("login success");
} else {
System.out.println("login fail");
}
//释放资源
stmt.close();
conn.close();
}
复制代码
数据库语句
CREATE TABLE `tb_user` (
`username` varchar(20) DEFAULT NULL,
`password` varchar(20) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
复制代码
1.防止SQL注入
1.获取PreparedStatement对象
new Connection().prepareStatement(String sql)
造成SQL注入的原因还是在拼接字符串时被钻了篡改语义的空子,所以PreparedStatement干脆不拼接字符串,直接使用?占位符代替参数值。
//SQL语句中的参数值,这里使用?占位符替代
String sql = "select * from user where username = ? and password = ?";
//通过Connection对象获取获取,并传入对应的SQL语句
PreparedStatement pstmt = conn.prepareStatement(sql);
复制代码
2.设置参数值
PreparedStatement对象 : setXxx(参数1,参数2):给?赋值
使用?占位符代替参数拼接后,我们使用setXxx来设置参数:
Xxx为数据类型,如setInt(参数1,参数2)
参数1为 ?的位置编号(即第几个?),参数2为?的值
3.执行SQL
不需要再传递sql,直接使用PreparedStatement对象调用,
pstmt.executeUpdate();
pstmt.executeQuery();
@Test
public void testLogin_Inject() throws Exception {
//获取连接
String url = "jdbc:mysql:///db1?serverTimezone=UTC";//jdbc:mysql://+ IP + Port /数据库名
String username = "root";
String password = "' or '1' ='1";//这里使用SQL注入也会失败
Connection conn = DriverManager.getConnection(url, username, password);
String name = "zhangsan";
String pwd = "1234";
String sql = "select * from tb_user where username=? and password = ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
//设置?的值
pstmt.setString(1, name);
pstmt.setString(2, pwd);
ResultSet rs = pstmt.executeQuery();
if (rs.next()) {
System.out.println("登录成功");
} else {
System.out.println("登录失败");
}
//释放资源
rs.close();
pstmt.close();
conn.close();
}
复制代码
预防机制原理:
在设置?的值时会对其转义,原本的单引号'
会被转义为'
,即原本的SQL注入就变成了'' or '1' ='1'
,无法起到原先的作用。
2.预编译SQL
PreparedStatement能预编译SQL,提高性能。
- 当我们在Java中写完SQL语句后,Java代码会将SQL传给MySQL进行检查SQL语法和编译SQL,这两步的执行时间比执行SQL可能更久。
- 对于Statement对象来说,要执行SQL语句需要每次将其导入Statement对象,如上图,MySQL会分别对两句SQL进行检查和编译;
- 而对于PreparedStatement对象来说,MySQL只会对SQL语句检查、编译一次,如果sql模板一样,则只需要进行一次检查、编译。
-
PreparedStatement预编译功能开启:在url后加
useServerPrepStmts=true
String url = "jdbc:mysql:///db1?serverTimezone=UTC&useServerPrepStmts=true"; 复制代码
-
配置MySQL执行日志(重启mysq|服务后生效)
log-output=FILE general-log=1 general_log_file="D:\mysql.log" #日志的位置 slow-query-log=1 slow_query_log_file="D:\mysql_slow.log" long_query_time=2 复制代码
使用之前的SQL注入测试
数据库连接池
- 数据库连接池是个容器,负责分配、管理数据库连接(Connection)
- 它允许应用程序重复使用一个现有的数据库连接,而不是再重新建立一个;
- 释放空闲时间超过最大空闲时间的数据库连接来避免因为没有释放数据库连接而引起的数据库连接遗漏
数据库连接池的实现
- 官方(SUN)提供的数据库连接池标准接口DataSource,由第三方组织实现此接口,其功能是获取连接
Connection getConnection()
- 常见的数据库连接池:DBCP、C3PO、Druid
- 其中Druid(德鲁伊)连接池是阿里巴巴开源的数据库连接池项目,功能强大,性能优秀,是Java语言最好的数据库连接池之一。
Druid连接池
- 导入jar包 druid-1.1.12.jar
- 定义配置文件
- 加载配置文件
- 获取数据库连接池对象
- 获取连接
//加载配置文件
Properties prop = new Properties();
prop.load(new FileInputStream("C:\Users\Aaa\Desktop\Agust\src\druid.properties"));
//获取连接池对象
DataSource dataSource = DruidDataSourceFactory.createDataSource(prop);
//获取数据库连接
Connection conn = dataSource.getConnection();
System.out.println(conn);
复制代码
配置文件
#druid.properties
driverClassName=com.mysql.cj.jdbc.Driver
url=jdbc:mysql:///db1?useSSL=false&serverTimezone=Asia/Shanghai
username=root
password=123
#初始化连接数量
initialSize=5
#最大连接数
maxActive=10
#最大等待时间
maxWait=3000
复制代码
Druid配置详解
属性 | 说明 | 建议值 |
---|---|---|
url | 数据库的jdbc连接地址。一般为连接oracle/mysql。示例如下: | |
mysql : jdbc:mysql://ip:port/dbname?option1&option2&… | ||
oracle : jdbc:oracle:thin:@ip:port:oracle_sid | ||
username | 登录数据库的用户名 | |
password | 登录数据库的用户密码 | |
initialSize | 启动程序时,在连接池中初始化多少个连接 | 10-50已足够 |
maxActive | 连接池中最多支持多少个活动会话 | |
maxWait | 程序向连接池中请求连接时,超过maxWait的值后,认为本次请求失败,即连接池 | 100 |
没有可用连接,单位毫秒,设置-1时表示无限等待 | ||
minEvictableIdleTimeMillis | 池中某个连接的空闲时长达到 N 毫秒后, 连接池在下次检查空闲连接时,将 | 见说明部分 |
回收该连接,要小于防火墙超时设置 | ||
net.netfilter.nf_conntrack_tcp_timeout_established的设置 | ||
timeBetweenEvictionRunsMillis | 检查空闲连接的频率,单位毫秒, 非正整数时表示不进行检查 | |
keepAlive | 程序没有close连接且空闲时长超过 minEvictableIdleTimeMillis,则会执 | true |
行validationQuery指定的SQL,以保证该程序连接不会池kill掉,其范围不超 | ||
过minIdle指定的连接个数。 | ||
minIdle | 回收空闲连接时,将保证至少有minIdle个连接. | 与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: | true |
public PreparedStatement prepareStatement(String sql) | ||
public PreparedStatement prepareStatement(String sql, | ||
int resultSetType, int resultSetConcurrency) | ||
maxPoolPrepareStatementPerConnectionSize | 每个连接最多缓存多少个SQL | 20 |
filters | 这里配置的是插件,常用的插件有: | stat,wall,slf4j |
监控统计: filter:stat | ||
日志监控: filter:log4j 或者 slf4j | ||
防御SQL注入: filter:wall | ||
connectProperties | 连接属性。比如设置一些连接池统计方面的配置。 | |
druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000 | ||
比如设置一些数据库连接属性: | ||
常见问题汇总
问题 在连接时出现 java.sql.SQLException: No timezone mapping entry for 'UTC?useSSL=false'
解决方法:在url参数里加入?serverTimezone=UTC
String url = "jdbc:mysql:///db1?serverTimezone=UTC";
复制代码