「Java 中實例化類,是否還停留在 new ClassName()?」隨著項目複雜度提升,對象創建的高內聚、低耦合、可擴展性成為架構設計的核心課題。本文通過三個經典開源項目實戰案例,展示從簡單工廠、反射動態加載、線程上下文類加載器到 SPI 機制的實例化模式演進。
場景:根據條碼格式動態創建對應的 Writer 實現。
public BitMatrix encode(String contents, BarcodeFormat format,
int width, int height, Map<EncodeHintType, ?> hints) {
Object writer;
switch (format.ordinal()) {
case 1: writer = new AztecWriter(); break;
case 2: writer = new CodaBarWriter(); break;
case 3: writer = new Code39Writer(); break;
case 5: writer = new Code128Writer(); break;
case 6: writer = new DataMatrixWriter(); break;
case 7: writer = new EAN8Writer(); break;
case 8: writer = new EAN13Writer(); break;
case 9: writer = new ITFWriter(); break;
case 11: writer = new PDF417Writer(); break;
case 12: writer = new QRCodeWriter(); break;
case 15: writer = new UPCAWriter(); break;
case 16: writer = new UPCEWriter(); break;
default: throw new IllegalArgumentException("No encoder for " + format);
}
return ((Writer) writer).encode(contents, format, width, height, hints);
}
枚舉定義:
public enum BarcodeFormat {
AZTEC, CODABAR, CODE_39, CODE_93, CODE_128,
DATA_MATRIX, EAN_8, EAN_13, ITF, MAXICODE,
PDF_417, QR_CODE, RSS_14, RSS_EXPANDED, UPC_A, UPC_E, UPC_EAN_EXTENSION
}
設計亮點:
format.ordinal() 替代魔術數字Writer 接口,雙 encode() 方法MultiFormatWriter,調用者無感知具體實現類場景:根據配置文件動態加載用戶自定義的 Configuration 實現類。
private static Configuration getDefaultConfiguration() {
if (defaultImpl == null) {
defaultImpl = new DefaultConfiguration();
try {
String className = getString("webwork.configuration"); // 從配置讀取類名
if (!className.equals(defaultImpl.getClass().getName())) {
try {
// 關鍵:使用線程上下文類加載器
defaultImpl = (Configuration) ObjectFactory.getObjectFactory()
.buildBean(Thread.currentThread().getContextClassLoader()
.loadClass(className));
} catch (Exception e) {
LOG.error("Could not instantiate configuration", e);
}
}
} catch (IllegalArgumentException ignored) {}
}
return defaultImpl;
}
SystemClassLoader (AppClassLoader) ← 當前應用 classpath,優先級最高
↑ 父委派
ExtensionClassLoader ($JAVA_HOME/jre/lib/ext)
↑ 父委派
BootstrapClassLoader (rt.jar, 核心庫)
Class.forName():默認使用調用者的類加載器,通常從 Bootstrap 開始向下委派,開銷最大Thread.currentThread().getContextClassLoader():獲取線程上下文類加載器,通常是 SystemClassLoader,最直接訪問應用類,效率最高設計亮點:
場景:框架定義接口,第三方提供實現,框架自動發現並加載,無需硬編碼依賴。
// java.sql.Driver 接口定義在 JDK
// MySQL 驅動 JAR 中 META-INF/services/java.sql.Driver 內容:
com.mysql.cj.jdbc.Driver
// 框架代碼(ServiceLoader 標準用法):
ServiceLoader<Driver> loader = ServiceLoader.load(Driver.class);
for (Driver driver : loader) {
if (driver.acceptsURL(url)) {
return driver.connect(url, props);
}
}
| 模式 | 核心機制 | 耦合度 | 動態性 | 性能 | 典型場景 | 代表案例 |
|---|---|---|---|---|---|---|
| new 關鍵字 | 直接調用構造器 | 最高(強依賴具體類) | 無 | 最高 | 簡單工具類、值對象、測試代碼 | String、ArrayList |
| 簡單工廠 | 集中創建邏輯 + 接口 | 中(依賴工廠類) | 運行時條件分支 | 高 | 有限已知實現集、策略選擇 | ZXing Writer、JDK Calendar |
| 工廠方法 | 子類決定實例化 | 低(依賴抽象工廠) | 繼承擴展 | 高 | 框架預留擴展點 | Collection.iterator() |
| 抽象工廠 | 族系列對象創建 | 低 | 配置切換族 | 中 | 多維度變化(UI 主題、數據庫方言) | MyBatis Environment、JDBC DriverManager |
| 反射 + Class.forName | 字符串類名動態加載 | 極低(零編譯依賴) | 完全動態 | 低(反射開銷) | 插件系統、熱插拔、配置驅動 | JDBC Driver、Servlet 容器 |
| 線程上下文類加載器 | 突破雙親委派加載應用類 | 極低 | 完全動態 | 中 | Web 容器、模塊化、OSGi、插件 | Webwork/Struts2、Tomcat、Spring |
| SPI (ServiceLoader) | 標準化服務發現協議 | 極低(面向接口編程) | 部署時動態 | 中(緩存後極快) | JDK 標準擴展點、第三方庫集成 | JDBC、Logging、JSON、Script Engine |
| 依賴注入 (Spring) | IoC 容器管理生命周期 | 零(構造器/Setter 注入) | 配置/註解驅動 | 啟動慢、運行極快 | 企業級應用核心組件裝配 | Spring Bean、EJB、CDI |
new 或構建器模式@Autowired 構造器注入MethodHandle、VarHandle、GraalVM 原生編譯setAccessible(true),模塊化系統可能被禁止ServiceLoader 載入順序不確定,需優先級機制或手動選擇@Lazy 打破對象創建看似微末,實則是軟件架構「依賴倒置、控制反轉、面向接口編程」原則的落地起點。選對實例化模式,是從「寫出能跑的代碼」邁向「設計可演進的系統」的關鍵一步。