Struts 1.x 開發中,ActionForm 屬性手工映射極其繁瑣。本文展示如何利用 Java 反射 實現 ResultSet 到任意 ActionForm 的通用自動映射,消除重複 setXxx() 代碼,大幅提升開發效率。
每個查詢方法都需手寫:
while (rs.next()) {
UserForm form = new UserForm();
form.setId(rs.getInt("id"));
form.setName(rs.getString("name"));
form.setEmail(rs.getString("email"));
// ... 幾十行 setXxx()
list.add(form);
}
新增欄位 → 修改 Form → 修改查詢代碼 → 修改 JSP,四處同步。
public class BeanUtil {
/**
* 將 ResultSet 當前行映射到指定類別實例 (支援基本類型、String、Date 等)
* @param rs ResultSet (游標已定位到有效行)
* @param clazz 目標類別 (需有無參建構子、標準 setter)
* @return 填充好屬性的實例
*/
public static T mapRow(ResultSet rs, Class clazz) throws Exception {
T bean = clazz.getDeclaredConstructor().newInstance();
ResultSetMetaData meta = rs.getMetaData();
int cols = meta.getColumnCount();
for (int i = 1; i <= cols; i++) {
String colName = meta.getColumnName(i);
Object value = rs.getObject(i);
if (value == null) continue;
// 欄位名轉 setter 方法名: user_id → setUserId
String setterName = "set" + toCamelCase(colName);
try {
Method setter = clazz.getMethod(setterName, value.getClass());
setter.invoke(bean, value);
} catch (NoSuchMethodException e) {
// 嘗試常見類型兼容 (如 RS 返回 Timestamp, setter 要 Date)
Method setter = findCompatibleSetter(clazz, setterName, value);
if (setter != null) setter.invoke(bean, value);
}
}
return bean;
}
private static String toCamelCase(String underscore) {
StringBuilder sb = new StringBuilder();
boolean upper = false;
for (char c : underscore.toLowerCase().toCharArray()) {
if (c == '_') { upper = true; continue; }
sb.append(upper ? Character.toUpperCase(c) : c);
upper = false;
}
return sb.toString();
}
private static Method findCompatibleSetter(Class> clazz, String name, Object value) {
// 可擴展:Timestamp→Date, BigDecimal→Double 等
return null; // 簡化略
}
}
public ArrayList findAllUsers() {
String sql = "SELECT usr_id, usr_name, usr_email FROM users";
return query(sql, UserForm.class);
}
public ArrayList query(String sql, Class clazz) {
ArrayList list = new ArrayList<>();
try (Connection conn = getConnection();
PreparedStatement ps = conn.prepareStatement(sql);
ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
list.add(BeanUtil.mapRow(rs, clazz));
}
} catch (Exception e) { throw new RuntimeException(e); }
return list;
}
public class UsersActionForm extends ActionForm {
private String usrId;
private String usrName;
private String usrEmail;
// 標準 setter/getter
public void setUsrId(String usrId) { this.usrId = usrId; }
public String getUsrId() { return usrId; }
// ... 其他同理
}
public ActionForward execute(ActionMapping mapping, ActionForm form,
HttpServletRequest req, HttpServletResponse res) {
UserService service = new UserService();
ArrayList list = service.findAllUsers();
req.setAttribute("userList", list);
return mapping.findForward("success");
}
| 優點 | 缺點/風險 |
|---|---|
| 消除 90% 手工映射代碼 | 反射效能略低 (可緩存 Method 對象優化) |
| 新增欄位只需加 setter,零修改 DAO/Action | 欄位名與屬性名需遵循命名規約 (下劃線→駝峰) |
| 統一異常處理、資源關閉 | 複雜類型 (Blob, Clob, 自定義類) 需額外處理 |
ConcurrentMap, Map> 緩存 setter 反射對象,避免重複 getMethod。ConversionService 處理 Timestamp→Date、String→Enum 等跨類型賦值。AS 指定別名匹配屬性名,解決數據庫命名規範不一致問題。本技巧適用於存量 Struts 1.x 專案快速重構,亦可作為理解 ORM 映射原理的教學範例。