Java世界一直在遭受著異種語言的入侵,比如PHP,Ruby,Groovy、Javascript等,這些入侵者都有一個共同特征:全是同一類語言-----腳本語言,它們都是在運行期解釋執行的。為什麼Java這種強編譯型語言會需要這些腳本語言呢?那是因為腳本語言的三大特征,如下所示:
腳本語言的這些特性是Java缺少的,引入腳本語言可以使Java更強大,於是Java6開始正式支持腳本語言。但是因為腳本語言比較多,Java的開發者也很難確定該支持哪種語言,於是JSCP(Java Community ProCess)很聰明的提出了JSR233規范,只要符合該規范的語言都可以在Java平台上運行(它對JavaScript是默認支持的)。
簡單看看下面這個小例子:
function formual(var1, var2){ return var1 + var2 * factor; }
這就是一個簡單的腳本語言函數,可能你會很疑惑:factor(因子)這個變量是從那兒來的?它是從上下文來的,類似於一個運行的環境變量。該js保存在C:/model.js中,下一步需要調用JavaScript公式,代碼如下:
1 import java.io.FileNotFoundException; 2 import java.io.FileReader; 3 import java.util.Scanner; 4 5 import javax.script.Bindings; 6 import javax.script.Invocable; 7 import javax.script.ScriptContext; 8 import javax.script.ScriptEngine; 9 import javax.script.ScriptEngineManager; 10 import javax.script.ScriptException; 11 12 public class Client16 { 13 public static void main(String[] args) throws FileNotFoundException, 14 ScriptException, NoSuchMethodException { 15 // 獲得一個JavaScript執行引擎 16 ScriptEngine engine = new ScriptEngineManager().getEngineByName("javascript"); 17 // 建立上下文變量 18 Bindings bind = engine.createBindings(); 19 bind.put("factor", 1); 20 // 綁定上下文,作用於是當前引擎范圍 21 engine.setBindings(bind, ScriptContext.ENGINE_SCOPE); 22 Scanner input =new Scanner(System.in); 23 24 while(input.hasNextInt()){ 25 int first = input.nextInt(); 26 int second = input.nextInt(); 27 System.out.println("輸入參數是:"+first+","+second); 28 // 執行Js代碼 29 engine.eval(new FileReader("C:/model.js")); 30 // 是否可調用方法 31 if (engine instanceof Invocable) { 32 Invocable in = (Invocable) engine; 33 // 執行Js中的函數 34 Double result = (Double) in.invokeFunction("formula", first, second); 35 System.out.println("運算結果是:" + result.intValue()); 36 } 37 } 38 39 } 40 }
上段代碼使用Scanner類接受鍵盤輸入的兩個數字,然後調用JavaScript腳本的formula函數計算其結果,注意,除非輸入了一個非int數字,否則當前JVM會一直運行,這也是模擬生成系統的在線變更情況。運行結果如下:
輸入參數是;1,2 運算結果是:3
此時,保持JVM的運行狀態,我們修改一下formula函數,代碼如下:
function formual(var1, var2){ return var1 + var2 - factor; }
其中,乘號變成了減號,計算公式發生了重大改變。回到JVM中繼續輸入,運行結果如下:
輸入參數:1,2 運行結果是:2
修改Js代碼,JVM沒有重啟,輸入參數也沒有任何改變,僅僅改變腳本函數即可產生不同的效果。這就是腳本語言對系統設計最有利的地方:可以隨時發布而不用部署;這也是我們javaer最喜愛它的地方----即使進行變更,也能提供不間斷的業務服務。
Java6不僅僅提供了代碼級的腳本內置,還提供了jrunscript命令工具,它可以再批處理中發揮最大效能,而且不需要通過JVM解釋腳本語言,可以直接通過該工具運行腳本。想想看。這是多麼大的誘惑力呀!而且這個工具是可以跨操作系統的,腳本移植就更容易了。
動態編譯一直是java的夢想,從Java6開始支持動態編譯了,可以再運行期直接編譯.java文件,執行.class,並且獲得相關的輸入輸出,甚至還能監聽相關的事件。不過,我們最期望的還是定一段代碼,直接編譯,然後運行,也就是空中編譯執行(on-the-fly),看如下代碼:
1 import java.io.IOException; 2 import java.lang.reflect.Method; 3 import java.net.URI; 4 import java.util.ArrayList; 5 import java.util.Arrays; 6 import java.util.List; 7 8 import javax.tools.JavaCompiler; 9 import javax.tools.JavaFileObject; 10 import javax.tools.SimpleJavaFileObject; 11 import javax.tools.StandardJavaFileManager; 12 import javax.tools.ToolProvider; 13 14 public class Client17 { 15 public static void main(String[] args) throws Exception { 16 // Java源代碼 17 String sourceStr = "public class Hello { public String sayHello (String name) {return \"Hello,\"+name+\"!\";}}"; 18 // 類名及文件名 19 String clsName = "Hello"; 20 // 方法名 21 String methodName = "sayHello"; 22 // 當前編譯器 23 JavaCompiler cmp = ToolProvider.getSystemJavaCompiler(); 24 // Java標准文件管理器 25 StandardJavaFileManager fm = cmp.getStandardFileManager(null, null, 26 null); 27 // Java文件對象 28 JavaFileObject jfo = new StringJavaObject(clsName, sourceStr); 29 // 編譯參數,類似於javac <options>中的options 30 List<String> optionsList = new ArrayList<String>(); 31 // 編譯文件的存放地方,注意:此處是為Eclipse工具特設的 32 optionsList.addAll(Arrays.asList("-d", "./bin")); 33 // 要編譯的單元 34 List<JavaFileObject> jfos = Arrays.asList(jfo); 35 // 設置編譯環境 36 JavaCompiler.CompilationTask task = cmp.getTask(null, fm, null, 37 optionsList, null, jfos); 38 // 編譯成功 39 if (task.call()) { 40 // 生成對象 41 Object obj = Class.forName(clsName).newInstance(); 42 Class<? extends Object> cls = obj.getClass(); 43 // 調用sayHello方法 44 Method m = cls.getMethod(methodName, String.class); 45 String str = (String) m.invoke(obj, "Dynamic Compilation"); 46 System.out.println(str); 47 } 48 49 } 50 } 51 52 class StringJavaObject extends SimpleJavaFileObject { 53 // 源代碼 54 private String content = ""; 55 56 // 遵循Java規范的類名及文件 57 public StringJavaObject(String _javaFileName, String _content) { 58 super(_createStringJavaObjectUri(_javaFileName), Kind.SOURCE); 59 content = _content; 60 } 61 62 // 產生一個URL資源路徑 63 private static URI _createStringJavaObjectUri(String name) { 64 // 注意,此處沒有設置包名 65 return URI.create("String:///" + name + Kind.SOURCE.extension); 66 } 67 68 // 文本文件代碼 69 @Override 70 public CharSequence getCharContent(boolean ignoreEncodingErrors) 71 throws IOException { 72 return content; 73 } 74 }
上面代碼較多,可以作為一個動態編譯的模板程序。只要是在本地靜態編譯能夠實現的任務,比如編譯參數,輸入輸出,錯誤監控等,動態編譯都能實現。
Java的動態編譯對源提供了多個渠道。比如,可以是字符串,文本文件,字節碼文件,還有存放在數據庫中的明文代碼或者字節碼。匯總一句話,只要符合Java規范的就可以在運行期動態加載,其實現方式就是實現JavaFileObject接口,重寫getCharContent、openInputStream、openOutputStream,或者實現JDK已經提供的兩個SimpleJavaFileObject、ForwardingJavaFileObject,具體代碼可以參考上個例子。
動態編譯雖然是很好的工具,讓我們可以更加自如的控制編譯過程,但是在我們目前所接觸的項目中還是使用較少。原因很簡單,靜態編譯已經能夠幫我們處理大部分的工作,甚至是全部的工作,即使真的需要動態編譯,也有很好的替代方案,比如Jruby、Groovy等無縫的腳本語言。另外,我們在使用動態編譯時,需要注意以下幾點:
instanceof是一個簡單的二元操作符,它是用來判斷一個對象是否是一個類的實現,其操作類似於>=、==,非常簡單,我們看段程序,代碼如下:
1 import java.util.Date; 2 3 public class Client18 { 4 public static void main(String[] args) { 5 // String對象是否是Object的實例 true 6 boolean b1 = "String" instanceof Object; 7 // String對象是否是String的實例 true 8 boolean b2 = new String() instanceof String; 9 // Object對象是否是String的實例 false 10 boolean b3 = new Object() instanceof String; 11 // 拆箱類型是否是裝箱類型的實例 編譯不通過 12 boolean b4 = 'A' instanceof Character; 13 // 空對象是否是String的實例 false 14 boolean b5 = null instanceof String; 15 // 轉換後的空對象是否是String的實例 false 16 boolean b6 = (String) null instanceof String; 17 // Date是否是String的實例 編譯不通過 18 boolean b7 = new Date() instanceof String; 19 // 在泛型類型中判斷String對象是否是Date的實例 false 20 boolean b8 = new GenericClass<String>().isDateInstance(""); 21 22 } 23 } 24 25 class GenericClass<T> { 26 // 判斷是否是Date類型 27 public boolean isDateInstance(T t) { 28 return t instanceof Date; 29 } 30 31 }
就這麼一段程序,instanceof的應用場景基本都出現了,同時問題也產生了:這段程序中哪些語句編譯不通過,我們一個一個的解釋說:
在防御式編程中經常會用斷言(Assertion)對參數和環境做出判斷,避免程序因不當的判斷或輸入錯誤而產生邏輯異常,斷言在很多語言中都存在,C、C++、Python都有不同的斷言表現形式.在Java中斷言使用的是assert關鍵字,其基本用法如下:
assert<布爾表達式>
assert<布爾表達式> : <錯誤信息>
在布爾表達式為假時,跑出AssertionError錯誤,並附帶了錯誤信息。assert的語法比較簡單,有以下兩個特性:
(1)、assert默認是不啟用的
我們知道斷言是為調試程序服務的,目的是為了能夠迅速、方便地檢查到程序異常,但Java在默認條件下是不啟用的,要啟用就要在編譯、運行時加上相關的關鍵字,這就不多說,有需要的話可以參考一下Java規范。
(2)、assert跑出的異常AssertionError是繼承自Error的
斷言失敗後,JVM會拋出一個AssertionError的錯誤,它繼承自Error,注意,這是一個錯誤,不可恢復,也就是表明這是一個嚴重問題,開發者必須予以關注並解決之。
assert雖然是做斷言的,但不能將其等價於if...else...這樣的條件判斷,它在以下兩種情況下不可使用:
(1)、在對外的公開方法中
我們知道防御式編程最核心的一點就是:所有的外部因素(輸入參數、環境變量、上下文)都是"邪惡"的,都存在著企圖摧毀程序的罪惡本源,為了抵制它,我們要在程序處處檢驗。滿地設卡,不滿足條件,就不執行後續程序,以保護後續程序的正確性,處處設卡沒問題,但就是不能用斷言做輸入校驗,特別是公開方法。我們開看一個例子:
1 public class Client19 { 2 public static void main(String[] args) { 3 System.out.println(StringUtils.encode(null));; 4 } 5 } 6 7 class StringUtils{ 8 public static String encode(String str){ 9 assert str != null : "加密的字符串為null"; 10 /*加密處理*/ 11 return str; 12 13 } 14 }
encode方法對輸入參數做了不為空的假設,如果為空,則拋出AssertionError錯誤,但這段程序存在一個嚴重的問題,encode是一個public方法,這標志著它時對外公開的,任何一個類只要能傳遞一個String類型的參數(遵守契約)就可以調用,但是Client19類按照規定和契約調用encode方法,卻獲得了一個AssertionError錯誤信息,是誰破壞了契約協議?---是encode方法自己。
(2)、在執行邏輯代碼的情況下
assert的支持是可選的,在開發時可以讓他運行,但在生產環境中系統則不需要其運行了(以便提高性能),因此在assert的布爾表達式中不能執行邏輯代碼,否則會因為環境的不同而產生不同的邏輯,例如:
public void doSomething(List list, Object element) { assert list.remove(element) : "刪除元素" + element + "失敗"; /*業務處理*/ }
這段代碼在assert啟用的環境下沒有任何問題,但是一但投入到生成環境,就不會啟用斷言了,而這個方法就徹底完蛋了,list的刪除動作永遠不會執行,所以就永遠不會報錯或異常了,因為根本就沒有執行嘛!
以上兩種情況下不能使用斷言assert,那在什麼情況下能夠使用assert呢?一句話:按照正常的執行邏輯不可能到達的代碼區域可以防止assert。具體分為三種情況:
public void doSomething() { int i = 7; while (i > 7) { /* 業務處理 */ } assert false : "到達這裡就表示錯誤"; }
3.建立程序探針:我們可能會在一段程序中定義兩個變量,分別代兩個不同的業務含義,但是兩者有固定的關系,例如:var1=var2 * 2,那我們就可以在程序中到處設"樁"了,斷言這兩者的關系,如果不滿足即表明程序已經出現了異常,業務也就沒有必要運行下去了。
我們經常在系統中定義一個常量接口(或常量類),以囊括系統中所涉及的常量,從而簡化代碼,方便開發,在很多的開源項目中已經采用了類似的方法,比如在struts2中,org.apache.struts2.StrutsConstants就是一個常量類,它定義Struts框架中與配置有關的常量,而org.apache.struts2.StrutsConstants則是一個常量接口,其中定義了OGNL訪問的關鍵字。
關於常量接口(類)我們開看一個例子,首先定義一個常量類:
public class Constant { //定義人類壽命極限 public static final int MAX_AGE=150; }
這是一個非常簡單的常量類,定義了人類的最大年齡,我們引用這個常量,代碼如下:
public class Client{ public static void main(String[] args) { System.out.println("人類的壽命極限是:"+Constant.MAX_AGE); } }
運行結果easy,故省略。目前的代碼是寫在"智能型"IDE工具中完成的,下面暫時回溯到原始時代,也就是回歸到用記事本編寫代碼的年代,然後看看會發生什麼事情(為什麼要如此,下面會給出答案)
修改常量Constant類,人類的壽命極限增加了,最大活到180,代碼如下:
public class Constant { //定義人類壽命極限 public static final int MAX_AGE=180; }
然後重新編譯,javac Constant,編譯完成後執行:java Client,大家猜猜輸出的年齡是多少?
輸出的結果是:"人類的壽命極限是150",竟然沒有改成180,太奇怪了,這是為何?
原因是:對於final修飾的基本類型和String類型,編譯器會認為它是穩定態的(Immutable Status)所以在編譯時就直接把值編譯到字節碼中了,避免了在運行期引用(Run-time Reference),以提高代碼的執行效率。對於我們的例子來說,Client類在編譯時字節碼中就寫上了"150",這個常量,而不是一個地址引用,因此無論你後續怎麼修改常量類,只要不重新編譯Client類,輸出還是照舊。
對於final修飾的類(即非基本類型),編譯器會認為它不是穩定態的(Mutable Status),編譯時建立的則是引用關系(該類型也叫作Soft Final)。如果Client類引入的常量是一個類或實例,及時不重新編譯也會輸出最新值。
千萬不可小看了這點知識,細坑也能絆倒大象,比如在一個web項目中,開發人員修改了一個final類型的值(基本類型)考慮到重新發布的風險較大,或者是審批流程過於繁瑣,反正是為了偷懶,於是直接采用替換class類文件的方式發布,替換完畢後應用服務器自動重啟,然後簡單測試一下,一切Ok,可運行幾天後發現業務數據對不上,有的類(引用關系的類)使用了舊值,有的類(繼承關系的類)使用的是新值,而且毫無頭緒,讓人一籌莫展,其實問題的根源就在於此。
還有個小問題沒有說明,我們的例子為什麼不在IDE工具(比如Eclipse)中運行呢?那是因為在IDE中設置了自動編譯不能重現此問題,若修改了Constant類,IDE工具會自動編譯所有的引用類,"智能"化屏蔽了該問題,但潛在的風險其實仍然存在,我記得Eclipse應該有個設置自動編譯的入口,有興趣大家可以自己嘗試一下。
注意:發布應用系統時禁止使用類文件替換方式,整體WAR包發布才是萬全之策。但我覺得應特殊情況特殊對待,並不可以偏概全,大家以為呢?