(全文阅读时间大概20分钟。)从JDK9开始,module成为与class,interface,package等同等重要的一等公民,是(需要成为)Javaers日常高频处理的词汇。Java的modularity起源于2008年的Jigsaw项目,并从2014年开始在JDK9的开发过程中设计并实现。引入module不像引入像lambada表达式仅是语法的改变,它涉及到了JLS(java language specification),JVM,JDK,JAR, JNI, JVM TI(tool interface)和JDWP(javadebug wire protocol,java调试通信协议)等多个模块的改动。本文结合一个样例从JDK引入module机制的原因,引入module涉及的改动点,module目标,module与jar的关系,module与reflection的关系,automatic module和unnamed module等方面介绍modularity相关机制,希望给想在实际项目中使用java module的程序员提供参考。
一. Java为什么引入modularity机制? 1) classpath问题。Java的classloader采用parent委托模式,即application的classloader在加载一个class时首先委托给parent加载器,parent加载器再向上委托其parent加载器,依此类推,直到查找到已加载的类或者未查找到,此时再由classloader加载。但对一些通用框架,这种机制并不适合,如JNDI,JDBC,JAXB等其接口定义在bootstrap classloader中,而其SPI是由其他厂商实现,定义在system classloader(bootstrap classloader的子loader,即application classloader)中,parent无法加载child的类,这就需要打破parent delegation model;另外web容器为了隔离不同应用的类加载器,也定制classloader。上述各种场景使得类加载,类依赖关系比较复杂,例如:一个Web应用依赖的hibernate和spring都依赖了不同的log4j,很容易导致如下所列的一些运行时异常: 2) 安全性问题。jdk中包括很多internal类,它们虽然设置为public,但oracle官方一直警告开发人员不要使用;不过,警告信息对开发人员来讲等同于说可以使用,而且这些api又能很方便的满足开发需求,从而导致被广泛地“滥用”。例如:GC,Unsafe,BASE64Encoder等。 3)性能问题。jdk从1.0到8.0,为了保证兼容性,基本都是增加api,从而导致整个JDK有几百M,非常的臃肿,很多api实际上很少被用到,导致程序启动时较慢。在cloud native成为主流的今天,非常需要一个能够弹性伸缩的jdk image,同时也能够满足IoT场景下嵌入式设备的使用需求。
二. modularity涉及到的改动 上文已提到,JDK的modularity机制是一次由底到上的全局性的大升级,它涉及到JVM,JDK,tools等多层次的改动,尤其是JDK,全部的package按照module重新划分,移除了java.xml.ws,java.corba,java.transaction 等包,如下给出JVM,JTS,JVM TI的主要改动点。 1) JVM改动点: 3.16 Modules:介绍引入module后,module的描述文件module-info.java对包和类的访问控制(即module的strong encapsulation特性)产生的影响。 4.2.3 Module and Package Names:module命名规范。 4.4.11 The CONSTANT_Module_infoStructure:module的常量池结构。 4.7.25 The Module Attribute:类的module属性,表明一个module所依赖的其他modules,export和open的packages,使用和提供的services。 4.7.26 The ModulePackages Attribute:表明一个module export和open的packages。 4.7.27 The ModuleMainClass Attribute:module主类。 5.3.6 Modules and Layers:介绍module与layer,classloader之间的关系。
2) JLS改动点: 6.5.3 Meaning of Module Names and Package Names:module命名规范。 7.2 Host Support for Modules and Packages:应用系统决定如果创建和存储module和package。 7.7 Module Declarations:module创建语法规范。 13.3 Evolution of Packages and Modules:提供package和module访问控制机制。
3) JVM TI改动点: 增加“Bytecode Instrumentation of code inmodules”,agent通过AddModuleReads,AddModuleExports, AddModuleOpens, AddModuleUses 和AddModuleProvides等api修改module的运行时行为。
三. modularity目标 1) 明确的依赖配置(reliable configuration)。通过配置文件明确module之间的依赖关系,不允许有循环依赖,以此代替classpath记载类机制。 2) 封装增强(strong encapsulation)。增加module层的访问控制机制。引入module后,包和类之间的访问控制由module层的访问控制和原有的包和类的访问控制机制两级机制决定。 3) 可变的JDK文件大小。jdk按照模块重新划分为70个(jdk11.0.1)modules,开发者在发布应用时,根据所需定制自己的image,进而带来性能和安全的提升。
四. Jar与module的关系 Module的strong encapsulation由已有类型(package,class,method)的访问控制机制和moduel的可读性(readability)、可访问性(accessibility)共同决定的。例如:一个default的class所在的package即使exports,也不能被requires的module访问到。而如果没有export的package,即使是public的类型,也不能被其他module访问。见下表: 访问控制符 | 类自身访问 | 包内访问 | 子类访问 | 其他类(exported) | 其他类(unexported) | public | Y | Y | Y | Y | N | private(不能限制top-level类) | Y | N | N | N | N | 无控制符(default) | Y | Y | N | N | N | protected(不能限制top-level类) | Y | Y | Y | N | N |
表1:引入module后的访问控制机制 exports后只能跟package全限定名,不允许跟其他类型,如class,interface等;同时,export的package的subpackage如果没有export,也不能访问到。 一个module可以对应一个jar文件,也可以是JMOD文件(JDK的module都封装为该格式,参见jdk-11.0.1.jdk/Contents/Home/jmods)。jar和module的区别如下: module通过require关键字声明其依赖的其他modules,而jar不具备该功能。 module通过export关键字声明其对外暴露的package,而jar不具备该功能。 module必须包含文件module-info.java,而jar不需要。 module的名字必须与其定义文件module-info.java保持一致,其他module通过module名字加载该module。jar的文件名比较随意,没有语义上的约束。
总之,jar是把代码和资源文件打包后的文件格式,该格式被JVM用来加载类和配置文件;而module是package的容器,它有自己的名字,声明自己所依赖的其他module和对外暴露的包名,它可以是jar文件,也可以是jmod文件;一个module对应一个jar或jmod文件。 创建module工程: 图一 module源码结构 module描述文件需要放到与package外层平级的目录,上层目录可以是src目录,也可以是与module name相同的目录。 module的命名可以采用倒序的DNS,也可以采用短module名,只要保证工程下名称唯一即可。 $JDK11/jar -cfe mods/module1.jarorg.matoujun.module1.ModuleVerify -C out/module1 . 参见$JDK11/jar –help获取更多参数说明($JDK11表示JDK11的bin目录),展开的module的jar目录结构: 图二 module文件结构 $JDK11/java --module-path ./mods --module module1/org.matoujun.module1.ModuleVerify 因为main类已经指定,也可以这样运行module: $JDK11/java --module-path ./mods --module module1 modules具有传递性。例如:module1的module-info.java中,requires transitive java.logging; 使得依赖module1的其他modules自动依赖logging。这种特性又称为implied readability。 $JDK11/java --show-module-resolution--limit-modules java.logging --module-path ./mods --module module1 show-module-resolution:输出执行module1依赖的modules; limit-modules: 限制执行module1时依赖的modules,或使用--describe-module。 Graphviz是一个图形化工具,能够把特定的文本文件转为图形文件。mac xos下,brewinstall Graphviz $JDK11/jdeps -R --dot-output dots./mods/module1.jar 生成dot文件 dot -Tpng -O dots/summary.dot--dot文件转为png图形文件。如图三所示: 图三 module1的modules依赖 $JDK11/jlink --module-pathmods/:/usr/local/jdk-11.0.1.jdk/Contents/Home/jmods --add-modules module1--launcher module1Start=module1 --output module1-image 查看module1-image目录,总大小是37M: 图三:module1的image文件 图四:运行结果 五. module与reflection: 1) 如果一个package是exports的,我们可以在其他modules中通过反射创建对象,执行方法,但不能对非public的类型执行setAccessible(true)。 2) 如果一个package是open的,我们不可以在其他modules 直接声明对象,但可以在其他modules中通过反射创建对象,执行方法,也可以对非public的类型执行setAccessible(true)。open可以修辞module,也可以是package,并允许限定范围。例如:opens org.matoujun.module2.open tomodule1; 。通过open关键字,像spring这样的DI框架就可以很容易地注入实现类。通过uses和providers…with,ServiceLoader可以实现相同的功能。
六. automatic module与unnamed module 所有通过module-info.java定义的module都是named module,而在module path下的jar自动封装为automatic module,在classpath上的jar为unnamed module。举例如下: $JDK11/javac-d mods --module-source-path src $(find src -name "*.java")--module-path ../org.matoujun.module2/out/:./lib 执行module1,把jar包直接放在module-path下,jar包被直接加载为automatic modules,automatic module的name由jar的文件名决定,并去掉文件名中的版本号,”-”转为”.”: j11--module-path ./modss:../org.matoujun.module2/out/ -mmodule1/org.matoujun.module1.ModuleVerify 其中j11为$JDK11/java别名。参见图五。 图五 automatic module自动命名 通过classpath引用的jar包为unname dmodule,JDK允许unnamed module访问其他unnamed module和named module,但named module不允许访问unnamed module。因此如果运行一个module工程,如果直接调用classpath上jar中的类会抛出“java.lang.IllegalAccessError”错误。要解决这种问题,只能把classpath上的jar包转为automatic module或者named module。 总之,modularity是java一次里程碑的重大升级,JDK自身的module化已经证明其是一种稳定,可靠,灵活,安全的技术,因此在资源允许的情况下架构师和工程师们完全可以把它引入到具体的项目开发中。
|