雖然 Spring Boot 早已成為主流,但理解「Struts 2 + MyBatis」這類非 Spring 管理的整合架構,有助於掌握 MVC 分層、連線池、快取、事務控制等核心概念。本文將從零搭建一個可運行的 Web 專案,涵蓋 Jar 依賴、web.xml/struts.xml/mybatis-config.xml/mapper 配置、DAO/Service/Action 分層實作、前端 AJAX 互動,並實測 MyBatis 二級快取(Ehcache)效果。
src/ ├─ ms/ │ ├─ action/ │ ├─ dao/ │ │ └─ base/ │ ├─ model/ │ └─ service/ WebContent/ ├─ WEB-INF/ │ ├─ lib/ ← 所有 Jar 放這裡 │ ├─ web.xml │ ├─ struts.xml │ ├─ mybatis-config.xml │ └─ ms/model/userMapper.xml ├─ js/jquery-1.11.1.min.js └─ index.jsp struts2-core, xwork-core, ognl, freemarker 等)mybatis-3.3.0.jar, mybatis-ehcache)ehcache-core, slf4j-api)commons-logging, commons-io, commons-lang)log4j-1.2.17.jar)<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> 關鍵點:StrutsPrepareAndExecuteFilter 會攔截所有 *.action 請求,內部邏輯:
① 排除模式匹配 → ② 尋找 ActionMapping → ③ 執行 Action → ④ 結果處理。
<struts> <constant name="struts.devMode" value="false"/> <package name="default" extends="struts-default,json-default"> <global-results> <result type="json"> <param name="root">json</param> <param name="contentType">text/html</param> </result> </global-results> <action name="addUser" class="ms.action.UserAction" method="addUser"> <result>.</result> </action> <action name="queryAllUser" class="ms.action.UserAction" method="queryAllUser"> <result>.</result> </action> </package> </struts> 重點:繼承 json-default 才能使用 type="json" 結果類型;root 參數指定要序列化的屬性名(Action 中需有 getJson() 或同名屬性)。
<configuration> <settings> <setting name="cacheEnabled" value="true"/> </settings> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/demo"/> <property name="username" value="root"/> <property name="password" value="admin"/> <property name="poolMaximumActiveConnections" value="10"/> <property name="poolPingEnabled" value="true"/> <property name="poolPingQuery" value="select 1"/> </dataSource> </environment> </environments> <mappers> <mapper resource="ms/model/userMapper.xml"/> </mappers> </configuration> 連線池關鍵參數:POOLED 類型使用 MyBatis 內建池,poolMaximumActiveConnections 限制最大活躍連線,poolPingQuery 定期檢測連線存活。
<mapper namespace="UserMapper"> <cache type="org.mybatis.caches.ehcache.LoggingEhcache"> <property name="timeToIdleSeconds" value="3600"/> <property name="timeToLiveSeconds" value="3600"/> <property name="maxEntriesLocalHeap" value="1000"/> <property name="maxEntriesLocalDisk" value="10000000"/> <property name="memoryStoreEvictionPolicy" value="LRU"/> </cache> <insert id="saveUser" parameterType="ms.model.User"> insert into USER(account, name, address) values (#{account}, #{name}, #{address}) </insert> <select id="queryAllUser" resultType="ms.model.User"> select u.id, u.account, u.name, u.address from USER u </select> </mapper> 注意:實體類 User 必須實作 Serializable,否則 Ehcache 序列化失敗。
public class MySessionFactory { private static SqlSessionFactory factory; private MySessionFactory() {} public static synchronized SqlSessionFactory getSqlSessionFactory() { if (factory == null) { try { factory = new SqlSessionFactoryBuilder() .build(Resources.getResourceAsReader("mybatis-config.xml")); } catch (IOException e) { throw new RuntimeException(e); } } return factory; } } public class UserDao { public void add(User user) { SqlSession session = MySessionFactory.getSqlSessionFactory().openSession(); try { session.insert("UserMapper.saveUser", user); session.commit(); } catch (Exception e) { session.rollback(); throw e; } finally { session.close(); } } public List<User> queryAllUser() { SqlSession session = MySessionFactory.getSqlSessionFactory().openSession(); try { // 開啟查詢快取 session.getConfiguration().setCacheEnabled(true); List<User> list = session.selectList("UserMapper.queryAllUser"); session.commit(); return list; } finally { session.close(); } } } public class UserAction { private UserService userService = new UserService(); public void addUser() { PrintWriter out = null; try { HttpServletRequest req = ServletActionContext.getRequest(); HttpServletResponse resp = ServletActionContext.getResponse(); resp.setContentType("text/html;charset=UTF-8"); User user = new User(); user.setAccount(req.getParameter("account")); user.setName(req.getParameter("name")); user.setAddress(req.getParameter("address")); userService.add(user); out = resp.getWriter(); out.write(new Gson().toJson("success")); } catch (Exception e) { e.printStackTrace(); if (out != null) out.write(new Gson().toJson("fail")); } finally { if (out != null) { out.flush(); out.close(); } } } public void queryAllUser() { PrintWriter out = null; try { HttpServletResponse resp = ServletActionContext.getResponse(); resp.setContentType("text/html;charset=UTF-8"); List<User> list = userService.queryAllUser(); out = resp.getWriter(); out.write(new Gson().toJson(list)); } catch (Exception e) { e.printStackTrace(); if (out != null) out.write(new Gson().toJson("fail")); } finally { if (out != null) { out.flush(); out.close(); } } } } <script src="js/jquery-1.11.1.min.js"></script> <script> $(function() { // 載入用戶列表 $.post('queryAllUser.action', function(data) { $.each(data, function(i, user) { $('#userList').append("<li>id=" + user.id + ", " + user.account + "</li>"); }); }, 'json'); // 新增用戶 $('#addUser').click(function() { $.post('addUser.action', { account: $('#account').val(), name: $('#name').val(), address: $('#address').val() }, function(data) { if (data === 'success') { $('#userList').append("<li>" + $('#account').val() + "</li>"); alert('新增成功'); } }, 'json'); }); }); </script> select u.id, u.account... from USER u(快取未命中,查資料庫)。Cache Hit / Cache Miss。| 問題 | 原因 | 解決 |
|---|---|---|
| 快取不生效 | User 未實作 Serializable | 實作 java.io.Serializable |
| 快取不生效 | mybatis-config.xml 未開啟 cacheEnabled | 設定 <setting name="cacheEnabled" value="true"/> |
| 快取不生效 | mapper 未配置 <cache> | 在 userMapper.xml 加入 Ehcache 配置 |
| 連線池耗盡 | poolMaximumActiveConnections 過小 | 依併發量調大,並確保 finally 關閉 session |
| 中文亂碼 | 資料庫/連線/頁面編碼不一致 | 統一 UTF-8,URL 加 useUnicode=true&characterEncoding=UTF-8 |
本教學完整呈現了「Struts 2 + MyBatis」無 Spring 整合的全棧實作,從環境搭建、配置解析、分層代碼到快取驗證。掌握此架構有助於理解:
① MVC 職責分離 ② MyBatis 靈活 SQL 映射 ③ 連線池資源管理 ④ 二級快取透明加速 ⑤ 手動事務邊界控制。
若專案規模增長,建議遷移至 Spring Boot + MyBatis-Plus,享受自動配置、聲明式事務、分頁插件等現代化紅利。