ResultSet rs = Statement.executeQuery();
到了此處,我們的JDBC1.0已經寫完了,但是爲什麼說是1.0呢,因爲這個版本有一個很大的漏洞。
沒錯,那就是——依賴注入問題。
=============================華麗麗的分割線==============================
下面我們可以看這個例子
JDBC_Statement.java
public class JDBC_Statement
{
public static void main(String[] args) throws Exception
{
Scanner sc = new Scanner(System.in);
System.out.println("請輸入卡號");
String card_id = sc.nextLine();
System.out.println("請輸入密碼");
String pwd = sc.nextLine();
//1:加載驅動
Class.forName("com.mysql.jdbc.Driver");
//2:獲得數據庫連接
String user = "root";
String password = "root";
String url = "jdbc:mysql://localhost:3306/zdx?serverTimezone=UTC";
Connection conn =
DriverManager.getConnection(url,user,password);
//3:準備sql
String sql = "select
* from account where card_id ="+card_id
+" and password = '"+pwd+"'";
System.out.println(sql);
//4:創建Statement,發送sql
Statement stm = conn.createStatement();//獲的statement對象
//5:如果是查詢,處理結果集
ResultSet rs = stm.executeQuery(sql);
if(rs.next()){
System.out.println("可以取錢了");
}else{
System.out.println("密碼賬戶不正確");
}
//6:關閉連接,釋放資源,原則,先打開後關閉
stm.close();
conn.close();
}
這是一個銀行取錢的例子,如代碼所寫。
數據庫有這樣一張account表,數據和字段如下:
錯誤即提示賬戶不正確。
現在控制檯輸入正確卡號密碼
然後,我輸入錯誤的卡號密碼竟然也可以取錢!輸入錯誤的卡號 “123 or 1=1 -- ”密碼 "zxc"結果如下:
可見依然能訪問數據庫,這對於銀行可是致命性錯誤,密碼錯誤還可以取錢。
這就是依賴注入引起的著名錯誤。
依靠人爲輸入破壞sql結構.
select * from account where id = 1001 or 1=1 -- and password = 'xxx'
這條sql裏,其中--是sql裏的註釋 or 1=1 永遠爲真並且還是or連接,
所以這條sql只執行到 or 1=1 ,1=1又是恆等。所以跳過了密碼。
爲了解決這個問題,我們就要說PreparedStatement!
PreparedStatement簡介及使用
PreparedStatement構建動態SQL,通過PreparedStatement執行SQL語句,解決注入攻擊。
PreparedStatement是Statement的子接口。
通過連接創建PreparedStatement,創建時將SQL語句中發生變化的部分用佔位符“?“ 代替。
功能:和statement一樣,發送sql語句。
但是執行多個同構sql效率高。同構sql能省去①②③步驟。
使用步驟:
1.創建pstm
String sql = "select * from account where card_id = ? and password = ?"
PreparedStateement pstm = conn.prepareStatement(sql);
①驗證權限
②驗證語法
③將sql轉換內部指令
2.綁定參數
pstm.setInt(1,值);
pstm.setString(2,值);
3.發送綁定參數至DB數據庫
pstm.executeUpdate();//曾刪改
pstm.executedQuery();//查詢
④執行內部指令操作數據庫
mysql內部執行sql步驟:
①驗證權限
②驗證語法
③將sql轉換內部指令
④執行內部指令操作數據庫
使用PareparedStatement解決注入攻擊問題後的代碼如下:
JDBC2.0版本 是2.0版本>_< 。後面還會有更優秀的版本娓娓道來!
JDBC_PreparedStatement.java
public class JDBC_PreparedStatement
{
public static void main(String[] args) throws Exception
{
Scanner sc = new Scanner(System.in);
System.out.println("請輸入卡號");
String card_id = sc.nextLine();
System.out.println("請輸入密碼");
String pwd = sc.nextLine();
//1:加載驅動
//Class.forName("com.mysql.jdbc.Driver");
//2:獲得數據庫連接
String user = "root";
String password = "root";
String url = "jdbc:mysql://localhost:3306/zdx?serverTimezone=UTC";
Connection conn =
DriverManager.getConnection(url,user,password);
//3:準備sql
String sql = "select
* from account where card_id =? and password =?";
PreparedStatement pstm = conn.prepareStatement(sql);
pstm.setInt(1,
Integer.valueOf(card_id));
pstm.setString(2, pwd);
//5:如果是查詢,處理結果集
ResultSet rs = pstm.executeQuery();
if(rs.next()){
System.out.println("可以取錢了");
}else{
System.out.println("密碼賬戶不正確");
}
//6:關閉連接,釋放資源,原則,先打開後關閉
rs.close();
pstm.close();
conn.close();
}
現在用PreparedStatement程序執行,輸入卡號,密碼
出現異常,不會再出現密碼錯誤卻取錢的問題了!!!
總結對比一下 Statement 和PreparedStatement
|
Statement
|
PreparedStatement
|
關係
|
父接口
|
子接口
|
安全
|
存在注入隱患
|
解決sql注入
|
效率
|
執行異構sql快
|
執行同構sql快
|
至此JDBC2.0結束。JDBC基本內容結束,以下是JDBC進階內容。和優化版JDBC例子。
=================================華麗麗的分割線===========================
封裝數據訪問對象
1:通過分析總結,所有對數據庫表的操作都可以總結爲通過JDBC對錶的增刪改查,爲了減少冗餘代碼,
使得每次操作表時,不必都寫JDBC程序,所以將對一張表的所有數據訪功能,封裝在數據訪問對象
(Data Access Object)中,方便調用。
2:爲了方便數據傳輸,往往會將java程序中所有相關操作的零散字段值,封裝成一個實體對象--entity。
實體封裝原則:
表----實體類
字段---屬性
實現序列化
提供set,get方法。
以下代碼就是利用Dao數據訪問對象寫出的JDBC程序,利用封裝簡化了JDBC開發步驟。
試想一下,如果,有10個添加,20個刪除的請求,還像JDBC1.0,2.0版本那樣寫的話,要寫10遍,20遍大量相似代碼,
基本一致的代碼。這不僅耗時耗力,也不符合JAVA三大特性。所以,利用Dao數據訪問層,將對錶的常用操作,
封裝成方法,這樣再有10個添加或20個刪除,我們只需要調用10次,20次這個封裝好的添加或刪除方法,
而不用再寫那麼多遍方法,大大簡化了開發工作量。
以下是利用Dao思想實現的JDBC3.0是3.0版本後面還有更好的版本娓娓道來!>_<
JDBC_Dao.java
public class JDBC_Dao
{
/**
* 向account表增加一條記錄
*/
public void add(Account account){
Connection conn = null;
PreparedStatement pstm = null;
try {
//1加載驅動
Class.forName("com.mysql.jdbc.Driver");
//2創建連接
conn =
DriverManager.getConnection(
"jdbc:mysql://localhost:3306/zdx?serverTimezone=UTC",
"root",
"root");
//3準備sql
String sql = "insert
into account values(?,?,?,?)";
//4創建Statement,發送sql
pstm = conn.prepareStatement(sql);
pstm.setString(1,account.getCardId());
pstm.setString(2, account.getPassword());
pstm.setDouble(3, account.getBalance());
pstm.setString(4, account.getPhone());
int i = pstm.executeUpdate();
//5如果是查詢的話,處理結果集
} catch (Exception e)
{
e.printStackTrace();
}
finally{
//6釋放資源
try {
pstm.close();
conn.close();
} catch (SQLException e)
{
e.printStackTrace();
}
}
}
/**
* 向account表查詢一條記錄
* @param account
* @return
*/
public Account
query(Account account){
Connection conn = null;
PreparedStatement pstm =null;
ResultSet rs = null;
try {
//1加載驅動
Class.forName("com.mysql.jdbc.Driver");
//2創建連接
conn =
DriverManager.getConnection(
"jdbc:mysql://localhost:3306/zdx?serverTimezone=UTC",
"root",
"root");
//3準備sql
String sql = "select
* from account where "
+ "card_id = ? and password = ?";
//4創建Statement發送語句
pstm = conn.prepareStatement(sql);
pstm.setString(1, account.getCardId());
pstm.setString(2, account.getPassword());
rs = pstm.executeQuery();
//5處理結果集
while (rs.next())
{
account.setCardId(rs.getString("card_id"));
account.setPassword(rs.getString("password"));
account.setBalance(rs.getDouble("balance"));
account.setPhone(rs.getString("phone"));
}
} catch (Exception e)
{
e.getStackTrace();
}
finally{
//6釋放資源
try {
rs.close();
pstm.close();
conn.close();
} catch (SQLException e)
{
e.printStackTrace();
}
}
return account;
}
以上代碼依舊使用account表,需要再寫一個實體對象用來承載數據,一個test類用來調用方法,
,這些最基礎的,這裏就不贅述了,不清楚的同學就要自行參閱java語言了。
如果我查詢A卡,B卡,C卡,三張銀行卡信息,按照JDBC2.0要寫三個查詢方法,現在,只需要把參數傳遞過去,
調用三次query()方法就好了!
總結:可見利用Dao數據訪問層封裝JDBC常用方法,可以大大簡化方法步驟,不用重複寫方法,只需重複調用。
這就是JDBC3.0版本
=============================華麗麗的分割線==============================
但是仔細的讀者一定發現了,這裏還存在不少缺陷,沒錯,我們還可以改進它。
在JDBC3.0版本里,可以發現,查詢,添加方法,存在大量冗餘代碼,比如:
①同的加載驅動,
②相同的創建連接,
③相同的釋放資源。
在上個版本的代碼裏我只寫了添加查詢方法,如果還有刪除,修改,查詢所有等方法呢,
沒錯這些方法,也存在相同的創建連接,釋放鏈接。找見了問題,就好解決了。
那麼解決的辦法還是----封裝。
我們可以嘗試把1註冊驅動,2創建連接,6釋放資源,這三個步驟做成工具類-----JDBCutil
這樣,我們在Dao層裏面的JDBC方法,在遇到1,2,6等步驟時,不用再去寫代碼,只需調用封裝好的工具即可。
沒錯程序員都是很懶得!
以下是JDBC4.0是4.0版本,後面還有更完善的版本娓娓道來!
public class JDBC_Util
{
/**
* @return 返回鏈接
*/
public static Connection
getConnection() throws Exception
{
Connection conn = null;
Class.forName("com.mysql.jdbc.Driver");
conn = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/zdx?serverTimezone=UTC",
"root",
"root");
return conn;
}
/**
* 釋放資源
*/
public static void release(ResultSet rs,
Statement stm,
Connection conn){
try {
if(rs!=null){rs.close();}
if(stm!=null){stm.close();}
if(conn!=null){conn.close();}
} catch (SQLException e)
{
e.printStackTrace();
}
}
public class JDBC_Dao2
{
/**
* 向account表增加一條記錄
*/
public void add(Account account){
Connection conn = null;
PreparedStatement pstm = null;
try {
conn =
JDBC_Util.getConnection();
//3準備sql
String sql = "insert
into account values(?,?,?,?)";
//4創建Statement,發送sql
pstm = conn.prepareStatement(sql);
pstm.setString(1,account.getCardId());
pstm.setString(2, account.getPassword());
pstm.setDouble(3, account.getBalance());
pstm.setString(4, account.getPhone());
int i = pstm.executeUpdate();
//5如果是查詢的話,處理結果集
} catch (Exception e)
{
e.printStackTrace();
}
finally{
//6釋放資源
JDBC_Util.release(null, pstm, conn);
}
}
/**
* 向account表查詢一條記錄
* @param account
* @return
*/
public Account
query(Account account){
Connection conn = null;
PreparedStatement pstm =null;
ResultSet rs = null;
try {
conn =
JDBC_Util.getConnection();
//3準備sql
String sql = "select
* from account where " + "card_id
= ? and password = ?";
//4創建Statement發送語句
pstm = conn.prepareStatement(sql);
pstm.setString(1, account.getCardId());
pstm.setString(2, account.getPassword());
rs = pstm.executeQuery();
//5處理結果集
while (rs.next())
{
account.setCardId(rs.getString("card_id"));
account.setPassword(rs.getString("password"));
account.setBalance(rs.getDouble("balance"));
account.setPhone(rs.getString("phone"));
}
} catch (Exception e)
{
e.getStackTrace();
}
finally{
//6釋放資源
JDBC_Util.release(rs, pstm, conn);
}
return account;
}
細心地讀者會發現在代碼裏原本創建連接和釋放資源的位置都變成了方法調用。
conn =
JDBC_Util.getConnection();
JDBC_Util.release(rs, pstm, conn);
4.0版本通過工具類調用的方式進一步精簡了代碼,那麼4.0版本還有沒有缺陷了呢。
=============================華麗麗的分割線=================================
對於JDBC_Util.java來說,還有許多不足。
1:
conn = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/zdx?serverTimezone=UTC",
"root",
比如創建連接的方式,是在代碼裏寫死的用戶名和密碼,以及連接url,而java文件運行的時候,會編譯成class文件
也就是說,JDBC_Util最終會是JDBC_Util.class文件,那麼一旦數據庫改變密碼,用戶名,或改換數據庫,
整個文件還需要重新編譯執行。對此,我們可以把經常變動的代碼放到properties配置文件裏去。
2:每次調用獲得連接的方法都需要加載驅動,
Class.forName("com.mysql.jdbc.Driver");
調用10次則加載10次,大大浪費了JVM內存,其實對於加載驅動只需要加載一次,我們可以嘗試把加載驅動放到
靜態代碼塊裏。靜態代碼塊在類加載時執行,只執行一次。
properties配置文件簡介及使用:
1: InputStream is = new FileInputStream("配置文件路徑");
BufferedReader br = bew BufferedReader(new InputStramReader(is));
String as = br.readLine();
2:properties 是Map的實現類:
1:獲得配置文件的輸出流。
2:調用load(is);加載配置文件裏的信息至Properties對象中。
下面的JDBC5.0版本是對JDBC_Util的改進。採用了靜態代碼塊加讀取配置文件的優化方案。
包括test.java,JDBC_Util2.java,JDBC_Dao2.java,properties文件,共四個。
JDBC_Uril2.java
public class JDBC_Util2
{
private static final Properties prop = new Properties();
static{
InputStream is = null;
try {
is =
JDBC_Util2.class.getResourceAsStream("jdbc.properties");
prop.load(is);
String driverName = prop.getProperty("driverName");
Class.forName(driverName);
} catch (Exception e)
{
e.printStackTrace();
}
}
/**
* @return 返回鏈接
*/
public static Connection
getConnection() throws Exception
{
Connection conn = null;
String user = prop.getProperty("user");
String password = prop.getProperty("password");
String url = prop.getProperty("url");
conn =
DriverManager.getConnection(url,user,password);
return conn;
}
/**
* 釋放資源
*/
public static void release(ResultSet rs,
Statement stm,
Connection conn){
try {
if(rs!=null){rs.close();}
if(stm!=null){stm.close();}
if(conn!=null){conn.close();}
} catch (SQLException e)
{
e.printStackTrace();
}
}
public class JDBC_Dao2
{
/**
* 向account表增加一條記錄
*/
public void add(Account account){
Connection conn = null;
PreparedStatement pstm = null;
try {
conn =
JDBC_Util2.getConnection();
//3準備sql
String sql = "insert
into account values(?,?,?,?)";
//4創建Statement,發送sql
pstm = conn.prepareStatement(sql);
pstm.setString(1,account.getCardId());
pstm.setString(2, account.getPassword());
pstm.setDouble(3, account.getBalance());
pstm.setString(4, account.getPhone());
int i = pstm.executeUpdate();
//5如果是查詢的話,處理結果集
} catch (Exception e)
{
e.printStackTrace();
}
finally{
//6釋放資源
JDBC_Util2.release(null, pstm, conn);
}
}
/**
* 向account表查詢一條記錄
* @param account
* @return
*/
public Account
query(Account account){
Connection conn = null;
PreparedStatement pstm =null;
ResultSet rs = null;
try {
conn =
JDBC_Util2.getConnection();
//3準備sql
String sql = "select
* from account where "
+ "card_id = ? and password = ?";
//4創建Statement發送語句
pstm = conn.prepareStatement(sql);
pstm.setString(1, account.getCardId());
pstm.setString(2, account.getPassword());
rs = pstm.executeQuery();
//5處理結果集
while (rs.next())
{
account.setCardId(rs.getString("card_id"));
account.setPassword(rs.getString("password"));
account.setBalance(rs.getDouble("balance"));
account.setPhone(rs.getString("phone"));
}
} catch (Exception e)
{
e.getStackTrace();
}
finally{
//6釋放資源
JDBC_Util2.release(rs, pstm, conn);
}
return account;
}
}
jdbc.properties文件
driverName=com.mysql.jdbc.Driver
user=root
password=root
url=jdbc:mysql://localhost:3306/zdx?serverTimezone=UTC
Test_Dao.java
public class Test_Dao
{
public static void main(String[] args)
{
JDBC_Dao2 jd = new JDBC_Dao2();
Account account = new Account("10004","44444",99,"12345678900");
jd.add(account);
}
解決了依賴注入,冗餘代碼,資源浪費等問題之後,JDBC5.0完畢。這個版本可以算得上是較爲完善的版本了,但是還有瑕疵。
後面關於JDBC的問題涉及到事務,以及MVC模式,這裏由於篇幅問題,無法細細詳談,但我會竟可能詳細的寫。
請小夥伴自行閱讀有關oracle事務一章節,還有有關mvc編程思想書籍。
=================================華麗麗的分割線=========================
JDBC6.0最終版。
數據訪問層Dao
業務邏輯層service
我們在Dao層中封裝了對錶的常用操作,增刪改查。
我們在Util裏封裝了JDBCUtil工具類解決冗餘問題。
現在我們有一個銀行轉賬問題:
1.根據卡號,密碼,先查詢
2.轉出賬戶再餘額足夠的情況下,減去轉出資金。
3轉入賬戶添加轉入資金。
對於2,3步驟我們因當把他們看作是一個事務,事務的原子性,一致性,隔離性。要求
要麼一起成功,要麼一起不成功。當然從現實考慮,也確實應當這樣,如果在2,3步驟之間失敗了
就回退。
這整個轉賬邏輯,我們可以稱之爲業務。所以針對軟件的三層結構。我們把這部分稱爲-----業務層(service)
在service層調用Dao層的方法。
而在service層裏的連接對象和Dao層裏的連接對象不是同一個對象,這就會造成混亂,打破事務的一致性。
從而導致,轉出錢減去了,轉入賬戶沒加錢。
Service方法中的事務控制失敗。
原因:service控制事務時過的conn和DAO訪問數據庫使用的conn時兩個不同對象
解決:
保證service和Dao使用同一個conn對象
1:通過參數傳遞。
2:每個線程對象有一個Map屬性,可以存儲數據
在service獲得conn,將conn放入當前Thread對象,在DAO訪問數據庫時取時,從Thread取出conn
ThreadLocal簡介及使用:
操作當前線程對象中的一小塊空間。
1創建ThreadLocal對象 ThreadLocal<Connection> tdl = new ThreadLocal<Connection>();
2tdl.set(conn);將conn添加到當前線程。
3tdl.get();獲得當前線程中的數據
4tdl.remove()移除當前線程裏的數據
話不多說我們來看代碼有三個文件 JDBC_Util3.java JDBC_Dao3.java JDBC_Service.java
JDBC_Util3.java
public class JDBC_Util3
{
private static final Properties prop = new Properties();
private static final ThreadLocal<Connection> tdl = new ThreadLocal<Connection>();
static{
InputStream is = null;
try {
is =
JDBC_Util3.class.getResourceAsStream("jdbc.properties");
prop.load(is);
String driverName = prop.getProperty("driverName");
Class.forName(driverName);
} catch (Exception e)
{
e.printStackTrace();
}
}
/**
* @return 返回鏈接
*/
public static Connection getConnection() throws Exception
{
Connection conn = null;
conn = tdl.get();//獲得當前線程連接
if(conn == null){
//說明當前線程沒有conn
String user = prop.getProperty("user");
String password = prop.getProperty("password");
String url = prop.getProperty("url");
conn =
DriverManager.getConnection(url,user,password);
tdl.set(conn);
}
return conn;
}
/**
* 釋放資源
*/
public static void release(ResultSet rs,Statement stm,Connection conn){
try {
if(rs!=null){rs.close();}
if(stm!=null){stm.close();}
if(conn!=null){
conn.close();
tdl.remove();//移除當前線程對象中的conn
}
} catch (SQLException e)
{
e.printStackTrace();
}
}
JDBC_Dao3.java
public class JDBC_Dao3
{
/**
* 更新賬戶
* @param toAcc
*/
public void upDateAccount(Account toAcc)
{
Connection conn = null;
PreparedStatement pstm = null;
try {
conn =
JDBC_Util3.getConnection();
String sql = "update
account set card_id=?,"
+ "password=?,balance=?,phone=?
where card_id=?";
pstm = conn.prepareStatement(sql);
pstm.setString(1, toAcc.getCardId());
pstm.setString(2, toAcc.getPassword());
pstm.setDouble(3, toAcc.getBalance());
pstm.setString(4, toAcc.getPhone());
pstm.setString(5, toAcc.getCardId());
int i
= pstm.executeUpdate();
} catch (Exception e)
{
e.printStackTrace();
}finally{
JDBC_Util.release(null, pstm, null);
}
}
/**
* 查詢賬戶
* @param toCardId
* @return
*/
public Account
queryAccount(Integer toCardId) {
Connection conn = null;
Statement stm = null;
ResultSet rs = null;
Account acc = null;
try {
conn =
JDBC_Util3.getConnection();
stm = conn.createStatement();
String sql = "select
* from account where card_id = "
+ toCardId;
System.out.println(sql);
rs = stm.executeQuery(sql);
while (rs.next())
{
acc = new Account();
acc.setCardId(rs.getString("card_id"));
acc.setPassword(rs.getString("password"));
acc.setBalance(rs.getDouble("balance"));
acc.setPhone(rs.getString("phone"));
}
} catch (Exception e)
{
}finally{
JDBC_Util.release(rs, stm, null);
}
return acc;
}
}
JDBC_Service.java
public class JDBC_Service
{
public void transfer(Integer fromCardId,String password,
Integer toCardId,Double money){
Connection conn = null;
try{
conn =
JDBC_Util2.getConnection();
//事務自動提交關閉
conn.setAutoCommit(false);
JDBC_Dao3 dao = new JDBC_Dao3();
//驗證卡號是否存在
Account fromAcc = dao.queryAccount(fromCardId);
if(fromAcc == null){
throw new RuntimeException("卡號不存在");
}
//驗證密碼是否正確
if(null==password||!password.equals(fromAcc.getPassword())){
throw new RuntimeException("密碼錯誤");
}
//驗證餘額
if(fromAcc.getBalance()<money){
throw new RuntimeException("餘額不足");
}
//轉出賬戶更新
fromAcc.setBalance(fromAcc.getBalance()-money);
dao.upDateAccount(fromAcc);
Account toAcc = dao.queryAccount(toCardId);
//驗證到賬卡號
if(toAcc ==null){
throw new RuntimeException("到賬卡號不存在");
}
//轉入賬戶更新
toAcc.setBalance(toAcc.getBalance()+money);
dao.upDateAccount(toAcc);
//提交事務
conn.commit();
}catch(Exception e){
e.printStackTrace();
try {
conn.rollback();
} catch (SQLException e1)
{
e1.printStackTrace();
}
}finally{
JDBC_Util.release(null, null, conn);
}
}
最後main函數測試
JDBC_Service sv = new JDBC_Service();
sv.transfer(10002, "123321",
10004, 30.00);
轉賬成功:1004:99+30 10002:99-30
=========================華麗麗的終結=====================================
總結:雖然由於快節奏的開發,編程速度的追求,越愛越多的MVC框架出現,比如持久層的hibernate,
mybatis等等,他們對Dao層的支持都很強大,既 快速,又簡便。但是他們的底層同樣是使用了JDBC,
爲了追求高速簡便,我們可以不使用JDBC,但一定要了解JDBC。瞭解JDBC也有助於學習其他持久層框架。
以上就是我對JDBC全部心得。
限於文章篇幅原因,這裏僅僅介紹冰山一角。由於筆者的水平有限,編寫時間也很倉促,
文中難免會出現一些錯誤或者不準確的地方,不妥之處懇請讀者批評指正。