轉換工作歷時三個月完成。 Java 應用程式編譯通過,基本冒煙測試也順利完成,並被宣布可以部署。上線六個月後,平台團隊發現了一個令人擔憂的問題:Java 團隊中沒有人能夠維護這段程式碼。程式碼審查所需的時間是正常 Java 程式碼審查的兩倍。錯誤報告中引用了許多概念,例如工作儲存、流程部邏輯以及壓縮十進位欄位約定,這些概念對於沒有大型主機經驗的 Java 開發人員來說難以理解。雖然該應用程式是從 COBOL 轉換而來,但轉換後的 Java 程式碼的思維方式、結構和行為卻與 COBOL 非常相似。它簡直就是個「Jobol」。
Jobol(有時也寫JOBOL)是Java程式碼的合成詞,它保留了COBOL的過程式語義,但並沒有採用Java的物件導向特性。分析公司Intellyx的創辦人Jason Bloomberg對此做了明確的定義:Jobol在技術上是有效的Java程式碼,它保留了原有的軟體架構和COBOL語義,要求開發人員像對待COBOL一樣對待Java程式碼。由此帶來的一個關鍵後果是:將COBOL轉換為Jobol的企業並沒有擺脫對COBOL專業知識的需求。它只是將這種專業知識的需求轉移到了一種本身並不原生支援程式碼中包含的COBOL模式的語言上。它創建了一個比原始COBOL系統更難維護的系統,並且沒有提供任何預期的Java優勢。
Jobol 的真實樣貌
在診斷原因之前,需要先找出規律。以下是同一段業務邏輯(客戶信用檢查),以三種形式呈現:原始 COBOL 程式碼、自動化轉換器通常產生的 Jobol 程式碼,以及熟練的現代化開發人員產生的地道 Java 程式碼。
原始的 COBOL:
科博爾
PERFORM-CREDIT-CHECK.
MOVE WS-CUST-ID TO DB-CUST-KEY
READ CUSTOMER-FILE KEY IS DB-CUST-KEY
INVALID KEY
MOVE 'NOT FOUND' TO WS-STATUS
PERFORM HANDLE-MISSING-CUSTOMER
END-READ
IF CUST-CREDIT-SCORE < 650
MOVE 'DECLINED' TO WS-CREDIT-RESULT
MOVE 'Score below threshold' TO WS-DECLINE-REASON
ELSE IF CUST-OUTSTANDING-BALANCE > CUST-CREDIT-LIMIT
MOVE 'DECLINED' TO WS-CREDIT-RESULT
MOVE 'Balance exceeds limit' TO WS-DECLINE-REASON
ELSE
MOVE 'APPROVED' TO WS-CREDIT-RESULT
END-IF.
由逐行轉換器產生的 Jobol:
Java的
// Jobol: procedural COBOL logic mapped directly to Java
// No OOP, no encapsulation, static global state, COBOL naming conventions
public class CreditCheckProgram {
static String WS_CUST_ID;
static String DB_CUST_KEY;
static String WS_STATUS;
static String WS_CREDIT_RESULT;
static String WS_DECLINE_REASON;
static int CUST_CREDIT_SCORE;
static double CUST_OUTSTANDING_BALANCE;
static double CUST_CREDIT_LIMIT;
public static void PERFORM_CREDIT_CHECK() {
DB_CUST_KEY = WS_CUST_ID;
Customer customer = CustomerFile.read(DB_CUST_KEY);
if (customer == null) {
WS_STATUS = "NOT FOUND";
HANDLE_MISSING_CUSTOMER();
return;
}
CUST_CREDIT_SCORE = customer.getCreditScore();
CUST_OUTSTANDING_BALANCE = customer.getOutstandingBalance();
CUST_CREDIT_LIMIT = customer.getCreditLimit();
if (CUST_CREDIT_SCORE < 650) {
WS_CREDIT_RESULT = "DECLINED";
WS_DECLINE_REASON = "Score below threshold";
} else if (CUST_OUTSTANDING_BALANCE > CUST_CREDIT_LIMIT) {
WS_CREDIT_RESULT = "DECLINED";
WS_DECLINE_REASON = "Balance exceeds limit";
} else {
WS_CREDIT_RESULT = "APPROVED";
}
}
}
熟練的現代化改造所產生的慣用 Java 語言:
Java的
// Idiomatic Java: domain model, encapsulation, value objects, Spring service
@Service
public class CreditCheckService {
private final CustomerRepository customerRepository;
public CreditCheckService(CustomerRepository customerRepository) {
this.customerRepository = customerRepository;
}
public CreditDecision check(CustomerId customerId) {
Customer customer = customerRepository.findById(customerId)
.orElseThrow(() -> new CustomerNotFoundException(customerId));
if (customer.creditScore() < CreditThresholds.MINIMUM_SCORE) {
return CreditDecision.declined("Score below threshold");
}
if (customer.outstandingBalance().compareTo(customer.creditLimit()) > 0) {
return CreditDecision.declined("Balance exceeds limit");
}
return CreditDecision.approved();
}
}
Jobol 版本包含 32 行靜態全域狀態程式碼,採用過程式結構,使用 COBOL 命名,且必須先初始化全域類別狀態才能進行單元測試。而慣用版本只需 20 行程式碼,可注入、可測試,任何沒有 COBOL 背景的 Java 開發人員都能立即閱讀。兩者實現的業務規則相同,但只有其中一種版本能夠被此次現代化改造旨在提升的團隊所維護。
六種喬博爾模式及其產生原因
模式 1:全域靜態狀態取代工作存儲
COBOL 的工作儲存區 (WORKING-STORAGE SECTION) 保存程式層級的變量,這些變數在 PERFORM 呼叫之間保持不變。它是程式共享的可變狀態,每個段落都會讀取和寫入相同的 WS 變數。自動轉換器將其直接對應到單一類別上的 Java 靜態字段,雖然保留了共享狀態的語義,但卻違反了 Java 的所有封裝原則。
轉換器這樣做的原因:工作儲存到靜態欄位的對應是一種機械的一對一轉換。轉換器並不理解 Java 的封裝模型會將程式級共用狀態替換為實例變數、方法參數和傳回值。它只是轉換它所看到的內容,而沒有理解原始結構存在的原因。
其後果是:靜態狀態無法單獨測試,在沒有明確同步的情況下不安全,並且在結構上與它所取代的 COBOL 完全相同。任何需要為系統添加並發功能的開發人員都必須先解析靜態狀態,這項工作在轉換過程中被推遲,留給了後續的每一位維護者。
解決方法:根據變數的使用範圍,將工作儲存區變數對應到實例欄位、方法參數或傳回值。僅在一個段落內使用的變數變成局部變數。在段落之間傳遞的變數變成方法參數或傳回值。真正代表共享物件狀態的變數變成封裝良好的域類別上的實例欄位。
模式 2:PERFORM 鏈變成長靜態方法
COBOL 程式以 PERFORM 鏈的形式組織,主程序執行一系列命名段落,每個段落可以執行其他段落。自動轉換器將每個段落轉換為靜態方法,並將每個 PERFORM 轉換為靜態方法呼叫。最終產生一個包含數十個靜態方法的類,這些方法透過一個反映 COBOL PERFORM 結構的呼叫圖相互呼叫。
Java的
// Jobol: PERFORM chain mapped to static method calls
public class ProcessOrderProgram {
public static void main() {
validateOrder();
checkInventory();
calculateTotal();
applyDiscounts();
processPayment();
generateConfirmation();
updateInventory();
}
public static void validateOrder() { /* ... */ }
public static void checkInventory() { /* ... */ }
// ... 40 more static methods
}
轉換器這樣做的原因: COBOL 段落到 Java 方法的結構映射是直接的。轉換器能夠正確識別 PERFORM 表示「呼叫此程式碼單元」。但它忽略了 Java 的物件模型是圍繞領域實體組織程式碼的,而不是圍繞處理步驟的。
修復: 識別 PERFORM 鏈中嵌入的領域概念。 validateOrder, calculateTotal以及 applyDiscounts 是對訂單進行的操作。 processPayment 這是在支付環境中進行的操作。 checkInventory 以及 updateInventory 這些操作是對庫存的操作。重構將靜態方法分發到它們所屬的領域類別中,並用一個簡短的編排方法取代了冗長的過程呼叫鏈,該方法會將操作委託給對應的領域物件。
模式 3:無精度處理的數值類型
COBOL 的壓縮十進位 (COMP-3) 欄位具有精確的十進位運算能力。將 COMP-3 對應到 Java 的轉換器 double or float 引入系統性的浮點舍入誤差,這些誤差會在數百萬筆金融交易中累積。
Java的
// Jobol: COMP-3 mapped to double (wrong)
static double WS_TRANSACTION_AMOUNT; // Was PIC S9(9)V99 COMP-3
static double WS_RUNNING_TOTAL;
// After 10M additions of 0.1:
// Expected: 1,000,000.00
// Actual: 1,000,000.0000000002 (floating point accumulation)
Java的
// Idiomatic Java: COMP-3 mapped to BigDecimal (correct)
private BigDecimal transactionAmount; // Preserves decimal precision
private BigDecimal runningTotal;
// After 10M additions of 0.1:
// Result: exactly 1,000,000.00
轉換器這樣做的原因: double 是 Java 的預設數值類型。如果轉換器無法準確理解 COMP-3 語義,則會預設使用最常見的 Java 數值類型,這對於精確的十進制運算來說是錯誤的選擇。
修復: 每個 COMP-3 欄位都對應到 BigDecimal所有涉及 COMP-3 值的數值計算都使用 BigDecimal 算術。 scale() 的 BigDecimal 與 PIC 條款中隱含的小數位數相符: PIC S9(9)V99 隱含 2 位小數,映射到 BigDecimal 比例尺為 2。
模式 4:EBCDIC 字串和位元組數組遺跡
COBOL 將字元資料處理為採用 EBCDIC 編碼的固定長度填充字串。轉換器通常會產生 Java 程式碼,即使轉換為 UTF-8 編碼後,這些程式碼仍然保留了固定長度、空格填充的字串語義,導致生成的 Java 程式碼處理字串的方式類似於 20 世紀 70 年代的大型機程式設計師。
Java的
// Jobol: preserves COBOL fixed-length string handling
static char[] WS_CUSTOMER_NAME = new char[40]; // Was PIC X(40)
static char[] WS_CUSTOMER_ADDR = new char[80]; // Was PIC X(80)
// COBOL-style space-padding comparison
if (new String(WS_CUSTOMER_NAME).trim().equals("SMITH")) { ... }
// COBOL-style string concatenation with padding
System.arraycopy(name.toCharArray(), 0, WS_CUSTOMER_NAME, 0,
Math.min(name.length(), 40));
Java的
// Idiomatic Java: proper String types and domain objects
private String customerName; // Trim on read, no padding
private String customerAddress;
// Clean comparison
if ("Smith".equalsIgnoreCase(customer.name())) { ... }
轉換器這樣做的原因: COBOL PIC X(40) 定義具有固定的長度,轉換器會將其保留為 Java 陣列的大小。雖然 EBCDIC 到 UTF-8 的轉換可以處理,但固定長度語意卻被不必要地傳遞了。
修復: 地圖 PIC X(n) 字段 String 物件在讀取時進行修剪,並移除固定長度約束。長度約束僅在持久化邊界(資料庫列長度、檔案格式位置)處起作用,在領域模型中則無關緊要。
模式 5:透過狀態碼進行錯誤處理
COBOL 透過狀態碼傳遞錯誤訊息:FILE STATUS、SQLCODE 和 RETURN-CODE。 Java 轉換器保留了這個模式,產生的程式碼會在每次操作後檢查整數狀態碼,而不是使用 Java 的異常模型。
Java的
// Jobol: COBOL status code pattern in Java
static int WS_FILE_STATUS;
static int WS_SQL_CODE;
CustomerFile.read(DB_CUST_KEY);
WS_FILE_STATUS = CustomerFile.getLastStatus();
if (WS_FILE_STATUS == 10) {
// Not found
} else if (WS_FILE_STATUS == 35) {
// File not available
} else if (WS_FILE_STATUS != 0) {
// Unknown error
}
Java的
// Idiomatic Java: exceptions for error conditions
try {
Customer customer = customerRepository.findById(customerId)
.orElseThrow(() -> new CustomerNotFoundException(customerId));
// process customer
} catch (DatabaseUnavailableException e) {
log.error("Database unavailable during credit check", e);
throw new CreditCheckServiceException("Service temporarily unavailable", e);
}
轉換器這樣做的原因:狀態碼檢查是 COBOL 的固有模式;將其轉換為返回或設定狀態欄位的 Java 方法是一種機械轉換。認識到 Java 異常是其慣用的等效方式需要架構判斷,而不是機械翻譯。
修復: 將 COBOL 錯誤條件對應到 Java 異常。文件狀態 10(未找到記錄)變為 Optional.empty() 或特定領域的異常。 SQLCODE 錯誤會變成類型化的資料庫異常。 Java 程式碼結構體現了程式碼意圖:預期故障模式使用 try/catch 語句,意外故障模式使用未檢查異常。
模式 6:單一階級結構
COBOL 程式的設計本質上是單體式的,單一程式處理整個業務功能。轉換器會為每個 COBOL 程式建立一個單獨的 Java 類,產生的 Java 類別可能長達數千行,並且處理多種職責,而物件導向設計會將這些職責分散到多個功能不同的類別中。
轉換器這樣做的原因:一個 COBOL 程式對應一個 Java 類,轉換器的轉換單元就是程式本身。程式的內部結構(段落、節、PERFORM 鏈)直接成為類別的內部結構,而無需將其分解成更小的類別。
解決方案:對每個轉換後的類別應用單一職責原則。一個包含 3,000 行程式碼的 Jobol 類,其功能包括驗證輸入、從資料庫讀取資料、應用業務規則、格式化輸出和寫入文件,應該被拆分為:輸入驗證器、儲存庫、領域服務、格式化器和寫入器,每個元件都可獨立測試,並且每個元件都專注於一項職責。
結構分析先決條件
每個 Jobol 模式都有對應的重構方法。但是,重構不能由轉換器直接應用,它需要先理解程式的功能,才能決定如何重構。這種理解正是任何轉換工具在運行之前對來源 COBOL 程式碼進行結構分析所提供的。
具體而言,結構分析能夠提供轉換決策所需的輸入資訊:
業務邏輯清單。它完整列舉了每個程式中的 EVALUATE 條件、IF 分支和 COMPUTE 公式,以及程式實際做出的決策,而不僅僅是程式的結構。這份清單是重構後的 Java 程式碼必須符合的規格。一個包含 847 個 EVALUATE 條件的程式需要一個能夠容納 847 個決策點的目標類別結構;轉換器的輸出是初始草稿,而非最終產品。
依賴關係圖。它定義了哪些程式呼叫了哪些其他程式。跨越多個 COBOL 程式的 PERFORM 鏈在 Java 中會轉換為服務呼叫圖。依賴關係圖決定了哪些程式可以獨立重構(入站依賴較少),哪些程式必須作為協調群組進行重構(透過副本或共享資料存在許多共享依賴)。
識別死代碼。從未在任何生產執行路徑中執行過的段落和程式碼片段都是死程式碼,它們根本不應該出現在轉換後的 Java 程式碼中。包含死程式碼的轉換器會增加轉換後的程式碼庫大小以及後續的重構工作量。
資料模型恢復。 VSAM FD 條目、DB2 表定義和副本結構定義了程式的資料。每個 COBOL 資料結構都會對應到一個或多個 Java 域物件;這種映射關係由資料模型決定,而不是由 WORKING-STORAGE 中的欄位名稱決定。
| 分析輸入 | 轉化決策驅動因素 |
|---|---|
| 評估/如果分支計數 | 領域分類分解,每個服務處理多少個決策案例 |
| 入站相依性計數 | 重構順序,先重構依賴最少的程序。 |
| 死碼百分比 | 縮小範圍,將無法存取的程式碼從轉換中排除。 |
| COMP-3 場 | BigDecimal 每個數值欄位的映射 |
| PIC X 場長 | 字串類型映射和持久層約束 |
| 執行圖結構 | 服務層分解,PERFORM 鏈變成域服務調用 |
| 文件狀態用法 | 異常模型設計,狀態代碼與異常類型之間的映射關係 |
轉換後應該產生什麼結果呢?
Jobol 的替代方案是轉換過程,該過程以 COBOL 規範而非模板為準。 COBOL 程式了解商業邏輯,Java 程式則應以 Java 的慣用法來表示該邏輯。
轉換原理: 轉換的是意圖,不是語法。 COBOL
IF CUST-CREDIT-SCORE < 650 MOVE 'DECLINED' TO WS-CREDIT-RESULT代碼顯示「信用評分低於 650 的客戶將被拒絕」。 Java 程式碼應該用 Java 語言表達相同的意思:if (customer.creditScore() < CreditThresholds.MINIMUM_SCORE) return CreditDecision.declined(...)。 不if (CUST_CREDIT_SCORE < 650) { WS_CREDIT_RESULT = "DECLINED"; }.
能夠產生地道 Java 程式碼的實用轉換工作流程:
步驟 1: COBOL 原始碼結構分析、依賴關係圖分析、業務邏輯清單分析、資料模型復原、死程式碼辨識。此步驟產生規格。在此步驟完成之前,不會進行任何轉換。
步驟 2:領域模型設計,確定 COBOL 程序操作的業務實體(客戶、帳戶、交易、保單)。這些實體將成為 Java 領域類別。表示實體狀態的 WORKING-STORAGE 變數將成為對應領域類別的實例欄位。
步驟 3:服務層設計,確定 COBOL 段落和節所實現的業務操作(信用檢查、支付處理、餘額計算)。這些操作將轉換為對應領域服務類別中的 Java 服務方法。用於對段落進行排序的 PERFORM 鏈將成為服務的編排邏輯。
步驟 4:自動轉換作為草稿產生器,使用轉換工具根據步驟 2 和 3 中設計的領域模型,為每個程式產生一個初始 Java 草稿。對照業務邏輯清單審查草稿,以驗證每個決策分支是否都得到了表示。
步驟 5 針對 Jobol 模式進行系統重構,應用六項反模式修復:用實例狀態取代靜態狀態,將靜態方法分發到領域類,用異常替換狀態碼,替換 double - BigDecimal用以下方式取代固定長度數組 String將單體類別分解為單一職責的服務。
步驟 6:行為等效性測試,針對從 COBOL 分析產生的特徵化測試語料庫執行重構後的 Java 程式碼。每個 EVALUATE 條件和 IF 分支都應該至少有一個測試案例來執行,並驗證其與 COBOL 輸出的等效性。
SMART TS XL 從源頭阻止Jobol
Jobol 預防的結構分析前提條件,包括依賴關係圖、商業邏輯清單、死代碼識別和資料模型恢復,正是… SMART TS XL在任何轉換工具運行之前,對 COBOL 原始碼進行分析。
SMART TS XL“ 靜態程式碼分析 它列舉了 COBOL 原始程式碼中的每個 EVALUATE 條件、每個 IF 分支、每個 COMPUTE 公式以及每個 PERFORM 關係,這是重構後的 Java 程式碼必須忠實表示的完整業務邏輯清單。當轉換工具產生 Jobol 時,業務邏輯清單就是用來驗證重構的規格:清單中的每個條件都必須出現在 Java 程式碼中,並且位於對應的域類別中,使用 Java 慣用法表達。
應用程式依賴關係映射產生 PERFORM 圖和程式呼叫圖,它們決定了 Java 服務層的結構。頻繁相互呼叫的程式屬於同一個限界上下文。被許多其他程式呼叫的程式是共享服務,必須精心設計以支援 Java 架構中的所有使用者。依賴關係圖是服務層設計步驟的輸入,它將 COBOL 的隱式耦合結構轉換為 Java 的明確依賴注入結構。
遺留系統現代化分析會辨識出生產執行路徑中從未執行到的死程式碼、段落和章節,這些內容應完全排除在轉換範圍之外。將死代碼轉換為死代碼 Jobol,然後再重構死代碼 Jobol,會浪費大量精力,而死碼識別步驟可以在轉換開始前消除這些浪費。
影響分析功能可以驗證重構順序:當 COBOL 程式的介面在從 Jobol 重構為慣用的 Java 程式碼的過程中發生變化時,影響分析會識別出所有其他程序,並指出它們的 Java 等效項也必須更新,以保持與變更後的介面一致。依照依賴關係順序(先重構依賴項最少的程序)進行重構,可以盡量減少每個步驟所需的協調工作。
轉換器轉換語法,現代化改變意義。
自動 COBOL 到 Java 轉換器比人工重寫速度更快。它們的最佳化目標也不同:追求的是目標語言慣用法的語法對應,而非語意等價。當源語言(COBOL)和目標語言(Java)的結構模型存在根本差異時,語法對應就會產生 Jobol 這樣的語言。
那些避免使用 Jobol 的組織並非使用了更好的轉換器。他們將轉換器視為草稿產生器而非成品,在轉換開始前投入資源進行結構分析,在轉換器運行前設計 Java 領域模型,並以業務邏輯清單作為驗證規範,對轉換器的輸出進行系統性的重構。
使用 COBOL 是可以避免的。避免使用 COBOL 的代價是每次轉換前都需要進行結構分析和領域建模工作。而使用 COBOL 的代價是維護一個需要 COBOL 專業知識的 Java 系統,這意味著現代化帶來的預期效益——即能夠由 Java 開發人員維護系統——永遠無法實現。
轉換的是意圖,不是語法。這才是現代化的全部價值。


