級(jí) 別: 中級(jí) Tom McQueeney (tom.mcqueeney@gmail.com), 首席技術(shù)顧問(wèn), Idea Integration
2007 年 9 月 24 日 Java™ 腳本編程 API(Java scripting API)是 Java SE 6 中新增的,它向后兼容 Java SE 5,支持以一種簡(jiǎn)單且統(tǒng)一的方式在運(yùn)行時(shí)從 Java 應(yīng)用程序調(diào)用數(shù)十種腳本語(yǔ)言。本系列的 第 1 部分 介紹了這個(gè) API 的基本特性。第 2 部分進(jìn)一步講解它的功能,演示如何在無(wú)需停止并重新啟動(dòng)應(yīng)用程序的情況下,在運(yùn)行時(shí)執(zhí)行外部 Ruby、Groovy 和 JavaScript 腳本以修改業(yè)務(wù)邏輯。 Java SE 6 中新增的 Java 腳本編程 API 為運(yùn)行用各種動(dòng)態(tài)語(yǔ)言編寫的外部程序(并與之共享代碼和數(shù)據(jù))提供了一種統(tǒng)一的方式。Java 應(yīng)用程序與腳本語(yǔ)言的強(qiáng)大功能和靈活性相結(jié)合是非常有意義的,尤其是在腳本語(yǔ)言能夠更簡(jiǎn)潔地執(zhí)行某些任務(wù)的情況下。但是,Java 腳本編程 API 不僅僅能夠以一種統(tǒng)一的方式在 Java 程序中添加許多種腳本語(yǔ)言代碼,它還支持在運(yùn)行時(shí)尋找、讀取和執(zhí)行腳本??梢岳眠@些動(dòng)態(tài)功能在程序運(yùn)行時(shí)修改腳本,從而修改應(yīng)用程序的邏輯。本文演示如 何使用 Java 腳本編程 API 調(diào)用外部腳本來(lái)動(dòng)態(tài)地修改程序邏輯。還要討論在將一種或多種腳本語(yǔ)言集成到 Java 應(yīng)用程序中時(shí)可能遇到的問(wèn)題。 第 1 部分 用一個(gè) Hello World 風(fēng)格的應(yīng)用程序介紹了 Java 腳本編程 API。這里將展示一個(gè)更真實(shí)的示例應(yīng)用程序,這個(gè)程序使用腳本編程 API 創(chuàng)建一個(gè)動(dòng)態(tài)的規(guī)則引擎,它可以以外部 Groovy、JavaScript 和 Ruby 腳本的形式定義規(guī)則。這些規(guī)則決定申請(qǐng)人是否符合某些抵押產(chǎn)品的住宅貸款條件。如果用腳本語(yǔ)言定義業(yè)務(wù)規(guī)則,規(guī)則就更容易編寫,也便于非程序員(比如貸款 審查員)閱讀。通過(guò)使用 Java 腳本編程 API 將這些規(guī)則放在程序之外,還可以支持在應(yīng)用程序運(yùn)行時(shí)修改規(guī)則和添加新的抵押產(chǎn)品。 真實(shí)的應(yīng)用程序 這個(gè)示例應(yīng)用程序?yàn)樘摌?gòu)的 Shaky Ground Financial 公司處理住宅貸款申請(qǐng)。住宅抵押行業(yè)不斷地推出新的貸款產(chǎn)品,還常常修改對(duì)合格申請(qǐng)人的限制規(guī)則。Shaky Ground 公司不但希望能夠快速地添加和刪除抵押產(chǎn)品,還需要快速修改業(yè)務(wù)規(guī)則,從而控制哪些人符合產(chǎn)品的貸款條件。 Java 腳本編程 API 正好能夠滿足這種需求。這個(gè)應(yīng)用程序由一個(gè) ScriptMortgageQualifier 類組成,這個(gè)類負(fù)責(zé)判斷打算購(gòu)買某一資產(chǎn)的貸款人是否符合給定的抵押貸款產(chǎn)品的條件。清單 1 給出這個(gè)類。 清單 1. ScriptMortgageQualifier 類 // Imports and Javadoc not shown. public class ScriptMortgageQualifier { private ScriptEngineManager scriptEngineManager = new ScriptEngineManager(); public MortgageQualificationResult qualifyMortgage( Borrower borrower, Property property, Loan loan, File mortgageRulesFile ) throws FileNotFoundException, IllegalArgumentException, ScriptException { ScriptEngine scriptEngine = getEngineForFile(mortgageRulesFile); if (scriptEngine == null) { throw new IllegalArgumentException( "No script engine on classpath to handle file: " + mortgageRulesFile ); } // Make params accessible to scripts by adding to engine's context. scriptEngine.put("borrower", borrower); scriptEngine.put("property", property); scriptEngine.put("loan", loan); // Make return-value object available to scripts. MortgageQualificationResult scriptResult = new MortgageQualificationResult(); scriptEngine.put("result", scriptResult); // Add an object scripts can call to exit early from processing. scriptEngine.put("scriptExit", new ScriptEarlyExit()); try { scriptEngine.eval(new FileReader(mortgageRulesFile)); } catch (ScriptException se) { // Re-throw exception unless it's our early-exit exception. if (se.getMessage() == null || !se.getMessage().contains("ScriptEarlyExitException") ) { throw se; } // Set script result message if early-exit exception embedded. Throwable t = se.getCause(); while (t != null) { if (t instanceof ScriptEarlyExitException) { scriptResult.setMessage(t.getMessage()); break; } t = t.getCause(); } } return scriptResult; } /** Returns a script engine based on the extension of the given file. */ private ScriptEngine getEngineForFile(File f) { String fileExtension = getFileExtension(f); return scriptEngineManager.getEngineByExtension(fileExtension); } /** Returns the file's extension, or "" if the file has no extension */ private String getFileExtension(File file) { String scriptName = file.getName(); int dotIndex = scriptName.lastIndexOf('.'); if (dotIndex != -1) { return scriptName.substring(dotIndex + 1); } else { return ""; } } /** Internal exception so ScriptEarlyExit.exit can exit scripts early */ private static class ScriptEarlyExitException extends Exception { public ScriptEarlyExitException(String msg) { super(msg); } } /** Object passed to all scripts so they can indicate an early exit. */ private static class ScriptEarlyExit { public void noMessage() throws ScriptEarlyExitException { throw new ScriptEarlyExitException(null); } public void withMessage(String msg) throws ScriptEarlyExitException { throw new ScriptEarlyExitException(msg); } } } | 這個(gè)類相當(dāng)簡(jiǎn)單,因?yàn)樗阉袠I(yè)務(wù)決策任務(wù)都委派給了外部腳本。每個(gè)腳本表示一個(gè)抵押產(chǎn)品。每個(gè)腳本文件中的代碼包含一系 列業(yè)務(wù)規(guī)則,這些規(guī)則定義了符合這種抵押產(chǎn)品要求的貸款人類型、資產(chǎn)類型和貸款類型。由于采用了這種方式,只需在腳本目錄中添加新的腳本文件,就可以添加 新的抵押產(chǎn)品。如果某一抵押產(chǎn)品的業(yè)務(wù)邏輯改變了,那么只需更新腳本來(lái)反映規(guī)則的變化。 通過(guò)用腳本語(yǔ)言編寫抵押產(chǎn)品業(yè)務(wù)規(guī)則,可以展示 Java 腳本編程 API 的功能。這個(gè)程序還說(shuō)明有時(shí)候腳本語(yǔ)言代碼更容易閱讀、修改和理解,即使是非程序員也可以掌握腳本代碼。
ScriptMortgageQualifier 類的工作方式 ScriptMortgageQualifier 中的主要方法是 qualifyMortgage() 。 這個(gè)方法通過(guò)參數(shù)接受以下信息: - 貸款人
- 要購(gòu)買的資產(chǎn)
- 貸款細(xì)節(jié)
- 一個(gè)
File 對(duì)象,其中包含要執(zhí)行的腳本 這個(gè)方法的任務(wù)是用業(yè)務(wù)實(shí)體參數(shù)運(yùn)行腳本文件并返回一個(gè)結(jié)果對(duì)象,這個(gè)對(duì)象指出貸款人是否符合抵押產(chǎn)品的要求。這里沒(méi)有給 出 Borrower 、Property 和 Loan 的代碼。它們只是簡(jiǎn)單的實(shí)體類,可以在本文的源代碼中找到它們的代碼(見(jiàn) 下 載)。 為了找到一個(gè) ScriptEngine 來(lái)運(yùn)行腳本文件,qualifyMortgage() 方法使用了 getEngineForFile() 內(nèi)部 helper 方法。getEngineForFile() 方法使用 scriptEngineManager 實(shí)例變量(這個(gè)變量在類實(shí)例化時(shí)被設(shè)置為一個(gè) ScriptEngineManager ) 尋找能夠處理具有給定文件擴(kuò)展名的腳本的腳本引擎。getEngineForFile() 方法使用 ScriptEngineManager.getEngineByExtension() 方法(見(jiàn) 清 單 1 中的粗體代碼)搜索并返回 ScriptEngine 。 找到腳本引擎之后,qualifyMortgage() 將它接收的實(shí)體參數(shù)綁定到引擎的上下文,從而讓腳本能夠使用這些參數(shù)。前三個(gè) scriptEngine.put() 調(diào)用(也是粗體代碼)執(zhí)行這些綁定。第四個(gè) scriptEngine.put() 調(diào)用創(chuàng)建一個(gè)新的 MortgageQualificationResult Java 對(duì)象并通過(guò)腳本引擎共享它。腳本可以通過(guò)設(shè)置這個(gè)對(duì)象的屬性將它的運(yùn)行結(jié)果返回給 Java 應(yīng)用程序,qualifyMortgage() 將返回這個(gè)共享對(duì)象。腳本使用 result 全局變量訪問(wèn)這個(gè) Java 對(duì)象。每個(gè)腳本負(fù)責(zé)使用這個(gè)共享對(duì)象將自己的結(jié)果返回給 Java 應(yīng)用程序。 最后一個(gè) scriptEngine.put() 調(diào)用讓腳本可以通過(guò) scriptExit 變量使用一個(gè)內(nèi)部 helper 類(ScriptEarlyExit ,見(jiàn) 清 單 1)的實(shí)例。ScriptEarlyExit 定義了兩個(gè)簡(jiǎn)單的方法 —— withMessage() 和 noMessage() ,它們惟一的作用是拋出一個(gè)異常。如果腳本調(diào)用 scriptExit.withMessage() 或 scriptExit.noMessage() ,那么方法拋出一個(gè) ScriptEarlyExitException 異常。腳本引擎會(huì)捕捉這個(gè)異常、終止腳本處理并向調(diào)用腳本的 eval() 方法拋出一個(gè) ScriptException 異常。 通過(guò)以這種迂回的方式提前退出腳本,就可以以一致的方式從函數(shù)或方法外的腳本處理過(guò)程返回。并非所有腳本語(yǔ)言都提供了這種 方式所需的語(yǔ)句。例如,在 JavaScript 中,在執(zhí)行高層代碼時(shí)(這個(gè)示例應(yīng)用程序中的抵押處理腳本正是采用這種構(gòu)造方式),無(wú)法使用 return 語(yǔ)句。共享對(duì)象 scriptExit 解決了這個(gè)問(wèn)題,一旦腳本判斷出貸款人不符合抵押產(chǎn)品的要求,用任何語(yǔ)言編寫的腳本都可以通過(guò)這個(gè)對(duì)象退出。 在 qualifyMortgage 中,對(duì)腳本引擎的 eval 方法的調(diào)用(見(jiàn)粗體代碼)使用一個(gè) try /catch 塊捕捉 ScriptException 異常。catch 塊中的代碼檢查 ScriptException 錯(cuò)誤消息,從而判斷這個(gè)腳本異常是由 ScriptEarlyExitException 造成的,還是由真正的腳本錯(cuò)誤造成的。如果錯(cuò)誤消息包含名稱 ScriptEarlyExitException ,那么代碼就 認(rèn)為一切正常并忽略這個(gè)腳本異常。 這種在 Java 腳本編程 API 的腳本異常錯(cuò)誤消息中搜索字符串的技術(shù)有點(diǎn)兒笨拙,但這對(duì)于本示例中使用的 Groovy、JavaScript 和 Ruby 語(yǔ)言解釋器是有效的。如果所有腳本語(yǔ)言實(shí)現(xiàn)將從調(diào)用的 Java 代碼拋出的 Java 異常添加到異常堆棧中,那么會(huì)更方便,這樣就可以使用 Throwable.getCause() 方法獲取這些異常。JRuby 和 Groovy 等解釋器會(huì)這樣做,但是內(nèi)置的 Rhino JavaScript 解釋器并不這樣做。
運(yùn)行代碼:ScriptMortgageQualifierRunner 為了測(cè)試 ScriptMortgageQualifier 類,將使用測(cè)試數(shù)據(jù)表示四個(gè)貸款人、貸款人打算購(gòu)買的一項(xiàng)資產(chǎn)和一筆抵押貸款。我們將用一個(gè)貸款人、資產(chǎn)和貸款運(yùn)行所有三個(gè)腳本,檢查貸款人是否滿足腳本 所代表的抵押產(chǎn)品的業(yè)務(wù)規(guī)則。 清單 2 給出 ScriptMortgageQualifierRunner 程序的部分代碼,我們將用這個(gè)程序創(chuàng)建測(cè)試對(duì)象、在一個(gè)目錄中尋找腳本文件并通過(guò) 清 單 1 中的 ScriptMortgageQualifier 類運(yùn)行它們。為了節(jié)省篇幅,這里沒(méi)有給出這個(gè)程序的 createGoodBorrower() 、createAverageBorrower() 、createInvestorBorrower() 、createRiskyBorrower() 、createProperty() 和 createLoan() helper 方法。這些方法的作用僅僅是創(chuàng)建實(shí)體對(duì)象并設(shè)置測(cè)試所需的值。在 下 載 一節(jié)中可以獲得所有方法的完整源代碼。 清單 2. ScriptMortgageQualifierRunner 程序 // Imports and some helper methods not shown. public class ScriptMortgageQualifierRunner { private static File scriptDirectory; private static Borrower goodBorrower = createGoodBorrower(); private static Borrower averageBorrower = createAverageBorrower(); private static Borrower investorBorrower = createInvestorBorrower(); private static Borrower riskyBorrower = createRiskyBorrower(); private static Property property = createProperty(); private static Loan loan = createLoan(); /** * Main method to create a File for the directory name on the command line, * then call the run method if that directory exists. */ public static void main(String[] args) { if (args.length > 0 && args[0].contains("-help")) { printUsageAndExit(); } String dirName; if (args.length == 0) { dirName = "."; // Current directory. } else { dirName = args[0]; } scriptDirectory = new File(dirName); if (!scriptDirectory.exists() || !scriptDirectory.isDirectory()) { printUsageAndExit(); } run(); } /** * Determines mortgage loan-qualification status for four test borrowers by * processing all script files in the given directory. Each script will determine * whether the given borrower is qualified for a particular mortgage type */ public static void run() { ScriptMortgageQualifier mortgageQualifier = new ScriptMortgageQualifier(); for(;;) { // Requires Ctrl-C to exit runQualifications(mortgageQualifier, goodBorrower, loan, property); runQualifications(mortgageQualifier, averageBorrower, loan, property); loan.setDownPayment(30000.0); // Reduce down payment to 10% runQualifications(mortgageQualifier, investorBorrower, loan, property); loan.setDownPayment(10000.0); // Reduce down payment to 3 1/3% runQualifications(mortgageQualifier, riskyBorrower, loan, property); waitOneMinute(); } } /** * Reads all script files in the scriptDirectory and runs them with this borrower's * information to see if he/she qualifies for each mortgage product. */ private static void runQualifications( ScriptMortgageQualifier mortgageQualifier, Borrower borrower, Loan loan, Property property ) { for (File scriptFile : getScriptFiles(scriptDirectory)) { // Print info about the borrower, loan and property. System.out.println("Processing file: " + scriptFile.getName()); System.out.println(" Borrower: " + borrower.getName()); System.out.println(" Credit score: " + borrower.getCreditScore()); System.out.println(" Sales price: " + property.getSalesPrice()); System.out.println(" Down payment: " + loan.getDownPayment()); MortgageQualificationResult result = null; try { // Run the script rules for this borrower on the loan product. result = mortgageQualifier.qualifyMortgage( borrower, property, loan, scriptFile ); } catch (FileNotFoundException fnfe) { System.out.println( "Can't read script file: " + fnfe.getMessage() ); } catch (IllegalArgumentException e) { System.out.println( "No script engine available to handle file: " + scriptFile.getName() ); } catch (ScriptException e) { System.out.println( "Script '" + scriptFile.getName() + "' encountered an error: " + e.getMessage() ); } if (result == null) continue; // Must have hit exception. // Print results. System.out.println( "* Mortgage product: " + result.getProductName() + ", Qualified? " + result.isQualified() + "\n* Interest rate: " + result.getInterestRate() + "\n* Message: " + result.getMessage() ); System.out.println(); } } /** Returns files with a '.' other than as the first or last character. */ private static File[] getScriptFiles(File directory) { return directory.listFiles(new FilenameFilter() { public boolean accept(File dir, String name) { int indexOfDot = name.indexOf('.'); // Ignore files w/o a dot, or with dot as first or last char. if (indexOfDot < 1 || indexOfDot == (name.length() - 1)) { return false; } else { return true; } } }); } private static void waitOneMinute() { System.out.println( "\nSleeping for one minute before reprocessing files." + "\nUse Ctrl-C to exit..." ); System.out.flush(); try { Thread.sleep(1000 * 60); } catch (InterruptedException e) { System.exit(1); } } } | ScriptMortgageQualifierRunner 中的 main() 方法搜索命令行上提供的腳本文件目錄,如果這個(gè)目錄存在,就用目錄的 File 對(duì)象設(shè)置一個(gè)靜態(tài)變量,并調(diào)用 run() 方法執(zhí)行進(jìn)一步的處理。 run() 方法對(duì) 清 單 1 中的 ScriptMortgageQualifier 類進(jìn)行實(shí)例化,然后用一個(gè)無(wú)限循環(huán)調(diào)用內(nèi)部方法 runQualifications() , 測(cè)試四個(gè)貸款人/貸款場(chǎng)景。這個(gè)無(wú)限循環(huán)模擬連續(xù)的抵押申請(qǐng)?zhí)幚?。這個(gè)循環(huán)讓我們可以在腳本目錄中添加或修改腳本文件(抵押貸款產(chǎn)品),這些修改會(huì)動(dòng)態(tài)地 生效,不需要停止應(yīng)用程序。因?yàn)檫@個(gè)應(yīng)用程序的業(yè)務(wù)邏輯放在外部腳本中,所以可以在運(yùn)行時(shí)動(dòng)態(tài)地修改業(yè)務(wù)邏輯。 對(duì)于腳本目錄中的每個(gè)腳本文件,runQualifications() helper 方法分別調(diào)用 ScriptMortgageQualifer.qualifyMortgage 一次。每個(gè)調(diào)用前面有一系列打印語(yǔ)句,它們輸出腳本文件和貸款人的相關(guān)信息;調(diào)用之后,用打印語(yǔ)句顯示結(jié)果,即貸款人是否符合抵押產(chǎn)品的要求。腳本代碼使 用共享的 MortgageQualificationResult Java 對(duì)象返回其結(jié)果,檢查這個(gè)對(duì)象的屬性就可以判斷貸款人是否合格。 本文的源代碼 ZIP 文件包含三個(gè)用 Groovy、JavaScript 和 Ruby 編寫的腳本文件示例。它們分別代表一種標(biāo)準(zhǔn)的 30 年期固定利率抵押貸款產(chǎn)品。腳本中的代碼判斷貸款人是否符合這種抵押類型的要求,然后通過(guò)調(diào)用腳本引擎 put() 方法中提供的共享全局變量 result 來(lái)返回結(jié)果。全局變量 result 是 MortgageQualificationResult 類的實(shí)例(部分代碼見(jiàn)清單 3)。 清單 3. MortgageQualificationResult 類 public class MortgageQualificationResult { private boolean qualified; private double interestRate; private String message; private String productName; // .. Standard setters and getters not shown. } | 腳本設(shè)置 result 的屬性,從而指出貸款人是否符合抵押貸款的要求以及應(yīng)該采用的利率。腳本可以通過(guò) message 和 productName 屬性指出導(dǎo)致貸款人不合格的原因和返回相關(guān)的產(chǎn)品名稱。
腳本文件 在給出 ScriptMortgageQualifierRunner 的輸出之前,我們先看看這個(gè)程序運(yùn)行的 Groovy、JavaScript 和 Ruby 腳本文件。Groovy 腳本中的業(yè)務(wù)邏輯定義了一種條件相當(dāng)寬松的抵押產(chǎn)品,同時(shí)由于金融風(fēng)險(xiǎn)比較高,因此利率比較高。JavaScript 腳本代表一種政府擔(dān)保的抵押貸款,這種貸款要求貸款人必須滿足最大收入和其他限制。Ruby 腳本定義的抵押產(chǎn)品業(yè)務(wù)規(guī)則要求貸款人有良好的信用記錄,這些人要支付足夠的首付款,這種抵押貸款的利率比較低。 清單 4 給出 Groovy 腳本,即使您不了解 Groovy,也應(yīng)該能夠看懂這個(gè)腳本。 清單 4. Groovy 抵押腳本 /* This Groovy script defines the "Groovy Mortgage" product. This product is relaxed in its requirements of borrowers. There is a higher interest rate to make up for the looser standard. All borrowers will be approved if their credit history is good, they can make a down payment of at least 5%, and they either earn more than $2,000/month or have a net worth (assets minus liabilities) of $25,000. */ // Our product name. result.productName = 'Groovy Mortgage' // Check for the minimum income and net worth def netWorth = borrower.totalAssets - borrower.totalLiabilities if (borrower.monthlyIncome < 2000 && netWorth < 25000) { scriptExit.withMessage "Low monthly income of ${borrower.monthlyIncome}" + ' requires a net worth of at least $25,000.' } def downPaymentPercent = loan.downPayment / property.salesPrice * 100 if (downPaymentPercent < 5) { scriptExit.withMessage 'Down payment of ' + "${String.format('%1$.2f', downPaymentPercent)}% is insufficient." + ' 5% minimum required.' } if (borrower.creditScore < 600) { scriptExit.withMessage 'Credit score of 600 required.' } // Everyone else qualifies. Find interest rate based on down payment percent. result.qualified = true result.message = 'Groovy! You qualify.' switch (downPaymentPercent) { case 0..5: result.interestRate = 0.08; break case 6..10: result.interestRate = 0.075; break case 11..15: result.interestRate = 0.07; break case 16..20: result.interestRate = 0.065; break default: result.interestRate = 0.06; break } | 請(qǐng)注意全局變量 result 、borrower 、loan 和 property ,腳本使用這些變量訪問(wèn)和設(shè)置共享 Java 對(duì)象中的值。這些變量名是通過(guò)調(diào)用 ScriptEngine.put() 方法設(shè)置的。 還要注意 result.productName = 'Groovy Mortgage' 這樣的 Groovy 語(yǔ)句。這個(gè)語(yǔ)句似乎是直接設(shè)置 MortgageQualificationResult 對(duì)象的字符串屬性 productName ,但是,清 單 3 清楚地說(shuō)明它是一個(gè)私有的實(shí)例變量。這并不 表示 Java 腳本編程 API 允許違反封裝規(guī)則,而是說(shuō)明通過(guò)使用 Java 腳本編程 API,Groovy 和大多數(shù)其他腳本語(yǔ)言解釋器可以很好地操作共享的 Java 對(duì)象。如果一個(gè) Groovy 語(yǔ)句嘗試設(shè)置或讀取 Java 對(duì)象的私有屬性值,Groovy 就會(huì)尋找并使用 JavaBean 風(fēng)格的公共 setter 或 getter 方法。例如,語(yǔ)句 result.productName = 'Groovy Mortgage' 會(huì)自動(dòng)轉(zhuǎn)換為適當(dāng)?shù)?Java 語(yǔ)句:result.setProductName("Groovy Mortgage") 。這個(gè) Java setter 語(yǔ)句也是有效的 Groovy 代碼,可以在腳本中使用,但是直接使用屬性賦值語(yǔ)句更符合 Groovy 的風(fēng)格。 現(xiàn)在看看清單 5 中的 JavaScript 抵押產(chǎn)品腳本。這個(gè) JavaScript 腳本代表一種政府擔(dān)保的貸款,政府支持這種貸款是為了提高公民的住宅擁有率。所以,業(yè)務(wù)規(guī)則要求這是貸款人購(gòu)買的第一套住宅,而且貸款人打算在此居住,而 不是出租獲利。 清單 5. JavaScript 抵押腳本 /** * This script defines the "JavaScript FirstTime Mortgage" product. * It is a government-sponsored mortgage intended for low-income, first-time * home buyers without a lot of assets who intend to live in the home. * Bankruptcies and bad (but not terrible!) credit are OK. */ result.productName = 'JavaScript FirstTime Mortgage' if (!borrower.intendsToOccupy) { result.message = 'This mortgage is not intended for investors.' scriptExit.noMessage() } if (!borrower.firstTimeBuyer) { result.message = 'Only first-time home buyers qualify for this mortgage.' scriptExit.noMessage() } if (borrower.monthlyIncome > 4000) { result.message = 'Monthly salary of $' + borrower.monthlyIncome + ' exceeds the $4,000 maximum.' scriptExit.noMessage() } if (borrower.creditScore < 500) { result.message = 'Your credit score of ' + borrower.creditScore + ' does not meet the 500 requirement.' scriptExit.noMessage() } // Qualifies. Determine interest rate based on loan amount and credit score. result.qualified = true result.message = 'Congratulations, you qualify.' if (loan.loanAmount > 450000) { result.interestRate = 0.08 // Big loans and poor credit require higher rate. } else if (borrower.creditScore < 550) { result.interestRate = 0.08 } else if (borrower.creditScore < 600) { result.interestRate = 0.07 } else if (borrower.creditScore < 700) { result.interestRate = 0.065 } else { // Good credit gets best rate. result.interestRate = 0.06 } | 注意,JavaScript 代碼不能像 Groovy 腳本那樣使用 Java scriptExit.withMessage() 方法在一個(gè)語(yǔ)句中設(shè)置不合格消息并退出腳本。這是因?yàn)?Rhino JavaScript 解釋器并不把拋出的 Java 異常在 ScriptException 堆棧跟蹤中作為嵌入的 “錯(cuò)誤原因” 向上傳遞。因此,在堆棧跟蹤中更難找到 Java 代碼拋出的腳本異常消息。所以 清 單 5 中的 JavaScript 代碼需要單獨(dú)設(shè)置結(jié)果消息,然后再調(diào)用 scriptExit.noMessage() 來(lái)產(chǎn)生異常,從而終止腳本處理。 第三個(gè)抵押產(chǎn)品腳本是用 Ruby 編寫的,見(jiàn)清單 6。這種抵押產(chǎn)品要求貸款人具有良好的信用記錄,他們可以支付百分之二十的首付款。 清單 6. Ruby 抵押腳本 # This Ruby script defines the "Ruby Mortgage" product. # It is intended for premium borrowers with its low interest rate # and 20% down payment requirement. # Our product name $result.product_name = 'Ruby Mortgage' # Borrowers with credit unworthiness do not qualify. if $borrower.credit_score < 700 $scriptExit.with_message "Credit score of #{$borrower.credit_score}" + " is lower than 700 minimum" end $scriptExit.with_message 'No bankruptcies allowed' if $borrower.hasDeclaredBankruptcy # Check other negatives down_payment_percent = $loan.down_payment / $property.sales_price * 100 if down_payment_percent < 20 $scriptExit.with_message 'Down payment must be at least 20% of sale price.' end # Borrower qualifies. Determine interest rate of loan $result.message = "Qualified!" $result.qualified = true # Give the best interest rate to the best credit risks. if $borrower.credit_score > 750 || down_payment_percent > 25 $result.interestRate = 0.06 elsif $borrower.credit_score > 700 && $borrower.totalAssets > 100000 $result.interestRate = 0.062 else $result.interestRate = 0.065 end | | 在 JRuby 1.0 中不要忘記 $ 符號(hào) 在 Ruby 腳本中訪問(wèn)共享的 Java 對(duì)象時(shí),一定要記住 Ruby 的全局變量語(yǔ)法。如果省略了全局變量前面的 $ 符號(hào),那么 JRuby 1.0 和當(dāng)前的 JRuby 1.0.1 二進(jìn)制版本會(huì)拋出一個(gè) RaiseException ,而且不提供錯(cuò)誤的相關(guān)信息。JRuby 源代碼存儲(chǔ)庫(kù)中已經(jīng)糾正了這個(gè) bug,所以在以后的二進(jìn)制版本中應(yīng)該不會(huì)出現(xiàn)這個(gè)問(wèn)題。 | | 如清單 6 所示,在 Ruby 腳本中,需要在變量名前面加上 $ 符號(hào),這樣才能訪問(wèn)放在腳本引擎范圍內(nèi)的共享 Java 對(duì)象。這是 Ruby 的全局變量語(yǔ)法。腳本引擎以全局變量的形式向腳本共享 Java 對(duì)象,所以必須使用 Ruby 的全局變量語(yǔ)法。 還要注意,在調(diào)用共享的 Java 對(duì)象時(shí),JRuby 會(huì)自動(dòng)地將 Ruby 式代碼轉(zhuǎn)換為 Java 式代碼。例如,如果 JRuby 發(fā)現(xiàn)代碼按照 Ruby 命名約定(即以下劃線分隔單詞)調(diào)用 Java 對(duì)象上的方法,比如 $result.product_name = 'Ruby Mortgage' ,那么 JRuby 會(huì)尋找不帶下劃線的大小寫混合式方法名。因此,Ruby 式方法名 product_name= 會(huì)正確地轉(zhuǎn)換為 Java 調(diào)用 result.setProductName("Ruby Mortgage") 。
程序輸出 現(xiàn)在用這三個(gè)抵押產(chǎn)品腳本文件運(yùn)行 ScriptMortgageQualifierRunner 程序,看看它的輸出。可以使用源代碼下載文件中的 Ant 腳本運(yùn)行這個(gè)程序。如果喜歡使用 Maven,那么可以按照 ZIP 文件中的 README.txt 文件中的說(shuō)明用 Maven 構(gòu)建并運(yùn)行這個(gè)程序。Ant 命令是 ant run 。run 任務(wù)確保腳本引擎和語(yǔ)言 JAR 文件在類路徑中。清單 7 給出 Ant 的輸出。 清單 7. Ant 產(chǎn)生的程序輸出 > ant run Buildfile: build.xml compile: [mkdir] Created dir: C:\temp\script-article\build-main\classes [javac] Compiling 10 source files to C:\temp\script-article\build-main\classes run: [java] Processing file: GroovyMortgage.groovy [java] Borrower: Good Borrower [java] Credit score: 800 [java] Sales price: 300000.0 [java] Down payment: 60000.0 [java] * Mortgage product: Groovy Mortgage, Qualified? true [java] * Interest rate: 0.06 [java] * Message: Groovy! You qualify. [java] Processing file: JavaScriptFirstTimeMortgage.js [java] Borrower: Good Borrower [java] Credit score: 800 [java] Sales price: 300000.0 [java] Down payment: 60000.0 [java] * Mortgage product: JavaScript FirstTime Mortgage, Qualified? false [java] * Interest rate: 0.0 [java] * Message: Only first-time home buyers qualify for this mortgage. [java] Processing file: RubyPrimeMortgage.rb [java] Borrower: Good Borrower [java] Credit score: 800 [java] Sales price: 300000.0 [java] Down payment: 60000.0 [java] * Mortgage product: Ruby Mortgage, Qualified? true [java] * Interest rate: 0.06 [java] * Message: Qualified! [java] Processing file: GroovyMortgage.groovy [java] Borrower: Average Borrower [java] Credit score: 700 [java] Sales price: 300000.0 [java] Down payment: 60000.0 [java] * Mortgage product: Groovy Mortgage, Qualified? true [java] * Interest rate: 0.06 [java] * Message: Groovy! You qualify. [java] Processing file: JavaScriptFirstTimeMortgage.js [java] Borrower: Average Borrower [java] Credit score: 700 [java] Sales price: 300000.0 [java] Down payment: 60000.0 [java] * Mortgage product: JavaScript FirstTime Mortgage, Qualified? false [java] * Interest rate: 0.0 [java] * Message: Monthly salary of $4500 exceeds the $4,000 maximum. [java] Processing file: RubyPrimeMortgage.rb [java] Borrower: Average Borrower [java] Credit score: 700 [java] Sales price: 300000.0 [java] Down payment: 60000.0 [java] * Mortgage product: Ruby Mortgage, Qualified? true [java] * Interest rate: 0.065 [java] * Message: Qualified! [java] Processing file: GroovyMortgage.groovy [java] Borrower: Investor Borrower [java] Credit score: 720 [java] Sales price: 300000.0 [java] Down payment: 30000.0 [java] * Mortgage product: Groovy Mortgage, Qualified? true [java] * Interest rate: 0.06 [java] * Message: Groovy! You qualify. [java] Processing file: JavaScriptFirstTimeMortgage.js [java] Borrower: Investor Borrower [java] Credit score: 720 [java] Sales price: 300000.0 [java] Down payment: 30000.0 [java] * Mortgage product: JavaScript FirstTime Mortgage, Qualified? false [java] * Interest rate: 0.0 [java] * Message: This mortgage is not intended for investors. [java] Processing file: RubyPrimeMortgage.rb [java] Borrower: Investor Borrower [java] Credit score: 720 [java] Sales price: 300000.0 [java] Down payment: 30000.0 [java] * Mortgage product: Ruby Mortgage, Qualified? false [java] * Interest rate: 0.0 [java] * Message: Down payment must be at least 20% of sale price. [java] Processing file: GroovyMortgage.groovy [java] Borrower: Risk E. Borrower [java] Credit score: 520 [java] Sales price: 300000.0 [java] Down payment: 10000.0 [java] * Mortgage product: Groovy Mortgage, Qualified? false [java] * Interest rate: 0.0 [java] * Message: Down payment of 3.33% is insufficient. 5% minimum required. [java] Processing file: JavaScriptFirstTimeMortgage.js [java] Borrower: Risk E. Borrower [java] Credit score: 520 [java] Sales price: 300000.0 [java] Down payment: 10000.0 [java] * Mortgage product: JavaScript FirstTime Mortgage, Qualified? true [java] * Interest rate: 0.08 [java] * Message: Congratulations, you qualify. [java] Processing file: RubyPrimeMortgage.rb [java] Borrower: Risk E. Borrower [java] Credit score: 520 [java] Sales price: 300000.0 [java] Down payment: 10000.0 [java] * Mortgage product: Ruby Mortgage, Qualified? false [java] * Interest rate: 0.0 [java] * Message: Credit score of 520 is lower than 700 minimum [java] Sleeping for one minute before reprocessing files. [java] Use Ctrl-C to exit... | 這個(gè)輸出共有 12 個(gè)部分,這是因?yàn)槌绦驅(qū)⑺膫€(gè)貸款人示例提交給三個(gè)腳本,檢查這 12 種組合中貸款人是否符合抵押產(chǎn)品的要求。為了演示本文解釋的技術(shù),這個(gè)程序會(huì)等待一分鐘,然后重復(fù)處理抵押腳本。在這段停頓期間,可以編輯腳本文件來(lái)修改 業(yè)務(wù)規(guī)則,還可以在腳本目錄中添加新的腳本文件來(lái)表示新的抵押產(chǎn)品。在每次重復(fù)運(yùn)行時(shí),程序會(huì)掃描腳本目錄并處理它找到的所有腳本文件。 例如,假設(shè)您希望提高貸款所需的最低信用分?jǐn)?shù)。在一分鐘的停頓期間,可以編輯 src/main/scripts/mortgage-products 目錄中的 JavaScriptFirstTimeMortgage.js 腳本(見(jiàn) 清 單 5),將第 23 行上的業(yè)務(wù)規(guī)則由 if (borrower.creditScore < 500) { 改為 if (borrower.creditScore < 550) { 。在下次運(yùn)行規(guī)則時(shí),Risk E. Borrower 就不再符合 JavaScript FirstTime Mortgage 的要求。這個(gè)貸款人的信用分?jǐn)?shù)是 520,這個(gè)分?jǐn)?shù)低于目前的條件。錯(cuò)誤消息現(xiàn)在是 “Your credit score of 520 does not meet the 500 requirement”,但是同樣可以在程序運(yùn)行時(shí)糾正這個(gè)錯(cuò)誤的消息。
避免動(dòng)態(tài)腳本風(fēng)險(xiǎn) 在運(yùn)行時(shí)修改程序的功能是非常強(qiáng)大的,同樣也可能導(dǎo)致風(fēng)險(xiǎn)??梢栽谡谶\(yùn)行的應(yīng)用程序中添加新的功能和新的業(yè)務(wù)規(guī)則,而無(wú) 需停止并重新啟動(dòng)應(yīng)用程序。同樣,也很容易引入新的 bug,甚至是嚴(yán)重的 bug。 但是,動(dòng)態(tài)地修改正在運(yùn)行的應(yīng)用程序并不比修改停止運(yùn)行的應(yīng)用程序更危險(xiǎn)。靜態(tài)技術(shù)僅僅意味著必須重新啟動(dòng)應(yīng)用程序,然后 才能發(fā)現(xiàn)那些新的錯(cuò)誤。良好的軟件開(kāi)發(fā)實(shí)踐表明,對(duì)生產(chǎn)性應(yīng)用程序的任何修改(無(wú)論是動(dòng)態(tài)的,還是靜態(tài)的)都應(yīng)該先接受測(cè)試,然后才能引入生產(chǎn)環(huán)境中。 Java 腳本編程 API 并未改變這一規(guī)則。 外部腳本文件可以在開(kāi)發(fā)期間進(jìn)行常規(guī)的單元測(cè)試??梢允褂?JUnit 或其他測(cè)試工具和模擬 Java 對(duì)象來(lái)測(cè)試腳本,確保腳本在運(yùn)行時(shí)不會(huì)出現(xiàn)錯(cuò)誤并產(chǎn)生所期望的結(jié)果。將應(yīng)用程序邏輯放在外部非 Java 腳本文件中并不意味著無(wú)法測(cè)試這些腳本。 如果您當(dāng)過(guò) Web CGI 腳本程序員,那么一定知道必須注意傳遞給 ScriptEngine 的 eval() 方法的東西。腳本引擎會(huì)立即執(zhí)行傳遞給 eval 方法的代碼。因此,絕不要把來(lái)自不可信來(lái)源的字符串或 Reader 對(duì)象傳遞給腳本引擎。 例如,假設(shè)我們使用腳本編程 API 遠(yuǎn)程監(jiān)視一個(gè) Web 應(yīng)用程序。我們讓腳本引擎能夠訪問(wèn)關(guān)鍵的 Java 對(duì)象,這些對(duì)象提供 Web 應(yīng)用程序的狀態(tài)信息。還創(chuàng)建一個(gè)簡(jiǎn)單的 Web 頁(yè)面,這個(gè)頁(yè)面接受任意腳本表達(dá)式,它將這些表達(dá)式傳遞給腳本引擎進(jìn)行計(jì)算并在 Web 頁(yè)面上顯示輸出。這樣就可以對(duì)正在運(yùn)行的 Java 對(duì)象進(jìn)行查詢并執(zhí)行對(duì)象上的方法,從而幫助判斷應(yīng)用程序的狀態(tài)。 但是,在這種情況下,能夠訪問(wèn)這個(gè) Web 頁(yè)面的任何人都可以執(zhí)行任意腳本語(yǔ)句,可以訪問(wèn)任意共享 Java 對(duì)象。編程時(shí)的失誤、錯(cuò)誤的配置和安全漏洞會(huì)把機(jī)密信息泄露給未授權(quán)用戶,或者讓應(yīng)用程序遭遇拒絕服務(wù)攻擊(例如,攻擊者可以執(zhí)行與 System.exit 或 /bin/rm -fr / 等效的腳本語(yǔ)句)。與任何強(qiáng)大的工具一樣,Java 腳本編程 API 要求您保持謹(jǐn)慎,注意安全。
進(jìn)一步開(kāi)拓的方向 本文主要關(guān)注讓 Java 應(yīng)用程序能夠在運(yùn)行時(shí)動(dòng)態(tài)地讀取并執(zhí)行外部腳本,以及讓腳本能夠訪問(wèn)顯式提供給它們的 Java 對(duì)象。Java 腳本編程 API 還提供了其他特性。例如: - 可以使用腳本語(yǔ)言實(shí)現(xiàn)一個(gè) Java 接口,然后像使用任何其他 Java 接口引用一樣從 Java 代碼調(diào)用腳本代碼。
- 可以在腳本中實(shí)例化并使用 Java 對(duì)象,還可以讓 Java 應(yīng)用程序能夠訪問(wèn)這些對(duì)象。
- 可以在裝載動(dòng)態(tài)腳本時(shí)進(jìn)行預(yù)編譯,這可以讓以后的執(zhí)行過(guò)程更快。
- 可以設(shè)置腳本使用的輸入流和輸出流,這樣就很容易將文件用作腳本的控制臺(tái)輸入源,以及將腳本的控制臺(tái)輸出轉(zhuǎn)發(fā)到 文件或其他流。
- 可以設(shè)置位置參數(shù),腳本可以將這些參數(shù)用作命令行參數(shù)。
Java 腳本編程 API 定義了腳本引擎可以選擇實(shí)現(xiàn)的一些功能,所以并非所有腳本引擎都提供這些功能。在 參 考資料 中可以找到關(guān)于這些特性和其他特性的讀物和在線參考資料。
下載 描述 | 名字 | 大小 | 下載方法 | 源代碼和所有 JAR 文件 | java-scripting-part2.zip | 4.5MB | HTTP |
參考資料 學(xué)習(xí) 獲得產(chǎn)品和技術(shù) - Groovy: 下載最新的 Groovy 版本。
- Java SE 6 和 BEA JRockit:這些開(kāi)發(fā)工具包和運(yùn)行時(shí)環(huán)境支持 Java 腳本編程 API,還包含 Mozilla Rhino JavaScript 引擎的一個(gè)簡(jiǎn)化版本。
- Scripting 項(xiàng)目:java.net 上的開(kāi)放源碼 Scripting 項(xiàng)目為 20 多種語(yǔ)言提供了腳本引擎接口,并鏈接到其他著名的 Java 腳本引擎。要想使用這些腳本語(yǔ)言之一,只需安裝這個(gè)項(xiàng)目提供的腳本引擎實(shí)現(xiàn) JAR 文件以及腳本語(yǔ)言解釋器 JAR 文件。
- Scripting for the Java Platform 1.0 Reference Implementation:JSR-223 參考實(shí)現(xiàn)提供三個(gè) JAR 文件,支持在 Java SE 5 中運(yùn)行 Java 腳本編程 API。下載并解壓 sjp-1_0-fr-ri.zip 文件,將 js.jar、script-api.jar 和 script-js.jar 文件放在自己的類路徑中。
討 論
關(guān)于作者 | | | Tom McQueeney 是 Idea Integration(美國(guó)的一家咨詢公司)的一名 Java 開(kāi)發(fā)人員和應(yīng)用程序架構(gòu)師。他熱衷于將一些動(dòng)態(tài)語(yǔ)言(如 Ruby 和 Groovy)集成到 Java 項(xiàng)目中,從而提高開(kāi)發(fā)的速度、效率和樂(lè)趣。他曾經(jīng)是 O'Reilly OSCON 和 ApacheCon EuropeHe 的發(fā)言人,并且擔(dān)任過(guò) Denver Java Users Group 的總裁一職。他和他的妻子(也是一名認(rèn)證的 Java 架構(gòu)師)居住在華盛頓。 |
|