雖然 Spring Boot 早已統一江湖,但「Struts 2 + Spring + Hibernate」這套經典整合架構完整展示了:MVC 分層、IOC 容器管理、AOP 事務/切面、Hibernate ORM 映射、二級快取、連線池、攔截器鏈等企業級開發核心機制。透過手動搭建並驗證每個功能點,能建立扎實的底層認知,為後續排查 Spring Boot 自動配置問題、設計微服務架構奠定堅實基礎。本文將從零搭建並驗證六大核心目標。
CREATE DATABASE demo DEFAULT CHARSET utf8; USE demo; CREATE TABLE `user` ( `id` BIGINT(20) NOT NULL AUTO_INCREMENT, `account` VARCHAR(200) NOT NULL, `name` VARCHAR(200) NOT NULL, `address` VARCHAR(1000) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 採用傳統 WEB-INF/lib 手動放 Jar 方式 (亦可改用 Maven),核心包包含:
<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> <url-pattern>*.action</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> 關鍵:ContextLoaderListener 啟動 Spring 根容器 (載入 Service/Dao/事務/資料源);StrutsPrepareAndExecuteFilter 啟動 Struts 核心控制器 (載入 Action)。
<beans ...> <!-- 1. 掃描註解 Bean (排除 Controller 留給 Struts) --> <context:component-scan base-package="ssh" /> <!-- 2. C3P0 連線池 --> <bean name="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close"> <property name="driverClass" value="com.mysql.jdbc.Driver"/> <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/demo"/> <property name="user" value="root"/> <property name="password" value="root"/> <property name="initialPoolSize" value="30"/> <property name="minPoolSize" value="30"/> <property name="maxPoolSize" value="80"/> <property name="acquireIncrement" value="1"/> <property name="maxIdleTime" value="60"/> <property name="acquireRetryAttempts" value="60"/> <property name="acquireRetryDelay" value="1000"/> <property name="breakAfterAcquireFailure" value="false"/> </bean> <!-- 3. SessionFactory (Hibernate + 二級快取) --> <bean id="sessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="hibernateProperties"> <props> <prop key="hibernate.dialect">org.hibernate.dialect.MySQLDialect</prop> <prop key="hibernate.show_sql">true</prop> <prop key="hibernate.hbm2ddl.auto">update</prop> <prop key="hibernate.cache.use_second_level_cache">true</prop> <prop key="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory</prop> <prop key="hibernate.cache.use_query_cache">true</prop> <prop key="hibernate.cache.provider_configuration_file_resource_path">ehcache.xml</prop> </props> </property> <property name="mappingLocations"> <value>classpath:ssh/model/User.hbm.xml</value> </property> </bean> <!-- 4. 事務管理器 --> <bean id="transactionManager" class="org.springframework.orm.hibernate4.HibernateTransactionManager"> <property name="sessionFactory" ref="sessionFactory"/> </bean> <!-- 5. 聲明式事務 (AOP) --> <tx:advice id="txAdvice" transaction-manager="transactionManager"> <tx:attributes> <tx:method name="add*" propagation="REQUIRED" rollback-for="java.lang.Exception"/> <tx:method name="delete*" propagation="REQUIRED" rollback-for="java.lang.Exception"/> <tx:method name="update*" propagation="REQUIRED" rollback-for="java.lang.Exception"/> <tx:method name="save*" propagation="REQUIRED" rollback-for="java.lang.Exception"/> </tx:attributes> </tx:advice> <aop:config> <aop:pointcut id="serviceMethods" expression="execution(* ssh.service..*.*(..))"/> <aop:advisor pointcut-ref="serviceMethods" advice-ref="txAdvice"/> </aop:config> <!-- 6. 自定義 AOP 切面測試 --> <bean id="aopTest" class="ssh.aop.AopTest"/> <bean id="myAop" class="ssh.aop.MyAop"/> <aop:config proxy-target-class="true"> <aop:aspect ref="myAop"> <aop:pointcut id="testMethods" expression="execution(* ssh.aop.AopTest.test*(..))"/> <aop:before pointcut-ref="testMethods" method="before"/> <aop:after pointcut-ref="testMethods" method="after"/> </aop:aspect> </aop:config> </beans> | 區塊 | 關鍵點 |
|---|---|
| C3P0 | initialPoolSize=minPoolSize 避免啟動擴容抖動;maxPoolSize 需 < MySQL max_connections;acquireRetryAttempts/Delay 緩衝 DB 重啟窗口 |
| 二級快取 | use_second_level_cache=true + EhCacheRegionFactory + 實體類實作 Serializable + .hbm.xml 配置 <cache usage="read-write"/> |
| 事務傳播 | REQUIRED 最常用:有事務加入,無則新建;rollback-for="Exception" 確保檢查異常也回滾 |
| AOP 切面 | proxy-target-class="true" 強制 CGLIB 代理 (不依賴介面);before/after 對應前置/後置通知 |
<struts> <constant name="struts.objectFactory" value="spring"/> <constant name="struts.devMode" value="false"/> <package name="default" extends="struts-default,json-default"> <global-results> <result type="json"> <param name="root">json</param> </result> </global-results> <action name="addUser" class="userAction" method="addUser"><result>.</result></action> <action name="queryAllUser" class="userAction" method="queryAllUser"><result>.</result></action> </package> </struts> 核心:struts.objectFactory=spring 讓 Struts 從 Spring 容器獲取 Action Bean (id="userAction"),實現 IOC 管理。
<hibernate-mapping package="ssh.model"> <class name="User" table="user"> <cache usage="read-write"/> <id name="id" column="id"><generator class="increment"/></id> <property name="account" column="account"/> <property name="name" column="name"/> <property name="address" column="address"/> </class> </hibernate-mapping> @Repository public class BaseDao extends HibernateDaoSupport { @Resource public void setMySessionFactory(SessionFactory sf) { super.setSessionFactory(sf); } } @Repository public class UserDao extends BaseDao { public void add(User user) { getHibernateTemplate().save(user); } @SuppressWarnings("unchecked") public List<User> queryAllUser() { HibernateTemplate ht = getHibernateTemplate(); ht.setCacheQueries(true); // 開啟查詢快取 List<User> list = (List<User>) ht.find("from User"); ht.setCacheQueries(false); return list; } } @Service public class UserService { @Resource private UserDao userDao; public void add(User user) { userDao.add(user); } public List<User> queryAllUser() { return userDao.queryAllUser(); } } @Controller public class UserAction { @Resource private UserService userService; @Resource private AopTest aopTest; // 驗證自定義 AOP public void addUser() { /* 參數接收、userService.add、Gson 回應 success/fail */ } public void queryAllUser() { aopTest.test1(); aopTest.test2(); // 觸發自定義切面 /* userService.queryAllUser、Gson 回應 List */ } } public class AopTest { public void test1() { System.out.println("AopTest test1 running~"); } public void test2() { System.out.println("AopTest test2 running~"); } } public class MyAop { public void before() { System.out.println("before~"); } public void after() { System.out.println("after~"); } } 瀏覽器訪問 index.jsp,點擊「添加」→ 後台印出 Hibernate SQL insert into user...,列表即時顯示新增資料;點擊「查詢」→ 列表顯示所有用戶。
配置 maxPoolSize=10,寫測試程式循環獲取 11 個連線 → 第 11 個阻塞等待。調大至 80 → 正常獲取。生產環境建議:maxPoolSize = (CPU 核心數 × 2) + 有效磁碟數,並監控 show processlist 確認實際佔用。
首次查詢 → Console 印出 select ... from user (Cache Miss);重新整理頁面再次查詢 → 無 SQL 印出 (Cache Hit),響應時間從 ~50ms 降至 ~5ms。Ehcache 統計 hitCount 遞增。
在 Service 某方法中連續 add(user1); throw new RuntimeException(); add(user2); → 資料庫僅插入 0 筆 (全回滾)。註解 @Transactional 或移除 AOP 配置 → 插入 1 筆 (事務失效)。
Action 加 @Controller,struts.xml class="userAction" (Bean id)。並發請求 100 次 → 僅構造 1 次 (單例)。若需多例 → Spring 配置 scope="prototype"。注意:Struts 2 預設 Action 多例,交給 Spring 後變單例,需評估執行緒安全 (Action 欄位無狀態則安全)。
查詢用戶時呼叫 aopTest.test1()/test2() → Console 輸出:before~AopTest test1 running~after~before~AopTest test2 running~after~
證明前置/後置通知正常織入。
| 現象 | 根因 | 解決 |
|---|---|---|
| Too many connections | C3P0 maxPoolSize > MySQL max_connections | 調小 maxPoolSize 或調大 MySQL max_connections (my.cnf) |
| 二級快取不生效 | User 未實作 Serializable / 映射檔缺 <cache> | 實作 Serializable + hbm.xml 加 <cache usage="read-write"/> |
| 事務不回滾 | 拋出檢查異常 / 未掃描到 tx:advice | rollback-for="java.lang.Exception" + 確認 aop:config 掃描路徑 |
| Action 非 Spring 管理 | struts.objectFactory 未設 spring | struts.xml 加 <constant name="struts.objectFactory" value="spring"/> |
| ClassNotFound: EhCacheRegionFactory | 缺 hibernate-ehcache jar | 補齊 hibernate-ehcache 版本與 hibernate-core 一致 |
本教學完整復現了傳統 SSH 架構的所有核心機制。掌握這些原理後,遷移至 Spring Boot 將輕鬆許多:
① application.yml 取代 xml 配置 ② @Transactional 取代 AOP xml ③ @Entity + JPA/Hibernate 取代 hbm.xml ④ @RestController 取代 Struts Action ⑤ spring-boot-starter-cache + @Cacheable 取代 Ehcache xml。
但無論框架如何演進,「連線池調優、快取策略、事務邊界、AOP 織入、Bean 生命週期」這些核心決策點永遠存在。願本文助你建立穩固的架構直覺。