者:面包君
鏈接:https://zhuanlan.zhihu.com/p/25214682
來源:知乎
著作權歸作者所有。商業(yè)轉載請聯(lián)系作者獲得授權,非商業(yè)轉載請注明出處。
最近在寫風控方面的內容,涉及到一些怎么設置網站的用戶登陸安全、投資安全、運營安全方面的內容時,正好想起來去年的“月餅門”事件。對于碼農來說,寫個程序實現(xiàn)腳本搶標,這樣的事情其實很簡單。正好借著最近Python折騰代碼的機會,整理下怎么通過Python來實現(xiàn)搶火車票、紅包這些。
需要的工具和組件有:
瀏覽器驅動ChromeDriver http://chromedriver.storage.googleapis.com/index.html?path=2.20/
Python 3.5
Splinter 執(zhí)行:pip install splinter安裝Splinter即可
重點介紹splinter怎么使用?
>>> from splinter.browser import Browser
>>> xx=Browser(driver_name="chrome")
>>> xx.visit("http://www.zhihu.com/")
介紹幾個常用功能:
1. 輸入:xx.fill("wd", "dataman")即可在搜索欄搜索dataman。
2. 輸入:button=xx.find_by_value(u"提問")
button=xx.find_by_id(u"zu-top-add-questionSBSBSBSBSBSBSB")尋找該按鈕
(通過快捷鍵F12查詢)
3. 輸入:button.click() 點擊該按鍵
下面用12306搶火車票/京東搶手機來示例下:
#12306秒搶Python代碼from splinter.browser import Browserx=Browser(driver_name="chrome")url=“https://kyfw.12306.cn/otn/leftTicket/init”x=Browser(driver_name="chrome")x.visit(url)#填寫登陸賬戶、密碼x.find_by_text(u"登錄").click()x.fill("loginUserDTO.user_name","your login name")x.fill("userDTO.password","your password")#填寫出發(fā)點目的地x.cookies.add({"_jc_save_fromStation":"%u4E0A%u6D77%2CSHH"})x.cookies.add({"_jc_save_fromDate":"2016-01-20"})x.cookies.add({u'_jc_save_toStation':'%u6C38%u5DDE%2CAOQ'})#加載查詢x.reload()x.find_by_text(u"查詢").click()#預定x.find_by_text(u"預訂")[1].click()#選擇乘客x.find_by_text(u"數(shù)據(jù)分析俠")[1].click()
#-*- coding:utf-8 -*-#京東搶手機腳本from splinter.browser import Browserimport time#登錄頁def login(b): #登錄京東 b.click_link_by_text("你好,請登錄") time.sleep(3) b.fill("loginname","account*****") #填寫賬戶密碼 b.fill("nloginpwd","passport*****") b.find_by_id("loginsubmit").click() time.sleep(3) return b#訂單頁def loop(b): #循環(huán)點擊 try: if b.title=="訂單結算頁 -京東商城": b.find_by_text("保存收貨人信息").click() b.find_by_text("保存支付及配送方式").click() b.find_by_id("order-submit").click() return b else: #多次搶購操作后,有可能會被轉到京東首頁,所以要再打開手機主頁 b.visit("http://item.jd.com/2707976.html") b.find_by_id("choose-btn-qiang").click() time.sleep(10) loop(b) #遞歸操作 except Exception as e: #異常情況處理,以免中斷程序 b.reload() #重新刷新當前頁面,此頁面為訂單提交頁 time.sleep(2) loop(b) #重新調用自己b=Browser(driver_name="chrome") #打開瀏覽器b.visit("http://item.jd.com/2707976.html")login(b)b.find_by_id("choose-btn-qiang").click() #找到搶購按鈕,點擊time.sleep(10) #等待10secwhile True: loop(b) if b.is_element_present_by_id("tryBtn"): #訂單提交后顯示“再次搶購”的話 b.find_by_id("tryBtn").click() #點擊再次搶購,進入讀秒5,跳轉訂單頁 time.sleep(6.5) elif b.title=="訂單結算頁 -京東商城": #如果還在訂單結算頁 b.find_by_id("order-submit").click() else: print('恭喜你,搶購成功') break
系列文章旨在記錄和總結自己在Java Web開發(fā)之路上的知識點、經驗、問題和思考,希望能幫助更多(Java)碼農和想成為(Java)碼農的人。
上篇文章介紹了數(shù)據(jù)庫應用的開發(fā)步驟,一般包括數(shù)據(jù)庫選型、數(shù)據(jù)庫設計、編寫數(shù)據(jù)庫訪問代碼三個步驟。而數(shù)據(jù)庫選型又因開發(fā)環(huán)境、測試環(huán)境和生產環(huán)境的不同而不同。
所以,我們在編寫數(shù)據(jù)庫訪問代碼時,要盡量使用符合SQL標準的語法。關于SQL和SQL標準知識,大家可以自行搜索。
因為我們的租房網應用只是用作演示,所以它的數(shù)據(jù)庫選型和數(shù)據(jù)庫設計就從簡而行,我就直接使用H2Database(簡稱H2,可以參考這篇文章)這個數(shù)據(jù)庫作為JDBC的驅動。
要想了解JDBC如何使用,那最權威的資料莫過于JDBC規(guī)范了。
JDBC規(guī)范的下載與Servlet等規(guī)范類似,都可以在JCP官網(https://jcp.org/en/home/index)找到,具體如何下載可以參考這篇文章。
截止到本文成稿之日,JDBC規(guī)范的最新版本好像是4.3,對應JSR221,是在2017年完成的。
JDBC規(guī)范的內容也不算少,足足有二百多頁,包括:
由于涉及內容太多,我們只能介紹JDBC最基本的用法。
上篇文章的結尾我們提到過,JDBC僅僅是一套接口,它的使用模式與接口的普遍性使用模式是類似的。
租房網應用作為使用JDBC的外部程序,它也需要配置一個具體的JDBC實現(xiàn)才行,這就是JDBC驅動。基本上,市面上各個數(shù)據(jù)庫(特別是關系數(shù)據(jù)庫)廠商都會提供自己的JDBC驅動,H2也不會例外。
那如何在租房網應用中添加H2的JDBC驅動的JAR包呢?我們當然可以到H2官網(http://www.h2database.com/html/main.html)去下載它,然后在IDE中配置第三方依賴庫(可以參考這篇文章)。
不對,我們不是學習過Maven了嗎?可以讓Maven來幫助我們管理依賴啊。我們在這篇文章中也已經把租房網應用改造成使用Maven來管理依賴了,比如Spring等依賴。我們當然也可以直接使用Maven來添加H2的JDBC驅動這個依賴啊。
H2的所有東西都在一個JAR包里,當然也包括了它的JDBC驅動了,我們在這篇文章中下載的JAR包是 h2-1.4.200.jar 。
現(xiàn)在,我們像添加Spring依賴那樣先到Maven倉庫中(https://mvnrepository.com/ )搜索H2這個依賴,后面的在POM文件中添加依賴等具體步驟就不再贅述了,可以參考這篇文章。
H2的依賴在Maven中的描述是這樣的:
<!-- https://mvnrepository.com/artifact/com.h2database/h2 --> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <version>1.4.200</version> <scope>test</scope> </dependency>
將它復制到我們租房網應用的POM文件中,這樣,添加H2的JDBC驅動的JAR包就一切大功告成了,當然,你必須確保你的機器能夠連接上互聯(lián)網,因為Maven會自動到Maven倉庫中下載。
其他數(shù)據(jù)庫的JDBC驅動的JAR包也可以同樣方式添加到我們的租房網應用中。
在使用JDBC的外部程序(這里就是我們的租房網應用)配置一個JDBC驅動,首先要有該驅動,上面已經解決這個問題了。
然后需要在外部程序中注冊該驅動的驅動器類。如何注冊呢?事實上,滿足 JDBC 4 規(guī)范的驅動會自動注冊到外部程序中,而我們添加的H2的JDBC驅動是符合 JDBC 4 規(guī)范的。因此,這一步就可以跳過了。
事實上,JDBC驅動的這種自動注冊機制是使用了JAR規(guī)范的一個特性,包含 META-INF/services/java.sql.Driver 文件的JAR文件可以自動注冊驅動器類。我們可以用壓縮/解壓縮工具打開 h2.1.4.200.jar 看看是否有這么個文件,當然,在Eclipse中可以直接看到:
不過,還是介紹一下如何解決JDBC驅動不能自動注冊的話該如何手動注冊的問題。有兩個方法:
Class.forName("org.h2.Driver");
Java -Djdbc.drivers=org.h2.Driver 外部程序的名字
當然,這種方式的變種是在外部程序中編寫API接口來設置這個屬性:
System.setProperty("jdbc.drivers", "org.h2.Driver");
假設我們的H2的JDBC驅動是不能自動注冊的,那么我們應該手動注冊它(當然,手動注冊也是沒有任何問題的,只不過是多此一舉而已)。
那么,應該在哪個地方添加手動注冊的代碼呢?因為目前我們只有在房源服務(HouseService類)里使用到了模擬的房源數(shù)據(jù),而未來我們是要將房源數(shù)據(jù)持久化到數(shù)據(jù)庫中的,所以我們暫且把它加到這個類中吧。
與模擬的房源數(shù)據(jù)一樣,把注冊JDBC驅動器類的代碼放到HouseService類的構造函數(shù)中:
public HouseService() throws ClassNotFoundException { Class.forName("org.h2.Driver"); mockHouses=new ArrayList<House>(); mockHouses.add(new House("1", "金科嘉苑3-2-1201", "詳細信息")); mockHouses.add(new House("2", "萬科橙9-1-501", "詳細信息")); }
當添加 Class.forName() 這一句代碼后,可以看到Eclipse自動提示有編譯錯誤:
Unhandled exception type ClassNotFoundException
這是因為Class類的forName()方法會拋出必須要我們處理的異常,如果不處理它,那就繼續(xù)拋給調用者,因此,我們在構造方法的聲明中添加了 throws ClassNotFoundException 這一部分,關于Java異常,我們以后詳細介紹。
好,現(xiàn)在讓我們在Eclipse中發(fā)布租房網應用到Tomcat,并啟動Tomcat(可以參考這篇文章),然而在Eclipse的Console(控制臺)窗口中卻看到眾多異常,這些異常形成一條異常鏈(即最底層的異常拋到外部調用者后,外部調用者捕獲后包裝成另一個異常再拋給它的外部調用者,如此重復下去,直到最頂層異常被處理后不再往外拋出):
可以看到,最底層的異常就是 Class.forName() 因為找不到 org.h2.Driver 這個類而拋出的異常,隨后導致 HouseService 的構造函數(shù)拋出異常,因此不能實例化 HouseService ,再導致Spring IoC框架找不到 HouseService 實例來注入到 HouseRenterController 這個Bean,最后導致 dispatcher 這個Servlet初始化失敗。
那到底是何原因找不到H2的JDBC驅動器類 org.h2.Driver 呢?原來是POM文件中的H2依賴的配置在作怪,我們必須把 <scope> 標簽的內容設置為 compile ,或直接去掉該標簽
<!-- https://mvnrepository.com/artifact/com.h2database/h2 --> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <version>1.4.200</version> <!-- <scope>test</scope> 必須去掉此標簽--> </dependency>
從標簽的名字可以得知它是設置某種范圍的,配置為 test ,則表示該依賴只在執(zhí)行測試時有效;配置為 compile (此為默認值),則表示該依賴在執(zhí)行編譯時有效(實際上是任何時候都有效,是程度最強的依賴)。
關于Maven的依賴的相關知識,以后再詳細介紹。
現(xiàn)在,我們重新發(fā)布應用,啟動Tomcat,可以發(fā)現(xiàn)一切正常矣!
接下來,我們就可以使用JDBC API來建立與數(shù)據(jù)庫的連接(數(shù)據(jù)庫往往單獨部署在獨立的數(shù)據(jù)庫服務器上,因此,需要通過網絡來訪問數(shù)據(jù)庫)。
JDBC API主要被包含在 java.sql 和 javax.sql 這兩個包中,在JDK的 rt.jar 中可以找到它們,屬于Java的標準(運行時)庫。
使用JDBC API中的 DriverManager 類的 getConnection() 方法即可建立與數(shù)據(jù)庫的連接。
getConnection() 方法有多個重載版本(關于重載,可以參考這篇文章),其中一個版本就是:
Connection getConnection(String url, String user, String password) throws SQLException
它返回一個數(shù)據(jù)庫連接,但需要傳入一些參數(shù),一般情況下,連接數(shù)據(jù)庫最基本的要求是要知道如下信息:
用戶名和密碼都有對應的參數(shù),是字符串類型的,顯然是由DBA為我們分配的。那這個數(shù)據(jù)庫URL該怎么寫呢?
既然我們用的是H2,那我們到它的官網上肯定能找到答案吧。否則的話,我們得到它卻不知如何使用,那H2的作者圖個啥!費過一番九牛二虎之力后,終于在其官網(http://www.h2database.com/html/main.html)的左側導航欄中的 Features 中找到了相關內容:
它支持的URL遠比我們想象的要復雜的多,但基本格式都是 jdbc:h2: 開頭,前綴 jdbc: 實際上是JDBC規(guī)范規(guī)定的格式,后面的 h2: 當然是指H2Database這個數(shù)據(jù)庫了。
可以看到,H2為我們提供了多種模式,我們可以選擇適合我們的模式。但對我們租房網應用來說,或者說對于在開發(fā)階段的應用來說,使用嵌入式模式或者內存模式是最簡單的,因為我們無需單獨部署數(shù)據(jù)庫服務器。我們這里就直接使用嵌入式模式吧:jdbc:h2:~/h2db/houserenter 。這里沒有數(shù)據(jù)庫服務器的IP地址和端口,有的只是一個路徑,最后的houserenter就是一個文件,它就代表一個數(shù)據(jù)庫,實際的數(shù)據(jù)就存在該文件中。當然,文件的格式我們是不知道的也不用知道。
那用戶名和密碼是什么呢?實際上,這里可以隨便填寫。但在測試環(huán)境和生產環(huán)境中,或者數(shù)據(jù)庫是獨立部署的環(huán)境中,用戶名和密碼是由DBA負責分配的。
現(xiàn)在,我們的HouseService類的構造函數(shù)變成:
public HouseService() throws ClassNotFoundException, SQLException { Class.forName("org.h2.Driver"); String url="jdbc:h2:~/h2db/houserenter"; String user="sa"; String password=""; Connection conn=DriverManager.getConnection(url, user, password); mockHouses=new ArrayList<House>(); mockHouses.add(new House("1", "金科嘉苑3-2-1201", "詳細信息")); mockHouses.add(new House("2", "萬科橙9-1-501", "詳細信息")); }
注意,getConnection()方法也會拋出必須處理的異常 SQLException ,我這選擇直接拋給HouseService類的構造函數(shù)的調用者。
現(xiàn)在,讓我們重新發(fā)布應用并啟動Tomcat,驗證一下是否有問題,結果正常,我們也可以到 ~/h2db (波浪線實際是指你登錄操作系統(tǒng)的用戶的家目錄,在Windows系統(tǒng)中,一般是 C:\Users\用戶名\)這個目錄下看看是否存在 houserenter 這個文件:
數(shù)據(jù)庫的運維,即數(shù)據(jù)庫服務器的部署和搭建,數(shù)據(jù)庫、表、索引等的創(chuàng)建、授權,數(shù)據(jù)的備份和遷移,數(shù)據(jù)的分庫分表分區(qū)等工作。這些工作往往有專門人員來負責,就是數(shù)據(jù)庫管理員,即DBA。
我們做好數(shù)據(jù)庫選型和設計之后,在測試環(huán)境和生產環(huán)境中(有的公司開發(fā)環(huán)境也需要),一般是向DBA申請服務器資源,讓他們部署和搭建數(shù)據(jù)庫服務器,建庫建表等。
當然,你也可以自己一人包攬所有這些工作。
最后,一定是啟動數(shù)據(jù)庫服務器,這樣我們的數(shù)據(jù)庫應用才能建立數(shù)據(jù)庫連接,當然必須保證數(shù)據(jù)庫URL是正確的。即數(shù)據(jù)庫連接能夠建立的條件必須包括:
而H2支持嵌入式模式,則沒有諸多限制,所以在開發(fā)環(huán)境中使用是最合適不過了!
現(xiàn)在,連接已經建立好了,我們就可以用它來執(zhí)行SQL語句了。不過,Connection接口并沒有執(zhí)行SQL的方法,而是提供了一個創(chuàng)建Statement對象的方法:
Statement createStatement() throws SQLException
而Statement接口擁有眾多執(zhí)行SQL的方法,最常用的有兩個:
int java.sql.Statement.executeUpdate(String sql) throws SQLException ResultSet java.sql.Statement.executeQuery(String sql) throws SQLException
正如方法名字所示,executeUpdate()方法主要是用來執(zhí)行insert、update或者delete等數(shù)據(jù)更新(寫)操作的SQL語句的(這些語句叫數(shù)據(jù)操縱語言,即Data Manipulation Language,簡稱DML)。當然,它還可以執(zhí)行建立、刪除數(shù)據(jù)表等操作的SQL語句(這些語句叫數(shù)據(jù)定義語言,即Data Definition Language,簡稱DDL)。
而executeQuery()方法是用來執(zhí)行select等數(shù)據(jù)查詢操作的SQL語句的(也屬于DML)。它返回查詢結果集,我們就可以使用ResultSet接口來訪問查詢結果了。
大家可以通過IDE或者其他方式查看它們的JavaDoc。
我們之前建立的數(shù)據(jù)庫連接中使用的是 jdbc:h2:~/h2db/houserenter (簡稱houserenter數(shù)據(jù)庫),這個庫中也是H2剛剛建立的,因此該庫中還沒有任何數(shù)據(jù)表,所以我們要先使用 create table 語句建立數(shù)據(jù)表。
根據(jù)上篇文章我們對租房網應用的數(shù)據(jù)庫設計,房源表包含三個字段,于是執(zhí)行SQL的代碼如下:
statement.executeUpdate("create table house(id varchar(20) primary key, name varchar(100), detail varchar(500))");
但是,如果我們把它放到HouseService類的構造函數(shù)的話,豈不是每次啟動應用時都要建立一次house表,大家可以試試,這樣的話第二次啟動應用時會拋出house表已經存在的異常。
那該怎么辦呢?我們可以在每次啟動應用時都使用 drop table 語句將原來的house表刪除,然后再執(zhí)行建表語句:
statement.executeUpdate("drop table if exists house");
接著,我們再使用 insert 語句插入模擬的房源數(shù)據(jù)(因為我們的租房網應用還沒有提供發(fā)布房源的功能):
statement.executeUpdate("insert into house values('1', '金科嘉苑3-2-1201', '詳細信息') "); statement.executeUpdate("insert into house values('2', '萬科橙9-1-501', '詳細信息') ");
現(xiàn)在,我們的HouseService類的構造方法變成這樣:
public HouseService() throws ClassNotFoundException, SQLException { Class.forName("org.h2.Driver"); String url="jdbc:h2:~/h2db/houserenter"; String user="sa"; String password=""; Connection conn=DriverManager.getConnection(url, user, password); Statement statement=conn.createStatement(); statement.executeUpdate("drop table if exists house"); statement.executeUpdate("create table house(id varchar(20) primary key, name varchar(100), detail varchar(500))"); statement.executeUpdate("insert into house values('1', '金科嘉苑3-2-1201', '詳細信息') "); statement.executeUpdate("insert into house values('2', '萬科橙9-1-501', '詳細信息') "); mockHouses=new ArrayList<House>(); mockHouses.add(new House("1", "金科嘉苑3-2-1201", "詳細信息")); mockHouses.add(new House("2", "萬科橙9-1-501", "詳細信息")); }
executeUpdate()方法的返回值是一個整型值,表示執(zhí)行該SQL語句后受影響的行數(shù),大家可以在上述代碼的基礎上打印每一次executeUpdate()方法的返回值,看看返回值是多少。
這樣,我們的數(shù)據(jù)準備已經完成,下面應該使用executeQuery()方法來改造查詢房源的方法了,原來的是這樣的:
public House findHouseById(String houseId) { for (House house : mockHouses) { if (houseId.equals(house.getId())) { return house; } } return null; }
首先,我們仍然需要建立數(shù)據(jù)庫連接,創(chuàng)建Statement對象,因為上面使用的都是局部變量。這不就有代碼重復了嗎?沒錯,我們需要消除這種重復,不過,我們暫且把這個任務放一邊,先重點看看如何查詢數(shù)據(jù)庫,如何訪問查詢結果集。
根據(jù)房源ID查詢房源的SQL語句很簡單:
select id,name,detail from house where id='這里是房源ID'
查詢條件房源ID是通過方法的參數(shù)傳入的,我們可以利用Java的字符串拼接功能,拼成上述SQL語句
ResultSet rs=statement.executeQuery("select id,name,detail from house where id='" + houseId + "'");
要特別注意拼接SQL語句時用到的單引號和雙引號(使用PreparedStatement可以避免此現(xiàn)象,還可以提高性能,以后討論),很容易出錯,而且容易讓黑客執(zhí)行SQL注入攻擊,這里就不介紹了。
當然,你必須確保house表存在,而且它有 id、name、detail 三列,這也是非常容易出錯的地方,很明顯,我們的代碼與數(shù)據(jù)庫設計緊耦合了,因此,我們需要思考一下怎么樣才能解耦!
言歸正傳,得到結果集之后,我們就可以訪問結果集中的數(shù)據(jù)了。因為查詢結果可能是多條記錄,或者零條記錄(當然,我們這里house表是以房源ID為主鍵的,而查詢條件就是房源ID,因此查詢結果要么該房源ID的房源不存在,要么就只能有一條房源),所以必須遍歷結果集,ResultSet接口提供的是 next()方法:
while (rs.next()) { return new House(rs.getString("id"), rs.getString("name"), rs.getString("detail")); }
當然,一旦訪問到一條房源記錄,那就可以直接返回了。
使用next()方法指向結果集的下一條記錄后(若無下一條則返回false),就可以用ResultSet接口提供的getXXX()方法來訪問每一列的值了,這里的 XXX 可以是各種數(shù)據(jù)類型,比如Byte、Short、Int、Long、Float、Double、Date、String等等,傳入的參數(shù)既可以是該列在select語句中的位置,也可以是selcet語句中出現(xiàn)的列名。
于是,我們的查詢房源的方法就變?yōu)榱耍?/p>
public House findHouseById(String houseId) { try { String url="jdbc:h2:~/h2db/houserenter"; String user="sa"; String password=""; Connection conn=DriverManager.getConnection(url, user, password); Statement statement=conn.createStatement(); ResultSet rs=statement.executeQuery("select id,name,detail from house where id='" + houseId + "'"); while (rs.next()) { return new House(rs.getString("id"), rs.getString("name"), rs.getString("detail")); } } catch (SQLException e) { System.out.println("HouseService findHouseById SQLException: " + e.getMessage() + "! houseId=" + houseId); } return null; }
需要注意的是,我這里采用了另外一種處理異常的方式,即使用 try-catch 語句捕獲拋出的必須處理的異常,并處理它(僅僅打印日志而已,往往要把異常的上下文信息打印出來,便于查找問題)。
另外一個查詢用戶感興趣房源的方法,也可以做相應的修改,HouseService類的構造方法中原來的添加模擬房源數(shù)據(jù)的代碼也可以刪除了,最后的HouseService變成了這樣:
package houserenter.service; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; import java.util.List; import org.springframework.stereotype.Service; import houserenter.entity.House; @Service public class HouseService { public HouseService() throws ClassNotFoundException, SQLException { Class.forName("org.h2.Driver"); String url="jdbc:h2:~/h2db/houserenter"; String user="sa"; String password=""; Connection conn=DriverManager.getConnection(url, user, password); Statement statement=conn.createStatement(); statement.executeUpdate("drop table if exists house"); statement.executeUpdate("create table house(id varchar(20) primary key, name varchar(100), detail varchar(500))"); statement.executeUpdate("insert into house values('1', '金科嘉苑3-2-1201', '詳細信息') "); statement.executeUpdate("insert into house values('2', '萬科橙9-1-501', '詳細信息') "); } public List<House> findHousesInterested(String userName) { // 這里查找該用戶感興趣的房源,省略,改為用模擬數(shù)據(jù) List<House> houses=new ArrayList<>(); try { String url="jdbc:h2:~/h2db/houserenter"; String user="sa"; String password=""; Connection conn=DriverManager.getConnection(url, user, password); Statement statement=conn.createStatement(); ResultSet rs=statement.executeQuery("select id,name,detail from house"); while (rs.next()) { houses.add(new House(rs.getString("id"), rs.getString("name"), rs.getString("detail"))); } return houses; } catch (SQLException e) { System.out.println("HouseService findHousesInterested SQLException: " + e.getMessage() + "! userName=" + userName); } return houses; } public House findHouseById(String houseId) { try { String url="jdbc:h2:~/h2db/houserenter"; String user="sa"; String password=""; Connection conn=DriverManager.getConnection(url, user, password); Statement statement=conn.createStatement(); ResultSet rs=statement.executeQuery("select id,name,detail from house where id='" + houseId + "'"); while (rs.next()) { return new House(rs.getString("id"), rs.getString("name"), rs.getString("detail")); } } catch (SQLException e) { System.out.println("HouseService findHouseById SQLException: " + e.getMessage() + "! houseId=" + houseId); } return null; } }
我們可以重新發(fā)布應用并啟動Tomcat,然后用瀏覽器訪問租房網應用進行驗證,基本上沒有問題,唯一存在功能上的問題就是,我們編輯的房源沒有存到數(shù)據(jù)庫中,所以我們需要為HouseService類增加一個更新房源的方法,具體實現(xiàn)如下:
public void updateHouseById(House house) { try { String url="jdbc:h2:~/h2db/houserenter"; String user="sa"; String password=""; Connection conn=DriverManager.getConnection(url, user, password); Statement statement=conn.createStatement(); statement.executeUpdate("update house set id='" + house.getId() + "', name='" + house.getName() + "', detail='" + house.getDetail() + "' where id='" + house.getId() + "'"); } catch (SQLException e) { System.out.println("HouseService updateHouseById SQLException: " + e.getMessage() + "! house=" + house.toString()); } }
要非常注意各列的類型是字符串,因此SQL語句中各列的值要加上單引號!
而我們的HouseRenterController控制器類的處理房源編輯表單的Handler方法由原來的:
@PostMapping("/house-form.action") public ModelAndView postHouseForm(String userName, House house) throws UnsupportedEncodingException { // 這里需要驗證用戶是否登錄,省略 //更新指定房源的詳情 House target=houseService.findHouseById(house.getId()); target.setName(house.getName()); target.setDetail(house.getDetail()); //將請求轉發(fā)到查找房源詳情的動作 ModelAndView mv=new ModelAndView(); mv.setViewName("redirect:house-details.action?userName=" + userName + "&houseId=" + house.getId()); return mv; }
修改為:
@PostMapping("/house-form.action") public ModelAndView postHouseForm(String userName, House house) throws UnsupportedEncodingException { // 這里需要驗證用戶是否登錄,省略 //更新指定房源的詳情 houseService.updateHouseById(house); //將請求轉發(fā)到查找房源詳情的動作 ModelAndView mv=new ModelAndView(); mv.setViewName("redirect:house-details.action?userName=" + userName + "&houseId=" + house.getId()); return mv; }
現(xiàn)在,再重新驗證一下,編輯房源也沒有問題了!
但是,我們使用JDBC的代碼還有很多不足:
不管怎樣,我們向數(shù)據(jù)持久化邁出了一步!
pring在2018年9月發(fā)布了Spring-Data-JDBC子項目的1.0.0.RELEASE版本(目前版本為1.0.6-RELEASE),Spring-Data-JDBC設計借鑒了DDD,提供了對DDD的支持,包括:
在前面領域設計:聚合與聚合根一文中,通過列子介紹了聚合與聚合根;而在領域設計:領域事件一文中,通過例子介紹了領域事件。
本文結合Spring-Data-JDBC來重寫這兩個例子,來看一下Spring-Data-JDBC如何對DDD進行支持。
Spring-Data-JDBC項目還較新,文檔并不齊全(Spring-Data-JDBC的文檔還是以Spring-Data-JPA為基礎編寫的,依賴還是Spring-Data-JPA,實際不需要Spring-Data-JPA依賴),所以這里給出搭建過程中的注意點。
新建一個maven項目,pom.xml中配置
<!--這里需要引入spring-boot 2.1.0以上,2.0的boot還沒有spring-data-jdbc--><parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.4.RELEASE</version></parent><dependencies> <!--引入spring-data-jdbc--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jdbc</artifactId> </dependency> </dependencies>
開啟jdbc支持
@SpringBootApplication @EnableAutoConfiguration @EnableJdbcRepositories // 主要是這個注解,來啟動spring-data-jdbc支持@EnableTransactionManagement public class TestConfig { }
領域設計:聚合與聚合根中舉了兩個列子:
我們先看Order與OrderDetail。
Order與OrderDetail組成了一個聚合,其中Order是聚合根,聚合中的操作都是通過聚合根來完成的。
在Spring-Data-JDBC中如何表示這一層關系呢?
@Getter // 1 @Table("order_info") // 2 public class Order { @Id // 3 private Long recId; private String name; private Set<OrderDetail> orderDetailList=new HashSet<>(); // 4 public Order(String name) { // 5 this.name=name; } // 其它字段略 public void addDetail(String prodName) { // 6 orderDetailList.add(new OrderDetail(prodName)); } } @Getter // 1 public class OrderDetail { @Id // 3 private Long recId; private String prodName; // 其它字段略 OrderDetail(String prodName) { // 7 this.prodName=prodName; } }
根據(jù)上面的說明,我們的sql結構如下:
DROP TABLE IF EXISTS `order_info`; CREATE TABLE `order_info` ( `rec_id` BIGINT(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵', `name` varchar(11) NOT NULL COMMENT '訂單名稱', PRIMARY KEY (`rec_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 ; DROP TABLE IF EXISTS order_detail; CREATE TABLE `order_detail` ( `rec_id` BIGINT(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵', `order_info` BIGINT(11) NOT NULL COMMENT '訂單主鍵,由spring-data-jdbc自動維護', `prod_name` varchar(11) NOT NULL COMMENT '產品名稱', PRIMARY KEY (`rec_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 ;
對聚合的操作,Spring-Data-JDBC提供了Repository接口,直接實現(xiàn)即可,提供了類似RubyOnRails那樣的動態(tài)查詢方法,不過需要通過Query注解自行編寫sql,詳見下文。
@Repository public interface OrderRepository extends CrudRepository<Order, Long> { }
這就搞定了,我們編寫一個測試,來測試一下:
@RunWith(SpringRunner.class) @SpringBootTest(classes=TestConfig.class) public class OrderTest { @Autowired private OrderRepository orderRepository; @Test public void testInit() { Order order=new Order("測試訂單"); order.addDetail("產品1"); order.addDetail("產品2"); Order info=orderRepository.save(order); // 1 Optional<Order> orderInfoOptional=orderRepository.findById(info.getRecId()); // 2 assertEquals(2, orderInfoOptional.get().getOrderDetailList().size()); // 3 } }
產品與產品評論的關系如下:
代碼表示就是簡單的通過id進行關聯(lián)。代碼如下:
@Getter public class Product { // 1 @Id private Long recId; private String name; public Product(String name) { this.name=name; } // 其它字段略 } @Getter public class ProductComment { @Id private Long recId; private Long productId; // 2 private String content; // 其它字段略 public ProductComment(Long productId, String content) { this.productId=productId; this.content=content; } }
對應的sql如下:
DROP TABLE IF EXISTS `product`; CREATE TABLE `product` ( `rec_id` BIGINT(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵', `name` varchar(11) NOT NULL COMMENT '產品名稱', PRIMARY KEY (`rec_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 ; DROP TABLE IF EXISTS product_comment; CREATE TABLE `product_comment` ( `rec_id` BIGINT(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵', `product_id` BIGINT(11) NOT NULL COMMENT '產品主鍵,手動賦值', `content` varchar(11) NOT NULL COMMENT '評論內容', PRIMARY KEY (`rec_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 ;
產品和評論都是聚合根,所以都有各自的倉儲類:
@Repository public interface ProductRepository extends CrudRepository<Product, Long> { } @Repository public interface ProductCommentRepository extends CrudRepository<ProductComment, Long> { @Query("select count(1) from product_comment where product_id=:productId") // 1 int countByProductId(@Param("productId") Long productId); // 2 }
熟悉Mybatis的朋友對這段代碼應該很眼熟吧!
測試如下:
@RunWith(SpringRunner.class) @SpringBootTest(classes=TestConfig.class) public class ProductTest { @Autowired private ProductRepository productRepository; @Autowired private ProductCommentRepository productCommentRepository; @Test public void testInit() { Product prod=new Product("產品名稱"); Product info=productRepository.save(prod); ProductComment comment1=new ProductComment(info.getRecId(), "評論1"); // 1 ProductComment comment2=new ProductComment(info.getRecId(), "評論2"); productCommentRepository.save(comment1); int num=productCommentRepository.countByProductId(info.getRecId()); assertEquals(1, num); productCommentRepository.save(comment2); num=productCommentRepository.countByProductId(info.getRecId()); assertEquals(2, num); productRepository.delete(info); // 2 num=productCommentRepository.countByProductId(info.getRecId()); assertEquals(2, num); } }
從上面的兩個例子可以看出:
// 來自spring示例 class Book { ...... private Set<AuthorRef> authors=new HashSet<>(); } @Table("book_author") class AuthorRef { Long authorId; } class Author { ...... String name; }
在領域設計:領域事件一文中使用Spring提供的ApplicationEvent演示了領域事件,這里通過對Order聚合根的擴展,來看看Spring-Data-JDBC對領域事件的支持。
假設上面的Order創(chuàng)建后,需要發(fā)送一個領域事件,該如何處理呢?
Spring-Data-JDBC默認提供了5個事件:
那么對于上面的需求,我們不需要創(chuàng)建什么事件,只需要創(chuàng)建一個監(jiān)聽器,來監(jiān)聽AfterSaveEvent事件就可以了。
@Bean public ApplicationListener<AfterSaveEvent> afterSaveEventListener() { return event -> { Object entity=event.getEntity(); if (entity instanceof Order) { Order order=(Order) entity; System.out.println("訂單[" + order.getName() + "]保存成功"); } }; }
重新執(zhí)行上面的OrderTest的測試方法,會得到如下輸出:
訂單[測試訂單]保存成功
如果我們需要自定義事件,該如何處理呢?Spring-Data-JDBC提供了DomainEvents和AfterDomainEventPublication注解:
@Getter public class OrderCreateEvent extends ApplicationEvent { // 1 private String name; public OrderCreateEvent(Object source, String name) { super(source); this.name=name; } } @Getter @Table("order_info") public class Order { ...... @DomainEvents public ApplicationEvent domainEvent() { // 2 return new OrderCreateEvent(this, this.name); } @AfterDomainEventPublication public void postPublish() { // 3 System.out.println("Event published"); } } public class TestConfig { ...... @Bean public ApplicationListener<OrderCreateEvent> orderCreateEventListener() { // 4 return event -> { System.out.println("訂單[" + event.getName() + "]保存成功"); }; } }
再次執(zhí)行上面的OrderTest的測試方法,會得到如下輸出:
訂單[測試訂單]保存成功 // 這是AfterSaveEvent事件觸發(fā)的 訂單[測試訂單]保存成功 // 這是自定義事件觸發(fā)的 Event published
Spring-Data-JDBC在原來Spring事件的基礎上進行了增強:
Spring-Data-JDBC的設計借鑒了DDD。本文演示了Spring-Data-JDBC如何對DDD進行支持:
Spring-Data-JDBC還提供了如下功能:
有興趣可自行參考文檔。
*請認真填寫需求信息,我們會在24小時內與您取得聯(lián)系。