首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 綜合教學

SSH (Struts2+Spring+Hibernate) 整合項目實戰總結:架構分層、前端模板、緩存策略、JSON 序列化與 Session 陷阱

     編輯:Java 綜合教學

專案概覽

典型電商前後台系統:前台商品瀏覽/購物車/訂單/用戶中心,後台 EasyUI 管理後台。技術棧:Struts 2.5 + Spring 4 + Hibernate 5 + MySQL + EasyUI + json-lib。

經驗總結:架構與工程化

1. 前端模板化:公共片段提取與佈局復用

  • 頂部導航 (top.jsp)、底部版權 (footer.jsp) 獨立文件,<%@ include file="common/top.jsp" %> 靜態包含。
  • 通用頁面結構放入 common/ 目錄,業務頁面僅填充內容區塊。
  • 導航麵包屑由服務端工具類動態生成,避免 JS 字符串拼接:
public class NavTool {
    public static String genNavCode(String subName) {
        return "您現在的位置:<a href='index.jsp'>首頁</a> > " + subName;
    }
    public static String genNavCode(List<String> hrefList) {
        StringBuilder sb = new StringBuilder("您現在的位置:<a href='index.jsp'>首頁</a> > ");
        for (String href : hrefList) sb.append(href).append(" > ");
        return sb.substring(0, sb.lastIndexOf(" > "));
    }
}

優點:邏輯集中、復用率高、SEO 友好、無前端框架依賴。

2. 系統啟動緩存:不變數據預熱到 Application

  • 商品分類樹、導航菜單、字典表等極少變動數據。
  • ServletContextListener 中 contextInitialized() 查詢 DB,application.setAttribute("categories", list)。
  • 全站直接 application.getAttribute("categories") 讀取,避免重複查詢。

3. SSH 整合體感:從 JDBC 模板到領域驅動

核心感悟:框架讓開發者聚焦業務邏輯,但也拉遠了與底層的距離——理解原理仍是排查疑難雜症的前提。

代碼層面的三大進階技巧

技巧一:多條件查詢合一 — 單方法 + 動態 HQL

舊寫法:getByName()、getByType()、getAll()… 代碼冗餘。
新寫法:查詢參數封裝入實體對象,構建器模式拼 HQL:

public List<Product> getProductList(Product query, PageBean page) {
    StringBuilder hql = new StringBuilder("FROM Product WHERE 1=1 ");
    List<Object> params = new ArrayList<>();

    if (query.getSpecialPrice() == 1) hql.append("AND specialPrice=1 ORDER BY specialPriceTime DESC ");
    if (query.getHot() == 1) hql.append("AND hot=1 ORDER BY hotTime DESC ");
    if (query.getBigType() != null) { hql.append("AND bigType.id=? "); params.add(query.getBigType().getId()); }
    if (query.getSmallType() != null) { hql.append("AND smallType.id=? "); params.add(query.getSmallType().getId()); }
    if (StringUtils.isNotBlank(query.getName())) { hql.append("AND name LIKE ? "); params.add("%" + query.getName() + "%"); }

    return baseDao.find(hql.toString(), params, page);
}

優勢:單入口、邏輯集中、易維護、參數化防注入。

技巧二:json-lib 日期/關聯對象序列化 — 註冊 ValueProcessor

問題:Date 序列化為完整對象圖 {"date":1,"day":3...};關聯對象 (如 BigType) 遞歸序列化導致循環引用/數據過大。

解決:實現 JsonValueProcessor 並註冊:

// 日期處理器
public class DateJsonValueProcessor implements JsonValueProcessor {
    private final String format;
    public DateJsonValueProcessor(String format) { this.format = format; }
    @Override public Object processArrayValue(Object v, JsonConfig cfg) { return process(v); }
    @Override public Object processObjectValue(String key, Object v, JsonConfig cfg) { return process(v); }
    private Object process(Object v) {
        if (v == null) return "";
        if (v instanceof Date) return new SimpleDateFormat(format).format((Date) v);
        return v.toString();
    }
}

// 關聯對象處理器 (僅保留 id/name)
public class ObjectJsonValueProcessor implements JsonValueProcessor {
    private final String[] fields; private final Class<?> clazz;
    public ObjectJsonValueProcessor(String[] fields, Class<?> clazz) { this.fields = fields; this.clazz = clazz; }
    @Override public Object processArrayValue(Object v, JsonConfig cfg) { return process(v); }
    @Override public Object processObjectValue(String key, Object v, JsonConfig cfg) { return process(v); }
    private Object process(Object v) {
        if (v == null) return JSONObject.NULL;
        JSONObject jo = new JSONObject();
        for (String f : fields) {
            try { jo.put(f, new PropertyDescriptor(f, clazz).getReadMethod().invoke(v)); }
            catch (Exception e) { jo.put(f, ""); }
        }
        return jo;
    }
}

// Action 中註冊
JsonConfig cfg = new JsonConfig();
cfg.setExcludes(new String[]{"orderProductList"}); // 排除循環引用集合
cfg.registerJsonValueProcessor(Date.class, new DateJsonValueProcessor("yyyy-MM-dd"));
cfg.registerJsonValueProcessor(BigType.class, new ObjectJsonValueProcessor(new String[]{"id","name"}, BigType.class));
cfg.registerJsonValueProcessor(SmallType.class, new ObjectJsonValueProcessor(new String[]{"id","name"}, SmallType.class));
JSONArray array = JSONArray.fromObject(list, cfg);

效果:"hotTime":"2024-01-15"、"bigType":{"id":1,"name":"手機"},乾淨可控。

技巧三:BaseDao 泛型封裝 — 消除重複 CRUD

public class BaseDao<T> {
    private Class<T> entityClass;
    public BaseDao() { this.entityClass = (Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0]; }

    public T get(Serializable id) { return (T) getSession().get(entityClass, id); }
    public void save(T entity) { getSession().save(entity); }
    public void update(T entity) { getSession().update(entity); }
    public void delete(T entity) { getSession().delete(entity); }
    public List<T> find(String hql, List<Object> params) { /* ... */ }
    public List<T> find(String hql, List<Object> params, PageBean page) { /* 分頁 */ }
}

子類零代碼繼承:public class ProductDao extends BaseDao<Product> {}。

遺留疑難:Session 屬性莫名消失 (非 Session 失效)

現象:存入 Session 的 對象類型屬性 (User、購物車 Map) 幾秒後讀取為 null,但 字符串/基本類型屬性 正常;Session 本身未銷毀 (session.getId() 不變)。

排查方向

  1. 序列化問題:對象未實現 Serializable,集群/持久化 Session 時反序列化失敗導致屬性丟失。
  2. 類加載器隔離:熱部署/重新部署導致舊 Session 中對象類與新類加載器不一致,ClassCastException 被吞沒返回 null。
  3. Session 屬性被攔截器/濾波器清理:檢查 session.removeAttribute() 調用點。
  4. Tomcat 配置:context.xml <Manager pathname="" /> 禁用持久化,或 sessionAttributeValueClassNameFilter 限制可序列化類。
  5. 並發修改:多線程並發讀寫同一 Session 屬性 (如購物車),未同步導致數據不一致。

建議驗證步驟

  1. 確保所有存入 Session 的對象 實現 Serializable 並定義 serialVersionUID。
  2. 啟用 Tomcat Session 存活監控:manager/html/sessions 觀察屬性變化。
  3. 臨時在濾波器記錄 session.getAttributeNames() 追蹤消失時刻。
  4. 若為集群環境,檢查 Redis/Database Session 存儲的序列化配置 (如 spring.session.serializer)。

此問題極具迷惑性,歡迎有確切根因的同學分享解決方案。

總結

  • SSH 整合核心在 Spring 管理 Bean、Hibernate 管 ORM、Struts2 接請求,配置文件模式固定 (applicationContext.xml, struts.xml, hibernate.cfg.xml)。
  • 工程化關鍵:模板復用、啟動緩存、動態 HQL、JSON 精準序列化、泛型 BaseDao。
  • Session 異常丟失多因序列化/類加載器/集群配置,需系統化排查。
  • 框架是加速器,原理理解是剎車——只會用不懂原理,遇到坑只能干瞪眼。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved
維度JDBC + Servlet 時代SSH 整合後
表結構變更手改 SQL、手改 Bean、手改 DAO改實體註解/映射文件,Hibernate 自動 DDL (hbm2ddl.auto=update)
DAO 代碼量每表一套 CRUD、關聯查詢手寫 SQLBaseDao 泛型封裝通用方法,僅寫特殊 HQL/QL
參數綁定手動 ps.setString(1, val)Struts2 OGNL 自動注入 Action 屬性,Service 直接用對象
事務管理手動 conn.commit/rollbackSpring @Transactional 聲明式,攔截器織入