2021年12月25日 星期六

4.3. 使用provides、uses、opens

宣告指令uses用於指示該模組相依於一個「服務(service)」,通常是介面(interface),如:

module service.consumer {
    uses some.serviceApi;
}

宣告指令provides用於指示該模組提供一個服務的實作(implementation),如:

module service.provider {
    provides some.serviceApi with some.serviceApiImpl;
}

最後一個宣告指令opens則和Java的映射(reflection)技術有關。

當使用多型時,Java的程式呼叫端在編譯(compile)時期可以知道物件參考的型別,但只有在執行(runtime)時期才能知道實際的實作。使用映射技術時,程式呼叫端在編譯時期甚至不需要知道物件參考的型別,但在執行時期就可以執行指定的物件方法。這並非初學者會接觸的範圍,但以現今大家對資訊安全的重視,這明顯具備一定程度的危險性!

有鑑於此,模組系統要求程式的呼叫端和被呼叫端都可以明確允許映射技術的使用!

以範例專案lab.reflection.provider為例,作為映射技術的被呼叫端,具備套件lab.reflection.provider.api和類別HelloWorld

package lab.reflection.provider.api;
public class HelloWorld {
    public String getGreeting() {
        return "hi, greeting from lab.reflection.provider.api";
    }
}

及模組資訊檔:

module lab.reflection.provider {
    exports lab.reflection.provider.api;
}

接下來建立專案lab.reflection.consumer作為映射技術的呼叫端。這個範例我們不使用javac的指令進行編譯,因此要先比照先前內容設定2Eclipse專案在模組部分的相依關係。

接下來先建立專案的模組資訊檔,宣告專案依賴模組lab.reflection.provider

module lab.reflection.consumer {
    requires lab.reflection.provider;
}
接下來建立套件lab.reflection.consumer.user與類別AccessByNormal。因為Eclipse已經完成專案Modulepath的設定,而且2個專案的模組資訊檔也有相應的exports和requires宣告,因此可以在行2直接import另一個專案的模組的套件與類別。
另外本類別示範類別的一般存取方式:
  1. imports使用類別,如行2。
  2. 建立物件與物件參考,如行6。
  3. 呼叫物件參考的方法,如行7。
下一個範例將使用映射技術,可以比對兩者的差異:
package lab.reflection.consumer.user;
import lab.reflection.provider.api.HelloWorld;
public class AccessByNormal {
    public static void main(String args[]) {
        try {
            HelloWorld om = new HelloWorld();
            System.out.println(om.getGreeting());
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }
}

接下來建立映射技術的呼叫者類別AccessByReflection,行2匯入的套件java.lang.reflect和類別Method用於映射技術:

package lab.reflection.consumer.user;
import java.lang.reflect.Method;
public class AccessByReflection {
  public static void main(String args[]) {
    try {
      Class c = Class.forName("lab.reflection.provider.api.HelloWorld");
      Method m = c.getMethod("getGreeting");
      System.out.println(m.invoke(c.getDeclaredConstructor().newInstance()));
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}

整個範例中未曾建立類別HelloWorld的物件並呼叫方法getGreeting(),唯一相關的就是將類別名稱和方法名稱以「字串」表示,因此只要更換字串內容就可以呼叫不同類別的方法,這也是映射技術神奇和危險的地方!

請注意,不管是類別AccessByNormalAccessByReflection,在類別HelloWorld 所在的模組使用「exports」都是可以通過編譯且執行結果相同;這表示依賴關係的提供端模組使用exports關鍵字時,使用端模組不管在編譯(compile)和執行(runtime)時期都可以存取。

接下來把提供端模組的模組資訊檔由exports宣告改為「opens」:

module lab.reflection.consumer {
    opens lab.reflection.provider;
}
此時可以發現使用端模組的類別AccessByNormal因為無法存取lab.reflection.provider.api.HelloWorld而編譯失敗,但AccessByReflection依然可以正常執行:
這差異顯示了使用宣告指令opens只開放執行時期使用,exports則開放編譯和執行時期使用。這讓Java程式設計師對於釋出的模組函式庫的存取控制有更大的運用。

沒有留言:

張貼留言