资讯专栏INFORMATION COLUMN

Java9模块化学习笔记二之模块设计模式

李文鹏 / 755人阅读

摘要:但是模块化当中,无法扫描只有模块中可以使用有两种解决方案定义一个专门的资源模块,并使用提供的接口,实现它,并将这个实现注册为服务。有两种方式使用或包名,包名模块名使用运行时动态。

模块设计的原则:

1、防止出现编译时循环依赖(主要是编译器不支持),但运行时是允许循环依赖的,比如GUI应用
2、明确模块的边界

几种模块设计:
API模块,聚合模块(比如java.base)

可选依赖

两种方式:
1、可选的编译时依赖(类似于maven的provided scope)声明: requires static , requires transitive static
2、使用services模式,缺点就是需要使用侵入性的ServiceLoader API

使用编译时可选依赖
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).

使用Services模式的可选依赖

请参考之前的对于Services的探讨

Versioned Modules

jar命令打包时可以通过 --module-version=选项支持将版本添加到module-info.class中作为一个属性。但是对于模块解析而言,版本是没有意义的,模块解析过程中,只看模块名,不支持版本。
所以如果需要版本化,还是得借助于Maven,Gradle之类的打包工具。

资源封装

分模块内资源访问、模块间资源访问

模块内资源访问

firstresourcemodule/
├── javamodularity
│   └── firstresourcemodule
│   ├── ResourcesInModule.java
│   ├── ResourcesOtherModule.java
│   └── resource_in_package.txt 包内资源
├── module-info.java
└── top_level_resource.txt 与module-info.java平级的资源

访问方式有几种,见下面代码:

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 API

跨模块资源访问

.
├── 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 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
有两种解决方案:
1、定义一个专门的i18n资源模块,并open module
2、使用java9提供的ResourceBundleProvider接口,实现它,并将这个实现注册为服务。

Deep Reflection 与 三方框架

深度反射与浅反射的区别:浅反射只是获取基本的类信息,比如字段名,方法上的注解等,而深度反射会进行字段赋值,方法调用等。
模块化强封装带来的问题就是,我们没法使用深度反射,比如对一个exports包中的某个公开类的private域进行反射调用,field.setAccessible(true)之类的就会出现异常;对非exports包中的类进行任何深度反射都是非法的。
那么我们熟悉的ORM框架,IOC框架等都广泛地使用了深度反射。这就会导致问题。如何解决?使用Services肯定是不行的,因为框架本身改动成本就会很大,没几个愿意这么改。
有两种方式: 1、使用open module或opens 包名, opens 包名 to 模块名;2、使用Module::addOpens运行时动态open。
java9还为反射类添加了canAccess方法、trySetAccessible方法

使用open module或opens 包名

open允许对open的模块或包进行深度反射

还有个问题,假如我们想对三方提供的模块进行深度反射,那该怎么办呢,总不能去拿到别人的代码改module-info.java声明吧。这个时候就要用到java命令行参数 --add-opens /=. 比如我想深度反射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 loadfromDatabase(String query, Class 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提供了三种类型的能力:1、查询模块属性(主要基于module-info.java的内容);2、运行时动态修改模块的行为;3、模块内资源访问
类图:

1、查询模块属性(主要基于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 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 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 = descriptor.exports(); // All exports, possibly qualified
    System.out.println("Exports: " + exports);

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

}

2、运行时动态修改模块的行为
比如动态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是无法使用的。

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

ClassLoaders in Layer


引入模块化以后,去掉了之前的ExtClassLoader,引入了PlatformClassLoader

如果我们为每个layer都传入不同的ClassLoader,那么则允许不同layer中存在相同的全限定类,这样可以做到隔离与相互不干扰。

Plug-in 架构

比如Eclipse,IDEA都是基于插件的应用
在Java9中,我们有两种方式来实现插件化:1、仍然利用以前的Services能力;2、结合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 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 pluginModuleRefs = finder.findAll();
    Set 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  Stream toStream(Iterable iterable) {
    return StreamSupport.stream(iterable.spliterator(), false);
  }

}

Container架构

比如tomcat,Jetty就是基于Container的应用,支持运行时动态depoy和undeploy应用。

与Plugin-in架构的区别:1、Container支持运行时deploy和undeploy;2、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) 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一样

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/71464.html

相关文章

  • Java9模块学习笔记三之迁移到Java9

    摘要:命令行参数文件鉴于迁移到后可能需要很长的命令行参数,有些会限制命令行长度,支持定义一个命令行参数文件。已有三分库可以自动转成模块,只要在启动时将放在指定路径中,便会自动变成。 java[c]命令行参数文件 鉴于迁移到java9后可能需要很长的命令行参数,有些os会限制命令行长度,java9支持定义一个命令行参数文件。使用方式: java @arguments.txt arguments...

    NeverSayNever 评论0 收藏0
  • Java9模块学习笔记一之快速入门

    摘要:如果你想查看运行时模块的加载过程输出结果表示为模块,由于我限制了不再往下输出了,而我们模块又没有别的额外依赖,所以仅有这行输出。 jdk9模块快速入门 列出自带模块:java --list-modulesmac多版本jdk共存:http://adolphor.com/blog/2016...模块规则示意图:showImg(https://segmentfault.com/img/bVb...

    cjie 评论0 收藏0
  • 《Java应用架构设计:模块模式与OSGi》读书笔记

    摘要:本书概括以软件系统为例,重点讲解了应用架构中的物理设计问题,即如何将软件系统拆分为模块化系统。容器独立模块不依赖于具体容器,采用轻量级容器,如独立部署模块可独立部署可用性模式发布接口暴露外部配置使用独立的配置文件用于不同的上下文。 本文为读书笔记,对书中内容进行重点概括,并将书中的模块化结合微服务、Java9 Jigsaw谈谈理解。 本书概括 以Java软件系统为例,重点讲解了应用架构...

    seanHai 评论0 收藏0
  • Java9的新特性

    摘要:新特性概述系列一安装及使用系列二运行系列三模块系统精要系列四更新系列五系列六系列七系列八系列九与的区别迁移注意事项参数迁移相关选项解析使用构建实例使用示例带你提前了解中的新特性 Java语言特性系列 Java5的新特性 Java6的新特性 Java7的新特性 Java8的新特性 Java9的新特性 Java10的新特性 Java11的新特性 Java12的新特性 Java13的新特性...

    ddongjian0000 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<