1. 程式人生 > >JavaWeb入門_模仿天貓整站Tmall_SSH實踐專案

JavaWeb入門_模仿天貓整站Tmall_SSH實踐專案

Tmall_SSH

技術棧 Struts2 + Hibernate + Spring + Jsp + Tomcat , 是 Java Web 入門非常好的練手專案

效果展示:

模仿天貓前臺
模仿天貓後臺

專案簡介

關聯專案
github - 天貓 JavaEE 專案
github - 天貓 SSH 專案
github - 天貓 SSM 專案

之前使用 JavaEE 整套技術來作為解決方案,實現模仿天貓網站的各種業務場景,現在開始使用框架技術,畢竟工作中還是要用框架。
本專案技術相對老舊,現在很少用 Struts2 了,但如果接手老專案的話還是要懂的,學習過程我們也可以認識到它們當時優秀的設計理念,
當時解決了哪些痛點,後面又是因為什麼被新技術替代,這樣才能加深對 Java Web 整個平臺的理解,不虧。

專案用到的技術如下:
Java:Java SE基礎
前端:HTML,CSS, JavaScript,AJAX, JQuery, Bootstrap
J2EE:Tomcat, Servlet, JSP, Filter
框架: HibernateStrutsSpringSSH整合
資料庫:MySQL

專案結構

表結構

建表sql 已經放在 Github 專案的 /sql 資料夾下

表名 中文含義 介紹
Category 分類表 存放分類資訊,如女裝,平板電視,沙發等
Property 屬性表 存放屬性資訊,如顏色,重量,品牌,廠商,型號等
Product 產品表 存放產品資訊,如LED40EC平板電視機,海爾EC6005熱水器
PropertyValue 屬性值表 存放屬性值資訊,如重量是900g,顏色是粉紅色
ProductImage 產品圖片表 存放產品圖片資訊,如產品頁顯示的5個圖片
Review 評論表 存放評論資訊,如買回來的蠟燭很好用,麼麼噠
User 使用者表 存放使用者資訊,如斬手狗,千手小粉紅
Order 訂單表 存放訂單資訊,包括郵寄地址,電話號碼等資訊
OrderItem 訂單項表 存放訂單項資訊,包括購買產品種類,數量等

表關係

Category-分類 Product-產品
Category-分類 Property-屬性
Property-屬性 PropertyValue-屬性值
Product-產品 PropertyValue-屬性值
Product-產品 ProductImage-產品圖片
Product-產品 Review-評價
User-使用者 Order-訂單
Product-產品 OrderItem-訂單項
User-使用者 OrderItem-訂單項
Order-訂單 OrderItem-訂單項
User-使用者 User-評價

以上直接看可能暫時無法完全理解,結合後面具體到專案的業務流程就明白了。


開發流程

首先使用經典的 SSH 模式進行由淺入深地開發出第一個分類管理模組 ,
然後分析這種方式的弊端,對其進行專案重構,重構這一塊可以學習到不少 Java 裡的中高階處理手法,
使得框架更加緊湊,後續開發更加便利和高效率。

實體類設計

準備 Category 實體類,並用 Hibernate 註解標示其對應的表,欄位等資訊。
舉個例子,對於 分類 / category 的 實體類 和 表結構 設計如下:
//已省略對應的 getter/setter 方法

DAOImpl 類設計

DAO 是 Data Access Object 的縮寫,專門用於進行資料庫訪問的操作。
DAOImpl 繼承了 HibernateTemplate,這是一個 Hibernate 框架提供的模板類,提供了各種各樣的 CRUD方法,滿足各種資料庫操作的需要。

注: 這裡沒有使用DAO介面,而是直接使用了DAOImpl 實現類的方式

重寫 HibernateTemplate 的 setSessionFactory() 方法, 以用於注入 SessionFactory ,

applicateionContext.xml 定義了 name="sf" 的 SessionFactory bean

SessionFactory 是在 spring 的配置檔案裡面定義的 bean ,可以看到其配置了連線資料庫的資料來源等資訊,這樣 dao 操作的時候,就不必獲取對應的資料庫連線進行操作,
Spring 將資料來源 ds 物件注入 SessionFactory ,sf 又被注入到 HibernateTemplate ,dao 繼承 HibernateTemplate 就可以直接操作資料庫了,非常簡便。

對比不使用 Spring+Hibernate 的情況,我們需要利用資料管理類 DBUtil 獲取 Connectoion ,
並在 dao 裡面獲取對應的 Statement 分別實現 CURD 等方法,利用 JDBC 從資料庫取出資料,再構造成 bean 物件返回。


Service 類

設計 CategoryService 介面,用於提供業務方法 list() ,即查詢所有的分類。

public interface CategoryService{
    public List list();
}

CategoryServiceImpl 實現了 CategoryService 介面,提供list()方法的具體實現,同時自動裝配(注入) 了 DAOImpl 的例項 dao ,
在 list() 方法中,通過 dao 獲取所有的分類物件。

@Service
public class CategoryServiceImpl  implements CategoryService {
    @Autowired 
    DAOImpl dao;
    @Override
    public List list() {
        DetachedCriteria dc = DetachedCriteria.forClass(Category.class); // 獲取DetachedCriteria物件
        dc.addOrder(Order.desc("id"));
        return dao.findByCriteria(dc); //這是 HibernateTemplate 提供的方法
    }
}

Action 類

CategoryAction 類作為 MVC 設計模式中的控制層起作用。

  1. 使用 basicstruts ,對應配置檔案 struts.xml 中定義的 basicstruts 保持一致
  2. 在 Result 註解中,定義了返回的頁面為 /admin/listCategory.jsp
  3. 自動裝配(注入)categoryService 物件,用於從資料庫獲取所有分類物件的集合。
  4. 把對 admin_category_list 路徑的訪問對映到 list 方法上
  5. list() 方法通過 categoryService 獲取到所有的分類物件,放在 categories 屬性中。
  6. 同時提供了 getCategories() 方法,用於向listCategory.jsp頁面傳遞資料

CategoryAction 部分程式碼 已省略 categories 的 getter/setter

配置檔案

web.xml

這個web.xml做了3件事情
1.讓所有請求都進入 Struts2 的過濾器 StrutsPrepareAndExecuteFilter
2.對所有請求進行 UTF-8 編碼
3.指定Spring配置檔案 applicationContext.xml 的位置

<web-app>
    <filter>
        <filter-name>struts2</filter-name>
        <filter-class>
            org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter
        </filter-class>
    </filter>
    <filter-mapping>
        <filter-name>struts2</filter-name>
        <dispatcher>FORWARD</dispatcher>
        <dispatcher>REQUEST</dispatcher>       
        <url-pattern>/*</url-pattern>
    </filter-mapping>
     
    <filter>
        <filter-name>encodingFilter</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>encodingFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
 
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:applicationContext.xml</param-value>
    </context-param>
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
</web-app>
struts.xml
<struts>
  <constant name="struts.i18n.encoding" value="UTF-8"></constant>
  <constant name="struts.objectFactory" value="spring"/>
  <package name="basicstruts" extends="struts-default">
 </package>
</struts>
applicationContext.xml

applicationContext 除了配置上述的 SessionFactory ,還要配置事務管理器

<!-- 配置事務管理器(宣告式的事務) --> 
    <bean id="transactionManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager"> 
        <property name="sessionFactory" ref="sf"></property> 
    </bean> 
訪問 jsp 顯示資料

Action 攜帶資料跳轉到 jsp ,作為檢視,擔當的角色是顯示資料,藉助 JSTL 的 c:forEach 標籤遍歷從CategoryAction 的 list() 的傳遞過來的集合。

listCategory.jsp 部分程式碼

localhost/admin_category_list 訪問效果

完整版的 listCategory.jsp 還包含4個公共檔案,分別是 頭部,導航,行業,頁尾。
分類管理還有增加,編輯,修改,刪除,分頁,另外後臺其他管理頁面,前臺頁面。具體的需要瀏覽程式碼,篇幅原因就不展開了。

思路流程圖

思路流程圖

重構(這裡非常精彩)

分類管理的 CURD 功能全部做好之後,程式碼層面的問題開始逐漸浮現出來了,問題主要表現在 Service 層,和 Action 層。

Service層的問題

Service 層程式碼如下:

首先看介面:CategoryService。 其宣告的方法基本上就是 CURD 和分頁。可以預見的是,在後續做產品管理,使用者管理,訂單管理等等,
也會有這麼一個非常近似的 CURD 的介面,換句話說,這裡是有做抽象和程式碼重構的機會和價值的。

然後看實現類:CategoryServiceImpl。 CategoryServiceImpl本身其實就是個架子,真正起作用的是為其注入的DAO物件,
所以這個地方也是可以引入委派模式,使得程式碼呼叫更加順暢。

Service 層的重構

Service層 的重構行為主要包括兩種角度

  1. 對CURD進行抽象
  2. 委派模式的重構

由於可以預見的在後續做產品管理,使用者管理,訂單管理等等,也會有這麼一個非常近似的CURD的介面,
那麼我們就做一個BaseService,裡面就提供這些CRUD和分頁查詢的方法。

接著設計 BaseServiceImpl 類,其 CURD 關鍵方法都是呼叫的被注入的 dao 完成的,這樣就十分適合使用委派模式來重構這塊程式碼。

不使用委派模式訪問資料庫都需要通過dao.XXX()來進行。 而委派重構之後,資料庫相關方法,不再需要通過dao,直接呼叫即可,程式碼看上去更簡潔。

委派類 ServiceDelegateDAO

設計一個新的類,叫做 ServiceDelegateDAO ,在其中注入 dao ,然後讓對 dao 的每一個方法進行委派。
那麼到底什麼是委派呢? 如 ServiceDelegateDAO 類所示:
public void delete(Object entity) throws DataAccessException { dao.delete(entity); }

當呼叫 ServiceDelegateDAO 物件的 delete(Object entity) 的時候,其實就是委派給的 dao 的 delete(Object entity) 方法。
但是從呼叫者的角度來看,呼叫者只知道 ServiceDelegateDAO 這個類的 delete(Object entity) 方法,而意識不到 dao 的存在。
而 dao 繼承了 HibernateTemplate ,一共有一百多個方法,哈哈這麼麻煩肯定有工具可以一鍵生成,
果然利用 idea 立馬就生成了所有委派方法,即快速又不會出錯,突然感覺好開心。

BaseServiceImpl

BaseServiceImpl 的構造器非常騷氣,BaseServiceImpl 的 clazz 物件需要引用實體類物件,這樣 CURD 方法中的 DetachedCriteria dc = DetachedCriteria.forClass(clazz); 才能獲取到對應的查詢物件。
所以這裡利用反射,在構造方法中,藉助異常處理和反射得到 Category.class 或者 Product.class 。 即要做到哪個類繼承了 BaseServiceImpl ,clazz 就對應哪個類對應的實體類物件。

首先要獲取是哪個類繼承了 BaseServiceImpl ,因為例項化子類,父類的構造方法一定會被呼叫,
所以在父類 BaseServiceImpl 裡故意丟擲一個異常,然後手動捕捉住它,
在其對應的 StackTrace 裡的第二個(下標是1) 棧跟蹤元素 StackTraceElement ,即對應子類。
這樣我們就拿到了子類名稱 CategoryServiceImpl 或者 ProductServiceImpl,具體的在程式碼註釋裡了,寫的非常清楚。

BaseServiceImpl 部分程式碼

這樣 CategoryService 就不需要自己宣告方法了,只需要繼承介面 BaseService 即可

CategoryServiceImpl 也不需要自己提供實現了,繼承 BaseServiceImpl 並實現介面 CategoryService 即可

這麼做的好處主要在於:後續新功能開發的過程中,當需要新增加新的Service類的話,比如 PropertyService,無需從頭開發,
只需要繼承 BaseServiceImpl 並實現 PropertyService,那麼其所需要CRUD一套方法都有了。

  1. 開發成本顯著降低
  2. 更加不容易出錯(因為方法都被抽象在父類中了,並且被前面的業務驗證過了,要出錯早就被糾正過了)

畫了個類關係圖

Action的問題

先看程式碼

CategoryAction 部分程式碼

這樣的CategoryAction程式碼完成功能是沒有問題的,但是問題恰恰在於,這樣一個本來是用於充當控制層(Controller)的類,需要集中應付太多的需求:

  1. 返回頁面的定義
  2. 單個物件的getter setter
  3. 集合物件的getter setter
  4. 分頁物件的getter setter
  5. 上傳檔案物件的getter setter
  6. Service層物件的注入
  7. 作為控制層進行的訪問路徑對映

把所有的這些程式碼,都放在一個類裡面,這個類就會顯得繁雜,不易閱讀,不易維護。 所以這個地方也是很有程式碼重構價值的。

Action 層的重構

目前 CategoryAction 存在的問題是一個類需要做太多的事情,顯的繁雜,影響閱讀和維護。 那麼重構思路就是把不同的事情,放在專門的類進行處理,各司其職。

  • 上傳專用 Action4Upload
    這個類就專門用於處理圖片上傳,其他的事情一概不管
  • 分頁專用 Action4Pagination
    專門用於處理分頁,並且繼承上傳專用 Action4Upload
  • 物件和集合 Action4Pojo
    用於提供實體物件以及實體物件集合的setter和getter.
    setter用於接收注入
    getter用於提供資料到JSP(VIEW)上
  • 注入服務專用 Action4Service
    Action4Service提供服務的注入

Action4Service 另外提供了一個方法 t2p() ,專門用於把物件指向對應的持久物件。

Action4Service .t2p()

方法呼叫的最後結果就導致父類 Action4Pojo 中宣告的 pojo 本身是指向瞬時物件的,現在指向了持久物件(從資料庫中取出的物件)。

再定義返回頁面的 Action4Result 繼承 Action4Service ,專門進行返回頁面的定義

這樣
CategoryAction 繼承Action4Result, 於是就間接地繼承了 Action4Service,Action4Pojo,Action4Pagination,Action4Upload,
於是就通過繼承提供了各種相關的功能,CategoryAction 本身只需要專注於扮演控制器(Controller)本身就行了。這些工作對後面其他 Action 同樣適用,大大簡化了後續開發。

此外,後續還有針對 Service 的關係查詢重構,和Service 多條件查詢重構,具體的由於篇幅原因,請移步github 專案的地址

頁面展示

前臺首頁

產品頁

本篇部落格所講不足整個專案的 1/10 ,有興趣的朋友請移步 github 專案的地址

參考

天貓SSH整站學習教程 裡面除了本專案,還有 Java 基礎,前端,Tomcat 及其他中介軟體等教程, 可以註冊一個賬戶,能儲存學習記錄。