第15次文章:反射+動態編譯+腳本引擎
- 2019 年 10 月 8 日
- 筆記
各位小夥伴兒大家好啊!又和各位見面了,風裡雨里,小白在這裡等你喲!
在前面的文章中,我們簡單的介紹過一點反射的內容,沒有深入,這次的反射內容會比上一次更加深刻一點!
1、動態語言
程式運行的時候,可以改變程式的結構或變數類型。典型的語言如:Python、ruby、JavaScript、c、c++,但是java不是動態語言,java可以稱之為「准動態語言」,java具有一定的動態性,我們可以利用反射機制、位元組碼操作獲得類似動態語言的特性。
JAVA反射機制是在運行狀態中,對於任意一個類,都能夠得到這個類的所有屬性和方法;對於任意一個對象,都能夠調用它的任意一個方法;這種動態獲取的資訊以及動態調用對象的方法的功能稱為java語言的反射機制.
2、基本概念
在java中,對象就是用來表示或者封裝一些數據。一個類被載入之後,JVM會創建一個對應類的Class對象,類的整個結構資訊會放到對應的Class對象中,這個Class對象就像一面鏡子,通過這面鏡子,我們可以看到這個類的全部資訊。包括其中的屬性,方法等等資訊。我們來測試一下反射機制的內容。
第一步:我們首先自己創建一個Javabean類,作為我們測試的對象
package com.peng.test.bean; public class User { private int age; private int id; private String uname; public int getAge() { return age; } public void setAge(int age) { this.age = age; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getUname() { return uname; } public void setUname(String uname) { this.uname = uname; } public User(int age, int id, String uname) { super(); this.age = age; this.id = id; this.uname = uname; } //Javabean必須要有無參構造器 public User() { // TODO Auto-generated constructor stub } }tips:在Javabean類中,一定要有無參構造器,在後續的反射機制中,一些創建對象的方法就是根據Javabean類的無參構造進行創建對象的。
第二步:利用Javabean類,創建對象,測試反射機制
public class Demo01 { public static void main(String[] args) { try { String path = "com.peng.test.bean.User";//需要使用的類的路徑 //Class對象的獲取方式1 Class clazz1 = Class.forName(path); System.out.println(clazz1); System.out.println(clazz1.hashCode()); Class clazz2 = Class.forName(path); System.out.println(clazz2.hashCode()); //Class對象的獲取方式2 Class clazz3 = String.class; //Class對象的獲取方式3 Class clazz4 = path.getClass(); System.out.println(clazz3 == clazz4); //不同數組對象的Class對象獲取 int[] arry01 = new int[10]; int[] arry02 = new int[30]; int[][] arry03 = new int[10][30]; double[] arry04 = new double[20]; System.out.println(arry01.getClass().hashCode()); System.out.println(arry02.getClass().hashCode()); System.out.println(arry03.getClass().hashCode()); System.out.println(arry04.getClass().hashCode()); } catch (ClassNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }程式碼運行後的結果為:

tips:
(1)Class對象的3種獲取方式,分別是Class.forName(),類.Class,對象.getClass(),在上面的程式碼中,分別對應程式碼「Class clazz1 = Class.forName(path);」、「Class clazz3 = String.class;」、「Class clazz4 = path.getClass();」。
(2)一個類的Class對象只會在JVM中載入一次,同一個類不論載入了多少次,也不論使用什麼方式載入,所得到的Class對象都是相同的。比如在測試中,我們對同一個類User載入了兩次,分別得到Class對象clazz1和clazz2對象,然後分別獲取其hashcode,結果顯示兩者的hashcode是完全相同的,這就代表了clazz1和clazz2屬於同一個對象。與此同時,clazz3是引用類String的Class對象,clazz4是字元串變數path的Class對象,但是最後列印結果也是clazz3=clazz4,這是因為變數path是String的一個實現類。以上結果均表明,在JVM中,載入得到的類的Class對象只會根據該類本身進行產生,而不會因為其載入方式和載入次數而改變。
(3)緊接著,我們又分別創建了四個數組,其長度和維度以及類型有所不同。我們通過最後的列印結果可以看出,arry01與arry02載入得到的Class對象是相同的,arry03和arry04的Class對象是不同的,也就代表著,數組對象的Class對象之間的區分,不在於其數組長度,而在於其維度以及數組的基本類型,一維數組和二維數組的反射對象不同,int和double數組的反射對象不同。
3、利用反射機制進行相應的操作
在上面的程式碼中,我們基本了解了反射的一些概念,下面我們使用反射API動態的操作構造器、方法、屬性。
public class Demo03 { @SuppressWarnings("all") public static void main(String[] args) { String path = "com.peng.test.bean.User"; try { Class clazz = Class.forName(path); //通過反射API調用構造方法,構造對象 User u = (User) clazz.newInstance(); System.out.println(u); Constructor<User> c = clazz.getDeclaredConstructor(int.class,int.class,String.class);//調用帶參構造 User u2 = c.newInstance(10,1001,"peng"); System.out.println(u2.getUname()); //通過反射API調用普通方法 User u3 = (User) clazz.newInstance(); Method method = clazz.getDeclaredMethod("setUname", String.class); method.invoke(u3, "peng03"); System.out.println(u3.getUname()); //通過反射API操作屬性 User u4 = (User) clazz.newInstance(); Field f = clazz.getDeclaredField("uname"); f.setAccessible(true); //不需要通過安全檢查,直接設置屬性值 f.set(u4, "peng04"); //通過反射來操作屬性值 System.out.println(u4.getUname()); //利用對象的方法來獲取屬性值 System.out.println(f.get(u4)); //利用反射來獲取屬性值 } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } }查看結果:

tips:
(1)利用反射機制的前提,依舊是需要先獲取一個User類的Class對象,在獲取對象u的時候,我們使用的方法是「clazz.newInstance();」其實這個方法的實現原理就是調用User對象的無參構造器,所以在我們創建Javabean的時候,一定要給其加上無參構造器。
(2)我們在創建對象的時候,也可以使用帶參構造器。此時我們就需要先獲取User類的帶參構造器,然後利用帶參構造器進行創建對象u2。
(3)利用反射的API調用方法的時候效率較為低下。在給u3設置屬性值「Uname」的時候,我們利用反射機制的實現為:「Method method = clazz.getDeclaredMethod("setUname", String.class);method.invoke(u3, "peng03");」,如果不使用反射,實現為:「u3.setUname(peng03);」。所以我們使用反射的時候要盡量慎重。
(4)在User類的定義中,我們會經常定義private的屬性或者方法,此時,被private修飾的屬性或者方法,都只能在該類的內部使用。當我們使用反射的時候,也是無法調用private修飾的方法或屬性的。所以我們通過反射中的setAccessible(true)的途徑,實現外部對私有內容的訪問與修改,其代表的含義為:不需要通過安全檢查,直接設置屬性值。
(5)使用反射機制和不使用反射機制進行運行時間的對比:通過安全檢查(即:setAccessible(false))的運行時間是不使用反射機制的12倍左右,不通過安全檢查(即:setAccessible(true))的效率大概是不使用該語句的4倍。所以慎重使用反射,如果不得不用反射,那麼儘可能將其設置為不通過安全檢查(即:setAccessible(true)),這樣可以在很大程度上提高運行效率。
二、動態編譯
1、為什麼我們需要使用動態編譯
java 6.0引入的動態編譯機制
靜態編譯:一次性編譯。在編譯的時候把你所有的模組都編譯進去。
動態編譯:按需編譯。程式在運行的時候,用到那個模組就編譯哪個模組。
現實中的實例:
比如開發一個閱讀器,支援txt,pdf,doc三種格式。我們把讀txt,讀pdf,讀doc定義為三個功能模組。
靜態編譯:我想看個txt,點擊應用程式圖標以後,三個功能都載入進來了。在這裡,另外兩個模組的作用就是佔用系統資源。
動態編譯:我想看個txt,點擊應用程式,判斷格式,只載入讀txt模組,使用讀txt模組。顯然,動態編譯速度快,節省了系統資源,利於今後拓展。
2、結合實際程式碼進行講解
public class Demo01 { public static void main(String[] args) throws IOException { /** * 動態編譯 */ JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); int result = compiler.run(null, null, null, "G:\java學習\test\HelloWorld.java"); System.out.println(result == 0?"編譯成功":"編譯失敗"); //通過Runtime調用執行類 Runtime run = Runtime.getRuntime(); Process process = run.exec("java -cp G:/java學習/test HelloWorld"); //輸出程式中的列印內容 InputStream in = process.getInputStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(in)); String info = ""; while((info = reader.readLine()) != null) { System.out.println(info); } } }tips:在動態編譯的時候,我們首先獲取java編譯器對象,然後對需要動態編譯的java類進行編譯操作,主要使用run方法。
int javax.tools.run(InputStream in,OutputStream out,OutputStream err,String... arguments) 方法參數介紹:
第一個參數:為java編譯器提供參數
第二個參數:得到java編譯器的輸出資訊
第三個參數:接收編譯器的錯誤資訊
第四個參數:可變參數(是一個String數組)能傳入一個或多個java源文件
返回值:0表示編譯成功,非0表示編譯失敗。
在動態編譯源文件之後,就可以執行源文件了。我們利用IO流輸出源文件「HelloWorld」中的內容。
三、腳本引擎執行JavaScript程式碼:
java腳本引擎是從jdk6.0之後添加的新功能。
腳本引擎介紹:
(1)使得java應用程式可以通過一套固定的介面與各種腳本引擎交互,從而達到在java平台上調用各種腳本語言的目的。
(2)java腳本API是連通java平台和腳本語言的橋樑。
(3)可以把一些複雜異變的業務邏輯交給腳本語言處理,這又大大提高了開發效率。
其實簡單的說,也就是通過java中的各個介面,使得JavaScript語句,可以在java的平台上得以實現。結合下面的實例進行解析:
public class Demo01 { public static void main(String[] args) throws ScriptException, NoSuchMethodException, IOException { //獲得腳本引擎對象 ScriptEngineManager sem = new ScriptEngineManager(); ScriptEngine engine = sem.getEngineByName("javascript"); //定義變數,存儲到引擎上下文中 engine.put("msg", "peng is a good man!"); String str = "var user = {name : 'peng' ,age:18,schools:['CTGU','UESTC']};"; str += "println(user.name);"; //執行腳本 engine.eval(str); engine.eval("msg = 'UESTC is a good school';");//msg的值,可以被動態改變 System.out.println(engine.get("msg")); System.out.println("#####################"); //定義函數 engine.eval("function add(a,b){var sum = a + b; return sum;}"); //取得調用介面 Invocable jsInvoke = (Invocable) engine; //執行腳本中定義的方法 Object result1 = jsInvoke.invokeFunction("add", new Object[] {13,20}); System.out.println(result1); //導入其他java包,使用其他包中的java類 String jscode = "importPackage(java.util);var list = Arrays.asList(["CTGU","UESTC"]);"; engine.eval(jscode);//執行js語句 //從JavaScript中取出list變數,並將其轉換為java中的list變數 List<String> list2 = (List<String>)engine.get("list"); for(String temp:list2) { System.out.println(temp); } //執行一個js文件(我們將a.js置於項目的src下即可) URL url = Demo01.class.getClassLoader().getResource("a.js"); FileReader fr = new FileReader(url.getPath()); engine.eval(fr); fr.close(); } }tips:上面的程式碼就是使用腳本語言執行javascript程式碼。
(1)首先就是獲取腳本引擎對象,我們利用引擎對象,選擇JavaScript語言。在腳本引擎中,我們可以按照JavaScript的語法,將JavaScript語句寫在字元串中,再使用腳本語言中的「eval」方法,執行字元串中的JavaScript語句。與此同時,我們也可以在eval方法中改變已經被定義的msg的值。
(2)在上面的程式碼中,我們主要是將js中的不同功能,使用其語法格式寫入字元串中,使用腳本引擎進行執行。同時我們還可以將js中的變數(比如List)轉換到java中進行使用。
在最後一段程式碼中,我們不再僅僅將js語言寫在java中的字元串中進行執行,而是在src文件下直接編寫一個a.js文件,文件內容如下所示,然後在java中直接執行此js文件。
//定義一個test方法 function test(){ var a = 3; var b = 4; println("invoke js file:"+(a+b)); } //執行test方法 test();