文章目錄
-
- JDBC
- 1,JDBC概述
-
- 1.1 JDBC概念
- 1.2 JDBC本質
- 1.3 JDBC好處
- 2,JDBC快速入門
-
- 2.1 編寫代碼步驟
- 2.2 具體操作
- 3,JDBC API詳解
-
- 3.1 DriverManager
- 3.2 Connection (事務歸我管)
-
- 3.2.1 擷取執行對象
- 3.2.2 事務管理
- 3.3 Statement
-
- 3.3.1 概述
- 3.3.2 代碼實作
- 3.4 ResultSet
-
- 3.4.1 概述
- 3.4.2 代碼實作
- 3.5 案例
- 3.6 PreparedStatement
-
- 3.6.1 SQL注入
- 3.6.2 代碼模拟SQL注入問題
- 3.6.3 PreparedStatement概述
- 3.6.4 使用PreparedStatement改進
- 3.6.5 PreparedStatement原理
- 4,資料庫連接配接池
JDBC
今日目标
- 掌握JDBC的的CRUD
- 了解JDBC中各個對象的作用
- 掌握Druid的使用
1,JDBC概述
在開發中我們使用的是java語言,那麼勢必要通過java語言操作資料庫中的資料。這就是接下來要學習的JDBC。
1.1 JDBC概念
JDBC 就是使用Java語言操作關系型資料庫的一套API
全稱:( Java DataBase Connectivity ) Java 資料庫連接配接
我們開發的同一套Java代碼是無法操作不同的關系型資料庫,因為每一個關系型資料庫的底層實作細節都不一樣。如果這樣,問題就很大了,在公司中可以在開發階段使用的是MySQL資料庫,而上線時公司最終選用oracle資料庫,我們就需要對代碼進行大批量修改,這顯然并不是我們想看到的。我們要做到的是同一套Java代碼操作不同的關系型資料庫,而此時sun公司就指定了一套标準接口(JDBC),JDBC中定義了所有操作關系型資料庫的規則。衆所周知接口是無法直接使用的,我們需要使用接口的實作類,而這套實作類(稱之為:驅動)就由各自的資料庫廠商給出。
1.2 JDBC本質
- 官方(sun公司)定義的一套操作所有關系型資料庫的規則,即接口
- 各個資料庫廠商去實作這套接口,提供資料庫驅動jar包
- 我們可以使用這套接口(JDBC)程式設計,真正執行的代碼是驅動jar包中的實作類
1.3 JDBC好處
- 各資料庫廠商使用相同的接口,Java代碼不需要針對不同資料庫分别開發
- 可随時替換底層資料庫,通路資料庫的Java代碼基本不變
以後編寫操作資料庫的代碼隻需要面向JDBC(接口),操作哪兒個關系型資料庫就需要導入該資料庫的驅動包,如需要操作MySQL資料庫,就需要再項目中導入MySQL資料庫的驅動包。如下圖就是MySQL驅動包
所謂驅動,就是各廠商對jdbc這套接口的實作類
2,JDBC快速入門
先來看看通過Java操作資料庫的流程
第一步:編寫Java代碼
第二步:Java代碼将SQL發送到MySQL服務端
第三步:MySQL服務端接收到SQL語句并執行該SQL語句
第四步:将SQL語句執行的結果傳回給Java代碼
2.1 編寫代碼步驟
- 建立工程,導入驅動jar包
-
注冊驅動
擷取位元組碼,其實還有一個天然的作用,就是将類加載進記憶體
-
擷取連接配接
Java代碼需要發送SQL給MySQL服務端,就需要先建立連接配接
- 定義SQL語句
-
擷取執行SQL對象
執行SQL語句需要SQL執行對象,而這個執行對象就是Statement對象
- 執行SQL
- 處理傳回結果
- 釋放資源
2.2 具體操作
- 建立新的空的項目。定義項目的名稱,并指定位置
- 對項目進行設定,JDK版本、編譯版本
可以裝多個jdk,然後選擇哪個即可
- 建立子產品,指定子產品的名稱及位置
-
導入驅動包
将mysql的驅動包放在子產品下的lib目錄(随意命名)下,并将該jar包添加為庫檔案
lib目錄和src同級别 (不然也建立不了目錄,隻能建立包)
- 在添加為庫檔案的時候,有如下三個選項
- Global Library : 全局有效
- Project Library : 項目有效
- Module Library : 子產品有效
改用全局有效試試
- 在src下建立類
- 編寫代碼如下
/**
* JDBC快速入門
*
* 注意 驅動包 我導入成全局的了
*/
public class JDBCDemo {
public static void main(String[] args) throws Exception {
//1. 注冊驅動
//Class.forName("com.mysql.jdbc.Driver");//将類加載到記憶體中
//mysql5之後 可以省略注冊這一步了 自動注冊
//2. 擷取連接配接
String url = "jdbc:mysql://127.0.0.1:3306/db1";
String user = "root";
String password = "1234";
Connection conn = DriverManager.getConnection(url, user, password);
//3. 定義sql
String sql = "update account set money = 2000 where id = 1";
//4. 擷取執行sql的對象 Statement
Statement st = conn.createStatement();
//5. 執行sql 修改語句對應的方法
int n = st.executeUpdate(sql); //傳回受影響的行數
//6. 處理結果 (此處就列印下行數了)
System.out.println(n);
//7. 釋放資源 (先開後放)
st.close();
conn.close();
}
}
3,JDBC API詳解
3.1 DriverManager
DriverManager(驅動管理類)作用:
- 注冊驅動
registerDriver方法是用于注冊驅動的,但是我們之前做的入門案例并不是這樣寫的。而是如下實作
我們查詢MySQL提供的Driver類,看它是如何實作的,源碼如下:
加載Driver類的時候靜态代碼塊會執行,也就會registerDriver()注冊驅動程式了
在該類中的靜态代碼塊中已經執行了
對象的DriverManager
方法進行驅動的注冊了,那麼我們隻需要加載registerDriver()
類,該靜态代碼塊就會執行。而Driver
就可以加載Class.forName("com.mysql.jdbc.Driver");
類。Driver
提示:
- MySQL 5之後的驅動包,可以省略注冊驅動的步驟
- 自動加載jar包中META-INF/services/java.sql.Driver檔案中的驅動類
-
擷取資料庫連接配接
[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-6NtgYq0r-1677226449633)(assets/image-20210725171355278.png)]
參數說明:
- url : 連接配接路徑
文法:jdbc:mysql://ip位址(域名):端口号/資料庫名稱?參數鍵值對1&參數鍵值對2…
示例:jdbc:mysql://127.0.0.1:3306/db1
細節:
- 如果連接配接的是本機mysql伺服器,并且mysql服務預設端口是3306,則url可以簡寫為:jdbc:mysql:///資料庫名稱?參數鍵值對
- 配置 useSSL=false 參數,禁用安全連接配接方式,解決警告提示
- user :使用者名
- poassword :密碼
- url : 連接配接路徑
代碼簡化1:
/**
* JDBC API詳解:DriverManger
*
*/
public class JDBCDemo2_DriverManager {
public static void main(String[] args) throws Exception {
//1. 注冊驅動
//Class.forName("com.mysql.jdbc.Driver");//将類加載到記憶體中 //Driver類的靜态代碼塊注冊的
//mysql5之後 可以省略注冊這一步了 自動注冊
//2. 擷取連接配接
// 1) 如果連接配接的是本機的mysql 并且端口是預設的3306 可以簡化書寫
// 2) 配置 useSSL=false 參數,禁用安全連接配接方式,解決警告提示 (輸出就沒有警告了)
String url = "jdbc:mysql:///db1?useSSL=false";
String user = "root";
String password = "1234";
Connection conn = DriverManager.getConnection(url, user, password);
//3. 定義sql
String sql = "update account set money = 2000 where id = 1";
//4. 擷取執行sql的對象 Statement
Statement st = conn.createStatement();
//5. 執行sql 修改語句對應的方法
int n = st.executeUpdate(sql); //傳回受影響的行數
//6. 處理結果 (此處就列印下行數了)
System.out.println(n);
//7. 釋放資源 (先開後放)
st.close();
conn.close();
}
}
3.2 Connection (事務歸我管)
Connection(資料庫連接配接對象)作用:
- 擷取執行 SQL 的對象
- 管理事務
3.2.1 擷取執行對象
-
普通執行SQL對象
入門案例中就是通過該方法擷取的執行對象。
-
預編譯SQL的執行SQL對象:防止SQL注入
通過這種方式擷取的
SQL語句執行對象是我們一會重點要進行講解的,它可以防止SQL注入。PreparedStatement
-
執行存儲過程的對象
通過這種方式擷取的
執行對象是用來執行存儲過程的,而存儲過程在MySQL中不常用,是以這個我們将不進行講解。CallableStatement
3.2.2 事務管理
先回顧一下MySQL事務管理的操作:
- 開啟事務 : BEGIN; 或者 START TRANSACTION;
- 送出事務 : COMMIT;
- 復原事務 : ROLLBACK;
MySQL預設是自動送出事務
接下來學習JDBC事務管理的方法。
Connection幾口中定義了3個對應的方法:
- 開啟事務 參與autoCommit 表示是否自動送出事務,true表示自動送出事務,false表示手動送出事務。而開啟事務需要将該參數設為為false。
- 送出事務
- 復原事務
具體代碼實作如下:
public class JDBCDemo3_Connection {
public static void main(String[] args) throws Exception {
//1. 注冊驅動
//Class.forName("com.mysql.jdbc.Driver");//将類加載到記憶體中 //Driver類的靜态代碼塊注冊的
//mysql5之後 可以省略注冊這一步了 自動注冊
//2. 擷取連接配接
// 1) 如果連接配接的是本機的mysql 并且端口是預設的3306 可以簡化書寫
// 2) 配置 useSSL=false 參數,禁用安全連接配接方式,解決警告提示 (輸出就沒有警告了)
String url = "jdbc:mysql:///db1?useSSL=false";
String user = "root";
String password = "1234";
Connection conn = DriverManager.getConnection(url, user, password);
//3. 定義sql
String sql1 = "update account set money = money + 500 where id = 1";
String sql2 = "update account set money = money - 500 where id = 2";
//4. 擷取執行sql的對象 Statement
Statement st = conn.createStatement();
try {
//S1: 執行sql之前開啟事務
conn.setAutoCommit(false);
//5. 執行sql 修改語句對應的方法
int n1 = st.executeUpdate(sql1); //傳回受影響的行數
System.out.println(n1);//6.處理結果
int i = 3/0; //自己造異常 模拟
int n2 = st.executeUpdate(sql2); //傳回受影響的行數
System.out.println(n2);//6.處理結果
//S2: 結果處理完,說明執行成功,送出事務
conn.commit();
}catch (Exception e){
//S2: 一旦發生異常就復原
conn.rollback();
e.printStackTrace();
}finally {
//7. 釋放資源 (先開後放) 不管有沒有異常
st.close();
conn.close();
}
}
}
3.3 Statement
3.3.1 概述
Statement對象的作用就是用來執行SQL語句。而針對不同類型的SQL語句使用的方法也不一樣。
- 執行DDL、DML語句
- 執行DQL語句 該方法涉及到了
對象,而這個對象我們還沒有學習,一會再重點講解。ResultSet
3.3.2 代碼實作
- 執行DML語句
@Test
public void testDML() throws SQLException {
//1. 注冊驅動 省略
//2. 擷取連接配接
String url = "jdbc:mysql:///db1?useSSL=false";
String user = "root";
String password = "1234";
Connection conn = DriverManager.getConnection(url, user, password);
//3. 定義sql
String sql = "update account set money = 3000 where id = 1";
//4. 擷取執行sql的對象 Statement
Statement st = conn.createStatement();
//5. 執行sql 修改語句對應的方法
int n = st.executeUpdate(sql); //執行DML語句後,受影響的行數
//6. 處理結果 (此處就列印下行數了)
if(n>0){
System.out.println("修改成功!");
}else {
System.out.println("失敗~");
}
//7. 釋放資源 (先開後放)
st.close();
conn.close();
}
- 執行DDL語句
@Test
public void testDDL() throws SQLException {
//1. 注冊驅動 省略
//2. 擷取連接配接
String url = "jdbc:mysql:///db1?useSSL=false";
String user = "root";
String password = "1234";
Connection conn = DriverManager.getConnection(url, user, password);
//3. 定義sql
String sql = "create database db2";
//4. 擷取執行sql的對象 Statement
Statement st = conn.createStatement();
//5. 執行sql 修改語句對應的方法
int n = st.executeUpdate(sql); //執行DML語句後,受影響的行數
//6. 處理結果 (此處就列印下行數了)
if(n>0){
System.out.println("建立成功!");
}else {
System.out.println("失敗~");
}
//7. 釋放資源 (先開後放)
st.close();
conn.close();
}
@Test
public void testDDL2() throws SQLException {
//1. 注冊驅動 省略
//2. 擷取連接配接
String url = "jdbc:mysql:///db1?useSSL=false";
String user = "root";
String password = "1234";
Connection conn = DriverManager.getConnection(url, user, password);
//3. 定義sql
String sql = "drop database if exists db2";
//4. 擷取執行sql的對象 Statement
Statement st = conn.createStatement();
//5. 執行sql 修改語句對應的方法
int n = st.executeUpdate(sql); //執行DML語句後,受影響的行數
//6. 處理結果 (此處就列印下行數了)
System.out.println(n);//DDL 删除操作 執行成功 也傳回0 不一定傳回1 此時不能通過n>1判斷是否成功了
//7. 釋放資源 (先開後放)
st.close();
conn.close();
}
DDL執行成功,結果也可能是0
注意:
- 以後開發很少使用java代碼操作DDL語句
3.4 ResultSet
3.4.1 概述
ResultSet(結果集對象)作用:
- 封裝了SQL查詢語句的結果。
而執行了DQL語句後就會傳回該對象,對應執行DQL語句的方法如下:
ResultSet executeQuery(sql):執行DQL 語句,傳回 ResultSet 對象
那麼我們就需要從
ResultSet
對象中擷取我們想要的資料。
ResultSet
對象提供了操作查詢結果資料的方法,如下:
boolean next()方法傳回值說明:
- 将光标從目前位置向前移動一行
- 判斷目前行是否為有效行
- true : 有效行,目前行有資料
- false : 無效行,目前行沒有資料
xxx getXxx(參數):擷取資料
- xxx : 資料類型;如: int getInt(參數) ;String getString(參數)
- 參數
- int類型的參數:列的編号,從1開始
- String類型的參數: 列的名稱
如下圖為執行SQL語句後的結果
一開始光标指定于第一行前,如圖所示紅色箭頭指向于表頭行。當我們調用了
next()
方法後,光标就下移到第一行資料,并且方法傳回true,此時就可以通過
getInt("id")
擷取目前行id字段的值,也可以通過
getString("name")
擷取目前行name字段的值。如果想擷取下一行的資料,繼續調用
next()
方法,以此類推。
3.4.2 代碼實作
@Test
public void testDML() throws SQLException {
//1. 注冊驅動 省略
//2. 擷取連接配接
String url = "jdbc:mysql:///db1?useSSL=false";
String user = "root";
String password = "1234";
Connection conn = DriverManager.getConnection(url, user, password);
//3. 定義sql
String sql = "select * from account";
//4. 擷取Statement對象
Statement st = conn.createStatement();
//5. 執行sql
ResultSet rs = st.executeQuery(sql);
//6. 處理結果
/* while (rs.next()){
//根據列标通路 注意從1開始
int id = rs.getInt(1);
String name = rs.getString(2);
double money = rs.getDouble(3);
System.out.println(id+" "+name+" "+money);
}*/
//根據列名通路最好
while (rs.next()){
int id = rs.getInt("id");
String name = rs.getString("name");
double money = rs.getDouble("money");
System.out.println(id+" "+name+" "+money);
}
//7. 千萬記得釋放資源
rs.close();
st.close();
conn.close();
}
3.5 案例
- 需求:查詢account賬戶表資料,封裝為Account對象中,并且存儲到ArrayList集合中
- 定義實體類Account
- 查詢資料,封裝到Account對象中
- 将Account對象存入ArrayList集合中
- 代碼實作
/**
* 查詢account賬戶表資料,封裝為Account對象中,并且存儲到ArrayList集合中
* 1. 定義實體類Account
* 2. 查詢資料,封裝到Account對象中
* 3. 将Account對象存入ArrayList集合中
*/
@Test
public void testResultSet2() throws SQLException {
//1. 注冊驅動 省略
//2. 擷取連接配接
String url = "jdbc:mysql:///db1?useSSL=false";
String user = "root";
String password = "1234";
Connection conn = DriverManager.getConnection(url, user, password);
//3. 定義sql
String sql = "select * from account";
//4. 擷取Statement對象
Statement st = conn.createStatement();
//5. 執行sql
ResultSet rs = st.executeQuery(sql);
//6. 處理結果
//根據列名通路最好
//準備好ArrayList
List<Account> list = new ArrayList<>();
while (rs.next()){
int id = rs.getInt("id");
String name = rs.getString("name");
double money = rs.getDouble("money");
Account account = new Account(id, name, money);
list.add(account);
}
for (Account account : list) {
System.out.println(account);
}
//7. 千萬記得釋放資源
rs.close();
st.close();
conn.close();
}
3.6 PreparedStatement
PreparedStatement作用:
- 預編譯SQL語句并執行:預防SQL注入問題
對上面的作用中SQL注入問題大家肯定不了解。那我們先對SQL注入進行說明.
3.6.1 SQL注入
SQL注入是通過操作輸入來修改事先定義好的SQL語句,用以達到執行代碼對伺服器進行攻擊的方法。
根據要求修改
application.properties
檔案中的使用者名和密碼,檔案内容如下:
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/test?useSSL=false&useUnicode=true&characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=1234
在MySQL中建立名為
test
的資料庫
在指令提示符中運作今天資料下的
day03-JDBC\資料\2. sql注入示範\sql.jar
這個jar包。
直接輕按兩下sql.jar也行
此時我們就能在資料庫中看到user表
接下來在浏覽器的位址欄輸入
localhost:8080/login.html
就能看到如下頁面
我們就可以在如上圖中輸入使用者名和密碼進行登陸。使用者名和密碼輸入正确就登陸成功,跳轉到首頁。使用者名和密碼輸入錯誤則給出錯誤提示,如下圖
但是我可以通過輸入一些特殊的字元登陸到首頁。
使用者名随意寫,密碼寫成
' or '1' ='1
也能登陸成功!
這就是SQL注入漏洞,也是很危險的。當然現在市面上的系統都不會存在這種問題了,是以大家也不要嘗試用這種方式去試其他的系統。
那麼該如何解決呢?這裡就可以将SQL執行對象
Statement
換成
PreparedStatement
對象。
3.6.2 代碼模拟SQL注入問題
- 準備工作
use db1;
-- 删除tb_user表
drop table if EXISTS tb_user;
-- 建立tb_user表
create table tb_user(
id int,
username varchar(20),
password varchar(32)
);
-- 添加資料
insert into tb_user VALUES
(1,'zhangsan','123'),
(2,'lisi','234');
SELECT * from tb_user;
@Test
public void testResultSet() throws SQLException {
String url = "jdbc:mysql:///db1?useSSL=false";
String user = "root";
String password = "1234";
Connection conn = DriverManager.getConnection(url, user, password);
//模拟使用者輸入:使用者名和密碼
String name = "hackdshjksdhdjh";
String pwd = "' or '1'='1"; //可怕的注入 直接修改了sql語句的含義
//sql語句
String sql = "select * from tb_user where username = '"+name+"' and password = '"+pwd+"'";
System.out.println(sql);
//擷取statement對象
Statement st = conn.createStatement();
//執行sql
ResultSet rs = st.executeQuery(sql);
//判斷登陸是否成功
if(rs.next()){//根據name和pwd能查到資料就說明登陸成功
System.out.println("登陸成功!");
}else {
System.out.println("失敗~");
}
//千萬記得釋放資源
rs.close();
st.close();
conn.close();
}
上面代碼是将使用者名和密碼拼接到sql語句中,拼接後的sql語句如下
從上面語句可以看出條件
username = 'hackdshjksdhdjh' and password = ''
不管是否滿足,而
or
後面的
'1' = '1'
是始終滿足的,最終條件是成立的,就可以正常的進行登陸了。
更為可怕的是,這條sql語句會查出tb_user表的所有資料
接下來我們來學習PreparedStatement對象.
3.6.3 PreparedStatement概述
PreparedStatement作用:
- 預編譯SQL語句并執行:預防SQL注入問題
- 擷取 PreparedStatement 對象
// SQL語句中的參數值,使用?占位符替代 String sql = "select * from user where username = ? and password = ?"; // 通過Connection對象擷取,并傳入對應的sql語句 PreparedStatement pstmt = conn.prepareStatement(sql);
-
設定參數值
上面的sql語句中參數使用 ? 進行占位,在執行之前肯定要設定這些 ? 的值。
PreparedStatement對象:setXxx(參數1,參數2):給 ? 指派
- Xxx:資料類型 ; 如 setInt (參數1,參數2)
- 參數:
- 參數1: ?的位置編号,從1 開始
- 參數2: ?的值
- 執行SQL語句
executeUpdate(); 執行DDL語句和DML語句
executeQuery(); 執行DQL語句
注意:
- 調用這兩個方法時不需要傳遞SQL語句,因為擷取SQL語句執行對象時已經對SQL語句進行預編譯了。
3.6.4 使用PreparedStatement改進
@Test
public void testPreparedStatement() throws SQLException {
String url = "jdbc:mysql:///db1?useSSL=false";
String user = "root";
String password = "1234";
Connection conn = DriverManager.getConnection(url, user, password);
//模拟使用者輸入:使用者名和密碼
String name = "hackdshjksdhdjh";
String pwd = "' or '1'='1"; //可怕的注入 直接修改了sql語句的含義
/*name = "lisi";
pwd = "234";*/ //當然也成功啦
//sql語句
String sql = "select * from tb_user where username = ? and password = ?";
//擷取statement對象
PreparedStatement ps = conn.prepareStatement(sql);//預編譯sql(然後插入值,這樣就不用拼接了)
//設定?值
ps.setString(1,name);//設定第一個?的值
ps.setString(2,pwd);//設定第二個?的值
//執行sql
ResultSet rs = ps.executeQuery(); //不用傳入sql了 前面傳過了
//也列印下sql
System.out.println(((JDBC4PreparedStatement)ps).asSql());
//發現整個pwd當做一個字元串處理了('号會被轉義),再也不會修改sql語句了
//判斷登陸是否成功
if(rs.next()){//根據name和pwd能查到資料就說明登陸成功
System.out.println("登陸成功!");
}else {
System.out.println("失敗~");
}
//千萬記得釋放資源
rs.close();
ps.close();
conn.close();
}
特殊字元如
'
被轉義了,再也不會修改sql語句的邏輯了
3.6.5 PreparedStatement原理
PreparedStatement 好處:
- 預編譯SQL,性能更高
- 防止SQL注入:将敏感字元進行轉義
Java代碼操作資料庫流程如圖所示:
- 将sql語句發送到MySQL伺服器端
- MySQL服務端會對sql語句進行如下操作
-
檢查SQL語句
檢查SQL語句的文法是否正确。
-
編譯SQL語句。将SQL語句編譯成可執行的函數。
檢查SQL和編譯SQL花費的時間比執行SQL的時間還要長。如果我們隻是重新設定參數,那麼檢查SQL語句和編譯SQL語句将不需要重複執行。這樣就提高了性能。
sql編譯一次,下次若隻是參數不同就不需要重新編譯,直接換參執行即可,大大提高效率
- 執行SQL語句
-
接下來我們通過查詢日志來看一下原理。
-
開啟預編譯功能
在代碼中編寫url時需要加上以下參數。而我們之前根本就沒有開啟預編譯功能,隻是解決了SQL注入漏洞。
(預設是不開的,得手動開才行,否則就是紙上談兵)
-
配置MySQL執行日志(重新開機mysql服務後生效)
在mysql配置檔案(my.ini)中添加如下配置
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
沒有權限就先複制到桌面,修改儲存好後再複制回去
重新開機看看日志目錄下是否有這兩個.log檔案
- java測試代碼如下
/**
* PreparedStatement原理
*/
@Test
public void testPreparedStatement2() throws SQLException {
String url = "jdbc:mysql:///db1?useSSL=false&useServerPrepStmts=true";
String user = "root";
String password = "1234";
Connection conn = DriverManager.getConnection(url, user, password);
//模拟使用者輸入:使用者名和密碼
String name = "hackdshjksdhdjh";
String pwd = "' or '1'='1"; //可怕的注入 直接修改了sql語句的含義
//sql語句
String sql = "select * from tb_user where username = ? and password = ?";
//擷取statement對象
PreparedStatement ps = conn.prepareStatement(sql);//預編譯sql(然後插入值,這樣就不用拼接了)
//設定?值
ps.setString(1, name);//設定第一個?的值
ps.setString(2, pwd);//設定第二個?的值
//執行sql
ResultSet rs = ps.executeQuery(); //不用傳入sql了 前面傳過了
if (rs.next()) System.out.println("登陸成功!");
else System.out.println("失敗~");
//再執行一次sql
ps.setString(1, "lisi");//設定第一個?的值
ps.setString(2, "234");//設定第二個?的值
rs = ps.executeQuery(); //不用傳入sql了 前面傳過了
//判斷登陸是否成功
if (rs.next()) {//根據name和pwd能查到資料就說明登陸成功
System.out.println("登陸成功!");
} else {
System.out.println("失敗~");
}
//千萬記得釋放資源
rs.close();
ps.close();
conn.close();
}
上面的代碼執行了兩次sql: 一次注入失敗,一次正常登陸成功
- 執行SQL語句(執行上面java代碼就是執行sql),檢視
日志如下:E:\log\mysql.log
MySQL, Version: 5.7.24-log (MySQL Community Server (GPL)). started with:
TCP Port: 0, Named Pipe: (null)
Time Id Command Argument
2023-02-24T14:12:46.862195Z 2 Connect root@localhost on db1 using TCP/IP
2023-02-24T14:12:46.864956Z 2 Query /* mysql-connector-java-5.1.48 ( Revision: 29734982609c32d3ab7e5cac2e6acee69ff6b4aa ) */SELECT @@session.auto_increment_increment AS auto_increment_increment, @@character_set_client AS character_set_client, @@character_set_connection AS character_set_connection, @@character_set_results AS character_set_results, @@character_set_server AS character_set_server, @@collation_server AS collation_server, @@collation_connection AS collation_connection, @@init_connect AS init_connect, @@interactive_timeout AS interactive_timeout, @@license AS license, @@lower_case_table_names AS lower_case_table_names, @@max_allowed_packet AS max_allowed_packet, @@net_buffer_length AS net_buffer_length, @@net_write_timeout AS net_write_timeout, @@performance_schema AS performance_schema, @@query_cache_size AS query_cache_size, @@query_cache_type AS query_cache_type, @@sql_mode AS sql_mode, @@system_time_zone AS system_time_zone, @@time_zone AS time_zone, @@transaction_isolation AS transaction_isolation, @@wait_timeout AS wait_timeout
2023-02-24T14:12:46.875712Z 2 Query SET character_set_results = NULL
2023-02-24T14:12:46.875885Z 2 Query SET autocommit=1
2023-02-24T14:12:46.886850Z 2 Prepare select * from tb_user where username = ? and password = ?
2023-02-24T14:12:46.887324Z 2 Execute select * from tb_user where username = 'hackdshjksdhdjh' and password = '\' or \'1\'=\'1'
2023-02-24T14:12:46.887585Z 2 Execute select * from tb_user where username = 'lisi' and password = '234'
2023-02-24T14:12:46.887825Z 2 Close stmt
2023-02-24T14:12:46.889170Z 2 Quit
上面日志中,前面是通路資料庫連接配接的一些步驟
後面, 倒數第五行中的
Prepare
是對SQL語句進行預編譯。倒數第四行和倒數第三行是執行了兩次SQL語句,而第二次執行前并沒有對SQL進行預編譯。
(去掉預編譯的代碼
&useServerPrepStmts=true
日志裡就沒有Prepare了)
小結:
- 在擷取PreparedStatement對象時,将sql語句發送給mysql伺服器進行檢查,編譯(這些步驟很耗時)
- 執行時就不用再進行這些步驟了,速度更快
- 如果sql模闆一樣,則隻需要進行一次檢查、編譯