第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();

Tags: