一 JDBC的简介
1.1 ODBC的出现
早期的数据库应用程序开发,因为没有通用的针对与数据库的编程接口,所以,开发人员需要学习相关数据库的API,才可以进行应用程序,这样增加了学习成本和开发周期。因此整个开发市场一直在呼吁有一套通用的编程接口
因为有市场需要,微软定义了一组用于数据库应用程序的编程接口ODBC(open database connectivity)。这一套方案大大缩短了程序的开发周期,可以让开发人员只需要调用同一套编程接口,无需考虑具体实现。
ODBC分为四个部分:
应用程序:开发人员所写的代码,ODBC提供的调用接口
驱动程序管理器:用于管理驱动程序的。
驱动程序:对接口的实现部分,各个数据库厂商来完成的。
数据源:就是连接数据库的一些参数:url,username,password
1.2 JDBC简介
Sun公司参考了ODBC方案,制定了一组专门为java语言连接数据库的通用接口JDBC。方便了java开发人员,开发人员不需要考虑特定的数据库的DBMS。JDBC不直接依赖于DBMS,而是通过驱动程序将sql语句转发给DBMS,由DBMS进行解析并执行,处理结果返回。
简单点说,它为Java开发者提供了一种标准的方法来连接和操作各种关系型数据库。
注意:驱动程序:由数据库厂商自己实现,程序员只需要拿来使用即可。
1.3 JDBC的工作原理
第一步: 注册驱动程序
第二步: 请求连接
第三步: 获取执行sql语句的对象,发送给DBMS
第四步:返回结果集,程序员进行处理
第五步: 关闭连接操作
1.4 JDBC中常用API
JDBC API包含了一组类和接口,这些类和接口使得Java程序能够连接到数据库,执行SQL语句,并处理结果。
java.sql.DriverManager
java.sql.Connection
java.sql.Statement
java.sql.Result
1)DriverManager
JDBC的驱动管理类,负责加载和注册驱动,会根据所提供的连接信息(如URL、用户名和密码)自动选择合适的驱动程序。其提供了用于连接数据库的方法getConnection(…)
常用方法:
getConnection(String url, String user, String password)
方法参数解析:
- url: 连接指定数据库的地址 (比如,jdbc:mysql://ip:port/dbname)
- user: 连接用户名
- password: 密码
2)Connection。
是一个接口,代表了与数据库的一个会话;通过DriverManager的getConnection方法,程序可以建立与数据库的连接,返回该接口的一个实现类对象。可以用来获取Statement、PreparedStatement和CallableStatement等对象。
常用方法:
Statement statement =conn.createStatement();
作用:用于获取Statement对象,通过连接会话对象,来获取可以发送sql语句对象
3)Statement相关
Statement:也是一个接口,用于执行静态SQL语句。每次执行都会解析、编译和执行SQL语句,效率较低,但灵活性高。
常用方法:
execute(String sql):通常用于DDL
executeUpdate(String sql):通常用于DML
executeQuery(String sql):用于DQL
PreparedStatement:用于执行预编译的SQL语句。预编译的SQL语句只需要解析、编译一次,之后可以多次执行,提高了执行效率。适用于需要多次执行相同或类似SQL语句的场景。
常用方法:
execute() ;------用于DDL和DML
executeUpdate();-----用于DML
executeQuery();-----用于DQL
CallableStatement:用于执行存储过程和函数。它可以接收参数、返回结果集和处理输出参数。
4)ResultSet
是一个接口,表示从数据库执行DQL语句时返回的结果集。其内部维护了一个指针,该指针默认指向的是第一行之前的位置。next方法用于移动指针到下一行。 指针指向某一行时,就可以调用相关的方法获取这一行上的所有列数据。
常用方法:
next():光标方法,向下移动一行,
getDate(int columnIndex)
getDate(String columnLabel)
getString(int columnIndex)
getString(String columnLabel)
getInt(int columnIndex)
getInt(String columnLabel)
getDouble(int columnIndex)
getDouble(String columnLabel)
二 原生JDBC入门编程
2.1 编写步骤
在编写JDBC的原生代码时,先创建好项目,加载好相应的静态资源,如图片、第三方jar包等,
==注意==:jar包要add到Library里
然后编写步骤如下:
-
注册驱动
-
建立连接
-
获取执行对象
-
处理结果集
-
关闭连接
2.2 案例1:JDBC更新数据
/*
JDBC的第一个程序编写: 修改mydb库里的emp表中7369编号的员工的部门编号为30
准备工作 准备好项目,然后加载第一方jar包,即MYSQL的驱动程序jar文件,注意,add as Library
*/
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.Statement;
public class JDBCDemo01 {
public static void main(String[] args) throws Exception {
// 第一步:加载驱动
Class.forName("com.mysql.cj.jdbc.Driver");
// 第二步:获取连接,(DriverManager 自动识别驱动程序,并向数据库发送连接请求,如果成功,则返回链接会话)
// 参数url: 连接数据库的路径地址 jdbc:mysql://ip:port/库名?serverTimezone=Asia/Shanghai&useTimezone= true
// 参数user:连接数据库的用户名: root
// 参数password:连接数据库的密码: 123456
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb?serverTimezone=Asia/Shanghai&useTimezone= true", "root", "123456");
System.out.println(conn);
//第三步:通过连接会话对象,来获取可以发送sql语句对象
Statement stat = conn.createStatement();
//第四步:使用statement的方法来发送sql语句
/**
* execute(String sql): 发送DDL的方法
* executeUpdate(String sql): 发送DML的方法
* executeQuery(String sql): 发送DQL的方法
* Statement该对象发送的sql语句,每次都会进行解析,编译,执行。效率低。
* 解析SQL: 检验SQL的语法格式是否正确
* 编译SQL: 验证书写的表名,字段等是否存在,如果存在,编译通过。
*
*/
int num=stat.executeUpdate("update emp set deptno=30 where empno=7369 ");
// 第五步:处理结果集
System.out.println("受影响的条数:"+num);
// 第六步 关闭连接会话
conn.close();
}
}
2.3 案例2:JDBC查询数据
/*
使用JDBC来完成第二个程序: 查询emp表中的20号部门的所有员工信息
*/
import java.sql.*;
public class JDBCDemo02 {
public static void main(String[] args) throws Exception {
//加载驱动
Class.forName("com.mysql.cj.jdbc.Driver");
//获取连接
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb?serverTimezone=Asia/Shanghai&useTimezone= true", "root", "123456");
//
Statement state = conn.createStatement();
//注意 DBMS执行后的结果被发送到客户端,被封装到resultSet的具体子类型的对象中
ResultSet resultSet = state.executeQuery("select EMPNO,ENAME,JOB,MGR,HIREDATE,SAL,COMM,DEPTNO from emp where deptno = 20 order by empno desc");
//第四步:处理查询结果集,里面存储了N行记录,指针默认在第一行之前,因此想要获取每一行的数据,需要移动指针
while (resultSet.next()) {// 向下移动指针,如果有数据,返回true
int empno = resultSet.getInt(1);
String ename = resultSet.getString(2);
String job = resultSet.getString(3);
int mgr = resultSet.getInt(4);
Date hiredate = resultSet.getDate(5);
double sal = resultSet.getDouble("SAL");
double comm = resultSet.getDouble("COMM");
int deptno = resultSet.getInt("DEPTNO");
System.out.println(empno+","+ename+","+job+","+mgr+","+hiredate+","+sal+","+comm+","+deptno);
}
// 第五步: 关闭连接
conn.close();
}
}
2.4 练习:JDBC插入和删除数据
插入练习:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.Statement;
public class JDBCDemo03 {
public static void main(String[] args) throws Exception {
Class.forName("com.mysql.cj.jdbc.Driver");
Connection connect = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb?serverTimezone=Asia/Shanghai&useTimezone= true", "root", "123456");
Statement statement = connect.createStatement();
String sql="insert into emp values (10000,'superman','hero',7369,'2024-08-30',2000,500,40)";
int i = statement.executeUpdate(sql);
System.out.println(i);
connect.close();
}
}
删除练习
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
public class JDBCDemo04 {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.cj.jdbc.Driver");
Connection root = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb?serverTimezone=Asia/Shanghai&useTimezone= true", "root", "123456");
Statement ss = root.createStatement();
int i = ss.executeUpdate("Delete from emp where empno=10000");
System.out.println(i);
root.close();
}
}
2.5 DBUtil工具类的封装
第一步:编写配置文件properties
#低版本的mysql的驱动和url
#driver=com.mysql.jdbc.Driver
#url=jdbc:mysql://localhost:3306/bd1906?useUnicode=true&characterEncoding=utf8
#高版本的mysql的驱动和url
driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/mydb?serverTimezone=Asia/Shanghai&useTimezone=true
username=root
passwd=123456
第二步:定义工具类DBUtil,读取配置文件,定义连接方法,关闭方法等
/*
自定义一个链接、关闭数据库的工具类型。
提供一个链接方法:getConnection()
提供一个关闭方法: closeConnection(Connection conn){}
*/
import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Properties;
public class DBUtil {
//定义四个静态变量
private static String driver;
private static String url;
private static String user;
private static String password;
static{
try {
//使用IO流读取jdbc.properties这个配置文件 相对位置"src/"
InputStream inputStream = DBUtil.class
.getClassLoader().
getResourceAsStream("jdbc.properties");
// 创建配置文件集合对象 Properties
Properties prop = new Properties();
//调用Properties对象的load方法,读取流里的信息
prop.load(inputStream);
//从对象身上获取键值对,注意,传入的key是文件中等号前的名
driver = prop.getProperty("driver");
url = prop.getProperty("url");
user = prop.getProperty("username");
password = prop.getProperty("password");
} catch (IOException e) {
e.printStackTrace();
}
}
// public static void main(String[] args) {
// Connection conn = DBUtil.getConnection();
// System.out.println(conn);
//
// DBUtil.closeConnection(conn);
//
// }
/**
* 请求连接数据库,返回连接会话
* @return
*/
public static Connection getConnection(){
Connection conn = null;
try {
Class.forName(driver);
conn = DriverManager.getConnection(url, user, password);
} catch (Exception e) {
throw new RuntimeException(e);
}
return conn;
}
/**
* 关闭连接
* @param conn
*/
public static void closeConnection(Connection conn){
if(conn != null){
try {
conn.close();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
}
第三步:调用DBUtil工具类,进行测试
使用DBUtil重构 修改上面的Update方法里的代码。
/*
使用DBUtil 重构 修改_01Simple.JDBCDemo01里的代码。
*/
public class JDBCDemo01 {
public static void main(String[] args) throws Exception {
// 调用DBUtil里的getConnection方法,获取连接对象
Connection conn = DBUtil.getConnection();
Statement stat = conn.createStatement();
int num=stat.executeUpdate("update emp set deptno=20 where empno=7369 ");
// 第五步:处理结果集
System.out.println("受影响的条数:"+num);
// 第六步 关闭连接会话
DBUtil.closeConnection(conn);
}
}
使用DBUtil重构上述查询的代码
/*
使用DBUtil 重构 _01Simple.JDBCDemo02
*/
import com.jdbc.day01.util.DBUtil;
import java.sql.*;
public class JDBCDemo02 {
public static void main(String[] args) throws Exception {
// 重构
Connection conn = DBUtil.getConnection();
Statement state = conn.createStatement();
//注意 DBMS执行后的结果被发送到客户端,被封装到resultSet的具体子类型的对象中
ResultSet resultSet = state.executeQuery("select EMPNO,ENAME,JOB,MGR,HIREDATE,SAL,COMM,DEPTNO from emp where deptno = 20 order by empno desc");
//第四步:处理查询结果集,里面存储了N行记录,指针默认在第一行之前,因此想要获取每一行的数据,需要移动指针
while (resultSet.next()) {// 向下移动指针,如果有数据,返回true
int empno = resultSet.getInt(1);
String ename = resultSet.getString(2);
String job = resultSet.getString(3);
int mgr = resultSet.getInt(4);
Date hiredate = resultSet.getDate(5);
double sal = resultSet.getDouble("SAL");
double comm = resultSet.getDouble("COMM");
int deptno = resultSet.getInt("DEPTNO");
System.out.println(empno+","+ename+","+job+","+mgr+","+hiredate+","+sal+","+comm+","+deptno);
}
// 重构
DBUtil.closeConnection(conn);
}
}
2.6 jdbc的批处理
每一次的sql操作都会占用数据库的资源。如果将N条操作先存储到缓存区中,然后再一次性刷到数据库中,这就减少了与数据库的交互次数。因此可以提高效率。
Statement 提供了以下两个常用的方法,用做批处理
addBatch(String sql):将sql语句添加到缓存中
executeBatch():将缓存中的sql一次性刷到数据库中
案例演示
/*
为什么使用JDBC的批处理:
因为Statement发生的SQL语句,DBMS每次都会解析,编译,因此性能比较低。
批处理的方法:可以提前将一部分SQL存储在缓存区中,然后一次性的将缓存区哄的SQL刷到DBMS里。
这样可以大大减少了客户端与数据库的交互次数,从而边相等提高效率。
*/
import com.jdbc.day01.util.DBUtil;
import java.sql.Connection;
import java.sql.Statement;
public class BatchTest {
public static void main(String[] args) {
Connection conn = null;
try {
conn = DBUtil.getConnection();
//向数据库中插入1030条记录
Statement stat = conn.createStatement();
for (int i = 0; i <=1030; i++) {
String[] genders = {"F","M"};
String gender = genders[(int)(Math.random()*2)];
String sql = "insert into testbatch values(null,'batch74200"+i+"','"+gender+"')";
// 将sql添加到缓存区
stat.addBatch(sql);
// 缓存区里存储了50个就冲刷一次
if(i%50==0){
// 将缓存里的sql冲刷到DBMS中
stat.executeBatch();
}
}
stat.executeBatch();
}catch (Exception e){
e.printStackTrace();
}finally {
DBUtil.closeConnection(conn);
}
}
}
三 SQL注入问题
3.1 登陆案例
3.1.1 需求分析以及数据准备
1.需求:实现输入用户名和密码后,实现跳转到主页面的功能
2.逻辑分析:
- 客户端:接收用户名和密码,并将这些信息发送到服务端
- 服务端:接收到客户端传过来的用户名和密码后,进行数据库校验是否存在这样的数据,如果存在,就将用户名对应的这一条记录返回,并封装成一个User对象。返回给客户端。
- 客户端收到返回信息后,判断Account对象是否存在,如果存在,就实现跳转.....
账户类代码:
package com.jdbc.day01._04SQLInject;
/*
定义一个java类型Account与表记录进行映射。 一个Account对象表示表中的一条记录信息
*/
import java.sql.Date;
import java.util.Objects;
public class Account {
private int id;
private String accountId;
private double balance;
private String usrname;
private String password;
private String idcard;
private Date opertime;
private char gender;
public Account()
{
}
public Account(int id, String accountId, double balance, String usrname, String password, String idcard, Date opertime, char gender) {
this.id = id;
this.accountId = accountId;
this.balance = balance;
this.usrname = usrname;
this.password = password;
this.idcard = idcard;
this.opertime = opertime;
this.gender = gender;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getAccountId() {
return accountId;
}
public void setAccountId(String accountId) {
this.accountId = accountId;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
this.balance = balance;
}
public String getUsrname() {
return usrname;
}
public void setUsrname(String usrname) {
this.usrname = usrname;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getIdcard() {
return idcard;
}
public void setIdcard(String idcard) {
this.idcard = idcard;
}
public Date getOpertime() {
return opertime;
}
public void setOpertime(Date opertime) {
this.opertime = opertime;
}
public char getGender() {
return gender;
}
public void setGender(char gender) {
this.gender = gender;
}
@Override
public boolean equals(Object object) {
if (this == object) return true;
if (object == null || getClass() != object.getClass()) return false;
Account account = (Account) object;
return id == account.id && Double.compare(balance, account.balance) == 0 && gender == account.gender && Objects.equals(accountId, account.accountId) && Objects.equals(usrname, account.usrname) && Objects.equals(password, account.password) && Objects.equals(idcard, account.idcard) && Objects.equals(opertime, account.opertime);
}
@Override
public int hashCode() {
return Objects.hash(id, accountId, balance, usrname, password, idcard, opertime, gender);
}
@Override
public String toString() {
return "Account{" +
"id=" + id +
", accountId='" + accountId + '\'' +
", balance=" + balance +
", usrname='" + usrname + '\'' +
", password='" + password + '\'' +
", idcard='" + idcard + '\'' +
", opertime=" + opertime +
", gender=" + gender +
'}';
}
}
服务端代码:提供了一个check方法,来验证账号密码是否能登录
import com.jdbc.day01.util.DBUtil;
import java.sql.Connection;
import java.sql.Date;
import java.sql.ResultSet;
import java.sql.Statement;
public class AppServer {
public Account checkLogin(String username,String password){
Account account = null;
Connection conn = null;
try{
//连接数据库
conn=DBUtil.getConnection();
Statement state = conn.createStatement();
// 用户名和密码同时作为where里的条件,进行查询,如果能查询到,则说明用户名和密码正确
String sql = "select * from bank_account where user_name = '"+username+"' and user_pwd = '"+password+"'";
//发送到数据库中执行
ResultSet rs = state.executeQuery(sql);
if(rs.next()){
int id = rs.getInt("id");
String accountId = rs.getString("account_id");
double balance = rs.getDouble("account_balance");
String usrname = rs.getString("user_name");
String password1 = rs.getString("user_pwd");
String idcard = rs.getString("user_idcard");
Date opertime = rs.getDate("oper_time");
char gender = rs.getString("gender").charAt(0);
account = new Account(id,accountId,balance,usrname,password1,idcard,opertime,gender);
}
}catch (Exception e){
e.printStackTrace();
}finally {
DBUtil.closeConnection(conn);
}
return account;
}
}
客户端:
/*
使用Scanner来模拟登录案例的客户端界面
*/
import java.util.Scanner;
public class AppClient {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.println("请输入用户名:");
String username = sc.nextLine();
System.out.println("请输入密码:");
String password = sc.nextLine();
// 来一个服务端对象
AppServer server = new AppServer();
// 调用服务端的checkLogin方法检查
Account account = server.checkLogin(username,password);
if(account==null)
System.out.println("用户名或密码错误!");
else
System.out.println("登录成功!正在调转");
}
}
登录成功演示:
登录失败演示:
问题演示:可以使用or连接,来破坏sql语句的检验,很久之前的WiFi万能钥匙就是这个原理。
所以就出现了问题:sql注入问题,存在安全隐患。
3.2 SQL注入问题(安全隐患)
Statement对象发送的语句可以被改变结构,即如果之前在where中设置的是两个条件,那么可以通过一些参数 比如 添加or 后面再跟其他条件。此时,where子句中是三个条件。
这种情况就叫做SQL注入。有安全隐患问题。
3.3 PreparedStatement类
3.3.1 预编译类的简介
- PreparedStatement是Statement的子类型
- 此类型可以确定SQL语句的结构,无法通过其它方式来增减条件。
- 此类型还通过占位符 "?"来提前占位,并确定语句的结构。
- 提供了相应的赋值方式:
ps.setInt(int index,int value)
ps.setString(int index,String value)
ps.setDouble(int index,double value)
ps.setDate(int index,Date value)index:表示sql语句中占位符?的索引。从1开始
value:占位符所对应的要赋予的值
- 执行方法:
ps.execute() ;------用于DDL和DML
ps.executeUpdate();-----用于DML
ps.executeQuery();-----用于DQL
3.3.2 修改Server里的代码(优化)
/*
提供一个用于检测用户名和密码是否正确的方法。
如果正确,返回一个Account对象
如果不正确,返回一个null
*/
/*
SQL注入:黑客通过传值的方式,改安SQL的条件结构,比如由两个条件,发成了三个条件
and password='"+password+"'....where username ='"+username+"'
password的值,传入了 111111'or1'='1 因此就会变成
"....where username ='"+username+""and password='111111'or '1'='1'"
可以看出,多一个or连接 1=1恒成立的条件。
也就是说Statement可以通过SQL注入的方法将条件的个数改变,换句话说,就是改变了SQL语句的整体结构。
因此Sun公司有设计了一个Statement的子接口PreparedStatement。
Prepatedstatement这个接口会先将SQL结构提前发送到DBMS中,并且DBMS会将该结构锁死。黑客再次通过SQL注入
的方法传入了带有or连接的恒成立的条件,DBMS也只会将其当成一个参数,而不是条件。
解决办法:使用PreparedStatement
*/
import com.jdbc.day01.util.DBUtil;
import java.sql.*;
public class AppServer2 {
public Account checkLogin(String username,String password){
Account account = null;
Connection conn = null;
try{
//连接数据库
conn=DBUtil.getConnection();
/*
调用prepareStatement(String sql) 先确定SQL的结果,发送到DBMS中
使用?来表示占位,用于传参
*/
// 用户名和密码同时作为where里的条件,进行查询,如果能查询到,则说明用户名和密码正确
String sql = "select * from bank_account where user_name = ? and user_pwd = ?";
PreparedStatement state = conn.prepareStatement(sql);
//提前发送完毕后,要继续给?赋值
state.setString(1,username);
state.setString(2,password);
//再次将参数发送到数据库中执行
ResultSet rs = state.executeQuery();
if(rs.next()){
int id = rs.getInt("id");
String accountId = rs.getString("account_id");
double balance = rs.getDouble("account_balance");
String usrname = rs.getString("user_name");
String password1 = rs.getString("user_pwd");
String idcard = rs.getString("user_idcard");
Date opertime = rs.getDate("oper_time");
char gender = rs.getString("gender").charAt(0);
account = new Account(id,accountId,balance,usrname,password1,idcard,opertime,gender);
}
}catch (Exception e){
e.printStackTrace();
}finally {
DBUtil.closeConnection(conn);
}
return account;
}
}
3.3.3 客户端的代码
package com.jdbc.day01._04SQLInject;
/*
使用Scanner来模拟登录案例的客户端界面
*/
import java.util.Scanner;
public class AppClient {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.println("请输入用户名:");
String username = sc.nextLine();
System.out.println("请输入密码:");
String password = sc.nextLine();
// 来一个服务端对象
AppServer2 server = new AppServer2();
// 调用服务端的checkLogin方法检查
Account account = server.checkLogin(username,password);
if(account==null)
System.out.println("用户名或密码错误!");
else
System.out.println("登录成功!正在调转");
}
}
结果展示:
错误:
利用漏洞:
所以sql注入问题得到了解决。