Java9模塊化學習筆記二之模塊設計模式

模塊設計的原則:

一、防止出現編譯時循環依賴(主要是編譯器不支持),但運行時是容許循環依賴的,好比GUI應用
二、明確模塊的邊界java

幾種模塊設計:
API模塊,聚合模塊(好比java.base)sql

可選依賴

兩種方式:
一、可選的編譯時依賴(相似於maven的provided scope)聲明: requires static , requires transitive static
二、使用services模式,缺點就是須要使用侵入性的ServiceLoader APIjson

使用編譯時可選依賴

module framework {
  requires static fastjsonlib;
}
public static void main(String... args) {
    try {
      Class<?> clazz = Class.forName("javamodularity.fastjsonlib.FastJson");
      FastJson instance =
        (FastJson) clazz.getConstructor().newInstance();
      System.out.println("Using FastJson");
    } catch (ReflectiveOperationException e) {
      System.out.println("Oops, we need a fallback!");
    }
  }

注意,經過requires static聲明後,運行時,即便fastjsonlib模塊在模塊路徑中,仍然會跑到異常塊中,由於requies static聲明的模塊不會出如今模塊解析路徑上。除非你經過jlink打包時,加入--add-modules fastjsonlib選項來顯式將其添加到模塊解析路徑(經過--add-modules也是做爲一個root module).api

使用Services模式的可選依賴

請參考以前的對於Services的探討tomcat

Versioned Modules

jar命令打包時能夠經過 --module-version=<V>選項支持將版本添加到module-info.class中做爲一個屬性。可是對於模塊解析而言,版本是沒有意義的,模塊解析過程當中,只看模塊名,不支持版本。
因此若是須要版本化,仍是得藉助於Maven,Gradle之類的打包工具。架構

資源封裝

分模塊內資源訪問、模塊間資源訪問app

模塊內資源訪問

firstresourcemodule/
├── javamodularity
│   └── firstresourcemodule
│   ├── ResourcesInModule.java
│   ├── ResourcesOtherModule.java
│   └── resource_in_package.txt 包內資源
├── module-info.java
└── top_level_resource.txt 與module-info.java平級的資源框架

訪問方式有幾種,見下面代碼:jvm

public class ResourcesInModule {

   public static void main(String... args) throws Exception {
      Class clazz = ResourcesInModule.class;
      InputStream cz_pkg = clazz.getResourceAsStream("resource_in_package.txt"); //<1> 
      URL cz_tl = clazz.getResource("/top_level_resource.txt"); //<2>

      Module m = clazz.getModule(); //<3>
      InputStream m_pkg = m.getResourceAsStream(
        "javamodularity/firstresourcemodule/resource_in_package.txt"); //<4>
      InputStream m_tl = m.getResourceAsStream("top_level_resource.txt"); //<5>

      assert Stream.of(cz_pkg, cz_tl, m_pkg, m_tl)
                   .noneMatch(Objects::isNull);
   }

}

在模塊化中,不推薦使用ClassLoder::getResource*
注意上面代碼中用到了Module APImaven

跨模塊資源訪問

.
├── firstresourcemodule
│   ├── javamodularity
│   │   └── firstresourcemodule
│   │   ├── ResourcesInModule.java
│   │   ├── ResourcesOtherModule.java
│   │   └── resource_in_package.txt
│   ├── module-info.java
│   └── top_level_resource.txt
└── secondresourcemodule

├── META-INF
│   └── resource_in_metainf.txt
├── foo
│   └── foo.txt
├── javamodularity
│   └── secondresourcemodule
│       ├── A.java
│       └── resource_in_package2.txt
├── module-info.java
└── top_level_resource2.txt

注意,下面代碼的前提是兩個模塊的包都沒暴露給對方

public class ResourcesOtherModule {

   public static void main(String... args) throws Exception {
      Optional<Module> otherModule = ModuleLayer.boot().findModule("secondresourcemodule"); //<1>

      otherModule.ifPresent(other -> {
         try {
            InputStream m_tl = other.getResourceAsStream("top_level_resource2.txt"); //<2>
            InputStream m_pkg = other.getResourceAsStream(
                "javamodularity/secondresourcemodule/resource_in_package2.txt"); //<3>
            InputStream m_class = other.getResourceAsStream(
                "javamodularity/secondresourcemodule/A.class"); //<4>
            InputStream m_meta = other.getResourceAsStream("META-INF/resource_in_metainf.txt"); //<5>
            InputStream cz_pkg =
              Class.forName("javamodularity.secondresourcemodule.A")
                   .getResourceAsStream("resource_in_package2.txt"); //<6>

            assert Stream.of(m_tl, m_class, m_meta)
                         .noneMatch(Objects::isNull);
            assert Stream.of(m_pkg, cz_pkg)
                         .allMatch(Objects::isNull);

         } catch (Exception e) {
            throw new RuntimeException(e);
         }
      });
   }

}

請注意<1>中的ModuleLayer.boot() API
<2>說明了模塊中的top-level資源老是能夠被其餘模塊訪問的
<3>將獲得null,由於模塊2的包沒有開放給模塊1,模塊包中的資源訪問遵循模塊的封裝原則
<4>將返回結果,上面提到資源訪問遵循模塊封裝原則,但對於.class文件除外。(想一想也是,由於是容許運行時獲取到別的模塊封裝的Class對象,只是不容許反射調用相關方法)
<5>因爲META-INF不是一個包,因此其不會遵循模塊封裝原則,換言之,也像top-level資源同樣,是能夠被其餘模塊訪問的。
<6>Class.forName會正常調用,不過接着調用的.getResourceAsStream會返回null,就像<3>說明的同樣。

記住一個原則:資源封裝只針對包下的(除.class外,包下的.class文件也能夠被其餘模塊訪問),其他的不會有封裝。

那麼問題來了,若是我真的很想公開包下的資源給其餘模塊呢?
使用open module或者opens 包名,好比:

open module aaa{
    ...
}

module aaa{

    opens a.b.c
}

ResourceBundle

咱們知道jdk有個i18n資源加載API: ResourceBundle。它的行爲是掃描classpath中的全部資源,只要符合baseName和Local便可加載到。
可是java9模塊化當中,沒法掃描classpath,只有模塊中可使用ResourceBundle::getBundle
有兩種解決方案:
一、定義一個專門的i18n資源模塊,並open module
二、使用java9提供的ResourceBundleProvider接口,實現它,並將這個實現註冊爲服務。

Deep Reflection 與 三方框架

深度反射與淺反射的區別:淺反射只是獲取基本的類信息,好比字段名,方法上的註解等,而深度反射會進行字段賦值,方法調用等。
模塊化強封裝帶來的問題就是,咱們無法使用深度反射,好比對一個exports包中的某個公開類的private域進行反射調用,field.setAccessible(true)之類的就會出現異常;對非exports包中的類進行任何深度反射都是非法的。
那麼咱們熟悉的ORM框架,IOC框架等都普遍地使用了深度反射。這就會致使問題。如何解決?使用Services確定是不行的,由於框架自己改動成本就會很大,沒幾個願意這麼改。
有兩種方式: 一、使用open module或opens 包名, opens 包名 to 模塊名;二、使用Module::addOpens運行時動態open。
java9還爲反射類添加了canAccess方法、trySetAccessible方法

clipboard.png

使用open module或opens 包名

open容許對open的模塊或包進行深度反射

clipboard.png

還有個問題,假如咱們想對三方提供的模塊進行深度反射,那該怎麼辦呢,總不能去拿到別人的代碼改module-info.java聲明吧。這個時候就要用到java命令行參數 --add-opens <module>/<pkg>=<targetmodule>. 好比我想深度反射java.base中的java.lang包,那麼能夠 --add-opens java.base/java.lang=mymodule,可是若是我不使用模塊化,而只是使用classpath-based,那麼咱們可使用--add-opens java.base/java.lang=ALL_UNNAMED,指定想未命名ALL_UNNAMED的代碼開放。

反射的替代方案:

java9基於JEP193提供了反射的替代方案用於訪問非public元素MethodHandles (始於java7),VarHandles(始於java9)
示例:
src

├── application
│   ├── javamodularity
│   │   └── application
│   │       ├── Book.java
│   │       └── Main.java
│   └── module-info.java
└── ormframework
    ├── javamodularity
    │   └── ormframework
    │       └── OrmFramework.java
    └── module-info.java

Book是一個POJO,裏面有個private title字段
OrmFramework是一個模擬orm行爲的demo,內容以下:

ublic class OrmFramework {

  private Lookup lookup;

  public OrmFramework(Lookup lookup) { this.lookup = lookup; }

  public <T> T loadfromDatabase(String query, Class<T> clazz) {
     try {
       MethodHandle ctor = lookup.findConstructor(clazz, MethodType.methodType(void.class));
       T entity =  (T) ctor.invoke();

       Lookup privateLookup = MethodHandles.privateLookupIn​(clazz, lookup);
       VarHandle title = privateLookup.findVarHandle(clazz, "title", String.class); // Name/type presumably found in some orm mapping config
       title.set(entity, "Loaded from database!");
       return entity;
     } catch(Throwable e) {
       throw new RuntimeException(e);
     }

  }

Main類內容以下:

public static void main(String... args) {
    Lookup lookup = MethodHandles.lookup();
    OrmFramework ormFramework = new OrmFramework(lookup);
    Book book = ormFramework.loadfromDatabase("/* query */", Book.class);
    System.out.println(book.getTitle());
  }

你可能要問,爲何OrmFramework須要傳入Lookup,由於只有application模塊的Lookup纔能有權限訪問那個模塊的非public元素,而OrmFramework模塊本身生成的Lookup是沒有權限訪問的。
因此使用MethodHandles與VarHandles時須要注意Lookup的權限

利用module相關api進行反射

java.lang.module提供了三種類型的能力:一、查詢模塊屬性(主要基於module-info.java的內容);二、運行時動態修改模塊的行爲;三、模塊內資源訪問
類圖:
clipboard.png

一、查詢模塊屬性(主要基於module-info.java的內容)

public class Introspection {

  public static void main(String... args) {
    Module module = String.class.getModule();

    String name1 = module.getName(); // Name as defined in module-info.java
    System.out.println("Module name: " + name1);

    Set<String> packages1 = module.getPackages(); // Lists all packages in the module
    System.out.println("Packages in module: " + packages1);

    // The methods above are convenience methods that return
    // information from the Module's ModuleDescriptor:
    ModuleDescriptor descriptor = module.getDescriptor();
    String name2 = descriptor.name(); // Same as module.getName();
    System.out.println("Module name from descriptor: " + name2);

    Set<String> packages2 = descriptor.packages(); // Same as module.getPackages();
    System.out.println("Packages from descriptor: " + packages2);

    // Through ModuleDescriptor, all information from module-info.java is exposed:
    Set<Exports> exports = descriptor.exports(); // All exports, possibly qualified
    System.out.println("Exports: " + exports);

    Set<String> uses = descriptor.uses(); // All services used by this module
    System.out.println("Uses: " + uses);
  }

}

二、運行時動態修改模塊的行爲
好比動態exports

Module target=...
Module current=getClass().getModule();
current.addExports("com.test.in.Hello",target);

看了這段代碼,你可能要問,第二行,假如我是在別的模塊中調用,那麼是否是任何模塊均可以修改其餘模塊的exports,opens等屬性呢,非也,JVM運行時會判斷Module對象的調用上下文,若是檢測到調用時非當前模塊,那麼就會出現異常。這種行爲叫作Caller Sensitive

Caller Sensitive
jdk定義了不少caller sensitive的方法,只要是caller sensitive的方法都會被註解@CallerSensitive標註,好比剛剛提到的Module::addExports,Field::setAccessible

Module API中可修改運行時行爲的幾個方法:
addExports(String pkgName, Module other)
addOpens(String pkgName, Module other)
addReads(Module other)

模塊上也能夠加註解

@Deprecated
module m{
}

你也能夠自定義模塊註解
注意:@Target(value={PACKAGE, MODULE})

@Retention(RetentionPolicy.RUNTIME)
@Target(value={PACKAGE, MODULE})
public @interface CustomAnnotation {

}

容器應用模式

Layers And Configurations

ModuleLayer API、boot layer、layer的父子關係、一個layer能夠有多個父layer
一個layer包含了當前root模塊的解析圖(module resolution graph),一個應用中能夠有多個layer,可是隻有一個boot layer,啓動時的boot layer是java給你自動建立的,你也能夠手動建立layer,那麼這個建立的layer的parent就是boot layer。 只有boot layer才能解析platform module,但children layer能夠共享boot layer中的Platform module,可是若是boot layer中沒有加載到的platform module,children module是沒法使用的。

clipboard.png

public static void main(String... args) {
    Driver driver = null; // We reference java.sql.Driver to see 'java.sql' gets resolved
    ModuleLayer.boot().modules().forEach(m -> System.out.println(m.getName() + ", loader: " + m.getClassLoader()));
    System.out.println("System classloader: " + ClassLoader.getSystemClassLoader());
  }

建立ModuleLayer的示例:

ModuleFinder finder=ModuleFinder.of(Paths.get("../modules"));
ModuleLayer  bootLayer=ModuleLayer.boot();
//第二個Finder參數是在第一個finder中找不到模塊時纔會去第二個finder中找,還有個resolveAndBind方法,區別在於,後者還會解析services provides/uses
Configuration config=bootLayer.configuration().resolve(finder,ModuleFinder.of(), Set.of("rootmodule")); 
ClassLoader cl=ClassLoader.getSystemClassLoader();
ModuleLayer newLayer=bootLayer.defineModulesWithOneLoader(config,cl);

上面的Configuration除了resolve方法外,還有個resolveAndBind方法,區別在於,後者還會解析services provides/uses

clipboard.png

ClassLoaders in Layer

clipboard.png
引入模塊化之後,去掉了以前的ExtClassLoader,引入了PlatformClassLoader

若是咱們爲每一個layer都傳入不一樣的ClassLoader,那麼則容許不一樣layer中存在相同的全限定類,這樣能夠作到隔離與相互不干擾。

Plug-in 架構

好比Eclipse,IDEA都是基於插件的應用
在Java9中,咱們有兩種方式來實現插件化:一、仍然利用之前的Services能力;二、結合ModuleLayer+Services實現封裝性更強的插件

public class PluginHostMain {

  public static void main(String... args) {
    if (args.length < 1) {
      System.out.println("Please provide plugin directories");
      return;
    }

    System.out.println("Loading plugins from " + Arrays.toString(args));

    Stream<ModuleLayer> pluginLayers = Stream
      .of(args)
      .map(dir -> createPluginLayer(dir)); //<1>

    pluginLayers
      .flatMap(layer -> toStream(ServiceLoader.load(layer, Plugin.class))) // <2>
      .forEach(plugin -> {
         System.out.println("Invoking " + plugin.getName());
         plugin.doWork(); // <3>
      });
  }

  static ModuleLayer createPluginLayer(String dir) {
    ModuleFinder finder = ModuleFinder.of(Paths.get(dir));

    Set<ModuleReference> pluginModuleRefs = finder.findAll();
    Set<String> pluginRoots = pluginModuleRefs.stream()
             .map(ref -> ref.descriptor().name())
             .filter(name -> name.startsWith("plugin")) // <1>
             .collect(Collectors.toSet());

    ModuleLayer parent = ModuleLayer.boot();
    Configuration cf = parent.configuration()
      .resolve(finder, ModuleFinder.of(), pluginRoots); // <2>

    ClassLoader scl = ClassLoader.getSystemClassLoader();
    ModuleLayer layer = parent.defineModulesWithOneLoader(cf, scl); // <3>

    return layer;
  }

  static <T> Stream<T> toStream(Iterable<T> iterable) {
    return StreamSupport.stream(iterable.spliterator(), false);
  }

}

clipboard.png

Container架構

好比tomcat,Jetty就是基於Container的應用,支持運行時動態depoy和undeploy應用。

clipboard.png

與Plugin-in架構的區別:一、Container支持運行時deploy和undeploy;二、Plugin-in是用的是Services思路,而Container模式不該該使用Services。這種狀況下,就須要使用模塊的open功能,可是咱們又不該該強制應用open,那麼這就須要用到ModuleLayer.Controller::addOpens了,與Module::addOpens是Caller Sensitive不一樣,它能夠實現跨模塊調用來修改模塊屬性。而後利用Deep reflection來實例化應用類

private static void deployApp(int appNo) {
    AppDescriptor appDescr = apps[appNo];//AppDescriptor是自定義的類
    System.out.println("Deploying " + appDescr);

    ModuleLayer.Controller appLayerCtrl = createAppLayer(appDescr);
    Module appModule = appLayerCtrl.layer()
      .findModule(appDescr.rootmodule)
      .orElseThrow(() -> new IllegalStateException(appDescr.rootmodule + " missing"));

    appLayerCtrl.addOpens(appModule, appDescr.appClassPkg,
      Launcher.class.getModule());

    ContainerApplication app = instantiateApp(appModule, appDescr.appClass);
    deployedApps[appNo] = app;
    app.startApp();
  }

private static ModuleLayer.Controller createAppLayer(AppDescriptor appDescr) {
    ModuleFinder finder = ModuleFinder.of(Paths.get(appDescr.appDir));
    ModuleLayer parent = ModuleLayer.boot();

    Configuration cf = parent.configuration()
       .resolve(finder, ModuleFinder.of(), Set.of(appDescr.rootmodule));

    ClassLoader scl = ClassLoader.getSystemClassLoader();
    ModuleLayer.Controller layerCtrl =
      ModuleLayer.defineModulesWithOneLoader(cf, List.of(parent), scl);

    return layerCtrl;
  }

private static ContainerApplication instantiateApp(Module appModule, String appClassName) {
    try {
      ClassLoader cl = appModule.getClassLoader();
      Class<?> appClass = cl.loadClass(appClassName);

      if(ContainerApplication.class.isAssignableFrom(appClass)) {
        return ((Class<ContainerApplication>) appClass).getConstructor().newInstance();
      } else {
        System.out.println("WARNING: " + appClassName + " doesn't implement ContainerApplication, cannot be started");
      }
    } catch (ReflectiveOperationException roe) {
      System.out.println("Could not start " + appClassName);
      roe.printStackTrace();
    }

注意點:只有jvm啓動時的boot layer才能解析platform module,在這裏就是Container的root layer,但children layer能夠共享boot layer中的Platform module,可是若是boot layer中沒有加載到的platform module,children module是沒法使用的。因此Container啓動時能夠指定參數--add-modules ALL-SYSTEM這樣即可以解析全部的platform module到layer module graph中

總之:不論是Plugin-in仍是Container模式,咱們都須要適應新的ModuleLayer API就像之前的ClassLoader API同樣

相關文章
相關標籤/搜索