JavaBean 是 Java 元件模型的基石,廣泛應用於 JSP、Spring、反射框架、序列化、屬性編輯器等場景。本文深度解析 JavaBean 三大核心契約、設計原則、命名慣例、進階特性與常見誤區,助您掌握編寫標準、可重用、框架友善的 JavaBean。
| 契約 | 要求 | 目的 | 違反後果 |
|---|---|---|---|
| 1. 實作 Serializable | implements java.io.Serializable | 支援持久化、網路傳輸、Session 複製、RMI 傳遞 | 框架序列化失敗、叢集環境 Session 複製異常 |
| 2. 提供無參建構子 | public BeanName() {}(可隱式) | 容器/框架透過反射實例化,如 Class.newInstance()、BeanUtils.instantiate() | 實例化拋出 InstantiationException,JSP <jsp:useBean> 失效 |
| 3. 公開屬性存取器 | getXxx()/setXxx() 命名慣例 | 內省機制識別屬性,支援屬性編輯、綁定、EL 表達式存取 | 屬性不可被工具/框架發現,自動注入/表單綁定失效 |
package com.example.model;
import java.io.Serializable;
import java.util.Date;
public class UserProfile implements Serializable {
// 屬性:私有封裝
private String username;
private int age;
private Date lastLoginTime;
private boolean active;
// 契約 2:無參建構子(明確宣告,防止後續加入有參建構子導致消失)
public UserProfile() {}
// 契約 3:標準 Getter/Setter
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
// boolean 型別慣用 isXxx(),但 getXxx() 也被接受
public boolean isActive() { return active; }
public void setActive(boolean active) { this.active = active; }
public Date getLastLoginTime() { return lastLoginTime; }
public void setLastLoginTime(Date lastLoginTime) {
this.lastLoginTime = lastLoginTime;
}
// 非屬性方法:業務邏輯、輔助方法,不受內省影響
public String getDisplayName() {
return username + (active ? " [在線]" : " [離線]");
}
}
get/set/is 前綴,首字母小寫getUsername() → 屬性 usernameisActive() → 屬性 active(boolean 專用)setLastLoginTime() → 屬性 lastLoginTime| 情況 | Getter | Setter | 屬性名 |
|---|---|---|---|
| boolean 基本型別 | isXxx() / getXxx() | setXxx() | xxx |
| Boolean 包裝型別 | getXxx() | setXxx() | xxx |
| 索引屬性 | getXxx(int index) | setXxx(int index, Type value) | xxx |
| 陣列/集合屬性 | getXxx() 返回陣列/集合 | setXxx(Type[]) | xxx |
BeanInfo info = Introspector.getBeanInfo(UserProfile.class);
for (PropertyDescriptor pd : info.getPropertyDescriptors()) {
System.out.println("Property: " + pd.getName()
+ ", Read: " + pd.getReadMethod()
+ ", Write: " + pd.getWriteMethod());
}
public class ScoreBoard implements Serializable {
private int[] scores = new int[10];
// 索引屬性 getter/setter
public int getScore(int index) { return scores[index]; }
public void setScore(int index, int value) { scores[index] = value; }
// 陣列屬性(整體存取)
public int[] getScores() { return scores; }
public void setScores(int[] scores) { this.scores = scores; }
}
import java.beans.PropertyChangeSupport;
import java.beans.PropertyChangeListener;
public class ObservableBean implements Serializable {
private final PropertyChangeSupport pcs = new PropertyChangeSupport(this);
private String value;
public String getValue() { return value; }
public void setValue(String value) {
String old = this.value;
this.value = value;
pcs.firePropertyChange("value", old, value); // 發送變更事件
}
public void addPropertyChangeListener(PropertyChangeListener l) {
pcs.addPropertyChangeListener(l);
}
public void removePropertyChangeListener(PropertyChangeListener l) {
pcs.removePropertyChangeListener(l);
}
}
import java.beans.VetoableChangeSupport;
import java.beans.PropertyVetoException;
public class ValidatedBean implements Serializable {
private final VetoableChangeSupport vcs = new VetoableChangeSupport(this);
private int age;
public int getAge() { return age; }
public void setAge(int age) throws PropertyVetoException {
vcs.fireVetoableChange("age", this.age, age); // 可能拋出 PropertyVetoException
this.age = age;
}
// VetoableChangeListener 管理方法...
}
建立 UserProfileBeanInfo 類(同包、名稱 = Bean名 + BeanInfo),可自訂:
| 框架/場景 | JavaBean 契約要求 | 備註 |
|---|---|---|
JSP <jsp:useBean> | 無參建構子、Getter/Setter | 範圍:page/request/session/application |
| Spring Bean | 無參建構子(建議)、Setter 注入 | Spring Bean ≠ JavaBean,但相容 |
| Struts ActionForm | 繼承 ActionForm、屬性對應表單欄位 | Struts 2 直接用 POJO Action |
| Hibernate/JPA Entity | 無參建構子(必要)、Getter/Setter、Serializable(建議) | 加上 @Entity、@Id 等註解 |
| JSON 序列化 (Jackson/Gson) | Getter/Setter、無參建構子 | 支援欄位直接存取(無需 Getter) |
| JavaServer Faces (JSF) | 完整 JavaBean 契約 | Managed Bean 基礎 |
真相: 只有需被容器/框架管理、序列化、反射存取的類才需遵循。Service、DAO、Controller、DTO、VO、Entity 各有其設計考量,不必強求。
真相: 領域模型應暴露行為而非狀態。account.setBalance(account.getBalance() + amount) 違反封裝,應為 account.deposit(amount)。JavaBean 契約適用於資料傳輸層,領域層優先豐富模型。
最佳實踐: 明確宣告 private static final long serialVersionUID = 1L;,避免跨 JVM/版本升級反序列化失敗。
// ❌ 危險:呼叫者可直接修改內部 Date 狀態
public Date getLastLoginTime() { return lastLoginTime; }
// ✅ 防禦性複製
public Date getLastLoginTime() {
return lastLoginTime != null ? (Date) lastLoginTime.clone() : null;
}
// ✅ 或回傳不可變替代品 (Java 8+)
public Instant getLastLoginTime() { return lastLoginTime != null ? lastLoginTime.toInstant() : null; }
Serializable 並宣告 serialVersionUIDpublic 無參建構子private,提供標準 Getter/SetterisXxx()@Data (Lombok) 或 record (Java 14+) 簡化樣板碼| 方案 | 適用場景 | 優勢 | 注意 |
|---|---|---|---|
Lombok @Data/@Value | DTO/VO/Entity | 零樣板碼、編譯期生成 | IDE 需安裝插件、調試見不到方法 |
Java 14+ record | 不可變資料載體 | 語言級支援、自動生成 equals/hashCode/toString、不可變 | 無無參建構子、不可繼承、非 JavaBean 完全相容 |
| Immutable POJO (Builder 模式) | 執行緒安全資料傳輸 | 不可變、鏈式構建、領域友善 | 樣板碼較多 |
JavaBean 看似簡單的三條契約,實則是Java 元件互操作的通用協議。掌握其核心規範、內省機制、進階特性與框架整合要點,能讓您編寫出既符合標準、又兼具封裝性與框架友善度的元件。記住:JavaBean 服務於資料傳輸與框架整合邊界,領域核心應堅持豐富模型,兩者分工協作,共築整潔架構。