我们一起聊聊服务模块化

开发 架构
模块化是个广泛的概念,用于软件编程就是将系统分解成独立且互相连接的模块的行为,拆分的模块通常需要提前定义好标准化的接口,以便让各模块独立开发情况下,还能互相调用不受影响。

服务模块化践行

2017年9月jdk 9正式发布,带来了很多新特性,其中之一便是模块化,JDK模块化的前身是项目 Jigsaw,于2008开始孵化,最早计划用于jdk7,一部分内容推迟到了jdk8,实际上在jdk9才完成了该项目全部目标,即实现一个模块系统,并以此实现jdk自身模块化。本文主要阐述模块化的概念,为什么关注模块化,基于jdk9的模块化实现原理和项目实践。

1.什么是模块化

模块化是个广泛的概念,用于软件编程就是将系统分解成独立且互相连接的模块的行为,拆分的模块通常需要提前定义好标准化的接口,以便让各模块独立开发情况下,还能互相调用不受影响。实际上在面向对象语言中对象之间的关注点分离与模块化的概念基本一致,在实际应用开发中,将复杂业务系统按照业务逻辑等分割成多个独立的模块,各模块提前定义好对外的服务接口,各模块独立开发,根据依赖的模块可独立完成业务模块测试、交付。Java语言并不是按照模块化思想设计的(除了package,在Java语言和虚拟机规范各版本第7章package,程序被组织为一组包。包的成员是类、接口以及子包,它们以包为编译单元声明)但是java社区早就有很多模块。一个jar,一个包,任何一个java类库,实际上都是一个模块,通常模块都附带一个版本号,以便模块升级提供新功能并不对低版本的模块产生影响。

2.为什么模块化

模块化有助于将应用分解为不同的模块,各个模块可以单独测试、开发、交付。类库基本上都是模块,如果你想将部分类库提供给别人使用或者使用了别人提供的类库,那么实际上你已经参与过模块化应用了。在实际项目中,一般使用构建工具(maven、gradle等)组建,明确指明了依赖的类库,以及变成类库,供他人使用。

模块化的好处之一是便于模块独立测试、开发、交付。模块可按照业务核心情况或依赖顺序部分交付,以便项目逐步完成交付,节省资源,增加迭代优化空间,这个概念提别像敏捷开发,采用迭代、循序渐进的方法进行软件开发,把一个大项目分为多个相互联系,但也可独立运行的小项目,并分别完成,在此过程中软件一直处于可使用状态。

模块化的另一个好处是便于升级,修复bug并提供新的服务,而版本号的存在就是为了区分模块的历史版本以及避免依赖发生错误。像guava、fastjson和fastjson2等类库证实了这点。

模块化也可给项目管理带来方便,复杂业务分割成一个个独立可复用的模块,项目结构性更好,出现问题或者需要部分优化,只需要关注部分模块,对于依赖的模块由其他人提供维护即可,减少了维护和关注的成本。

3.模块化的原理

首先需要安装jdk9,下载地址放在文末附录。

如下图1所示为安装好的jdk9,图2所示为jdk8的目录,是多个jar。

图片

图1

图片

图2

以上图1和图2对比可以看到jdk9拆分成了具体模块,不再是一个个的jar,每个模块都有一个module-info.class,文件定义模块的名字、依赖的模块、对外开放的类、接口实现类等,实际上module-info就是是模块化的声明文件。

除了组织形式发生变化外,真正的区别在哪里呢?图3是jdk.internal.loader.BuiltinClassLoader的loadClassOrNull方法中的代码片段,是进行类加载的方法,代码展示先查找LoadedModule (模块信息)如果有的话就进行类加载,否则的话,按照双亲委派模式向上委托进行类加载,后一步是为了向前兼容,前一步就是模块化实现的核心原理,类加载机制不再向上委托,而是根据LoadedModule限制类加载。

其初始化在java.lang.System# initPhase2如图3.1,主要是虚拟机进行系统模块化的初始化,并返回ModuleLayer,称为layer(层,表示一组类加载器),有两种层,虚拟机提供的boot layer和用户自定义的layer,用于将基础模块和用户定义模块与类加载器(层)关联。

图片

图3

图片

图3.1

模块的定义在Module#defineModules,详细的解释可在java9se虚拟机规范5.3.6找到,Java 虚拟机支持将类和接口组织成模块,调用defineModules,将模块与layer(类加载器)关联,设置模块可访问、开放的资源以及依赖的资源(由此限制模块的访问), 访问控制由类的运行时模块管理,不是由创建类的类加载器或类加载器服务的层管理,至此模块化的初始化和限制访问核心功能实现。也可按照以下代码理解模块化的组织和实现。BuiltinClassLoader的实现类有三个AppClassLoader,BootClassLoader,PlatformClassLoader,jdk9的类加载器。

//初始化 layer
ModuleLayer boot = ModuleLayer.boot();
Configuration configuration = boot.configuration();
//获取解析的模块
Set<ResolvedModule> modules = configuration.modules();
modules.forEach(resolvedModule -> {
//获取模块句柄
ModuleReference reference = resolvedModule.reference();
//模块化的名称
System.out.println(reference.descriptor().name());
try (ModuleReader reader = reference.open()) {
//模块化下的全部资源
reader.list().forEach(System.out::println);
} catch (IOException ioe) {
throw new UncheckedIOException(ioe);
}
});

jdk9以前的类加载机制是大家熟识的双亲委派三层模型,bootstrap classloader <-- extension classloader <-- application classloader,这里不在赘述。下面展示jdk9带来的改变,维持了三层模型,为了向前兼容,自JEP 220.extension classloader 变改为platform classloader,与application classloader 不在是URLClassLoader 的实现,而是其内部存有LoadedModule,并优先根据模块化信息自我进行类加载,否则委托给父类,而platform classloader还可以委托给application classloader ,实际的加载机制如下图4所示,模块化的类加载机制打破了双亲委派,效率更加高效。以上便是模块化实现的核心原理,Module控制模块下类和接口的访问性,模块化的类加载不再是双亲委派,运行时模块根据模块之间的关系,与layer(一组类加载器)关联,按照下图方式进行类加载。

图片

图4

4.模块化践行

下面实践基于jdk9模块化项目编译到运行全过程目录4.1以及完整多模块化的项目的使用4.2。

4.1模块化项目

由hello项目入手品略模块化项目的编译、打包、运行、生成运行时环境的过程,深入理解模块化的按需打包的优点。着重展示模块化项目从建立到可运行环境输出过程,项目名为hello,项目目录如下图5:

图片

图5

src目录下新建一个module-info.java,模块名是hello。在hello目录下,新建Main.java,添加代码代码,其实就是打印一个hello world。下面进行编译,运行,镜像输出。

public static void main(String[] args){
System.out.println("hello world");
}

 4.1.1编译

编译java文件,out是个目录,编译生成文件到out这个目录下:

javac -d out .\src\hello\Main.java  .\src\module-info.java

4.1.2打包

将out目录下全部文件也就是(*)打包成 hello.jar 文件,存放在jar目录下,并指定应用程序入口点为 hello.Main,-c创建新档案,-f指定档案文件名,-e指定应用程序入口点。

cd .\out\
mkdir jar
jar -cfe hello.jar hello.Main *

4.1.3运行

运行生成的jar ,--module-path指定模块路径, jar是存放hello.jar文件的目录,控制台输出 hello world

java --module-path .\jar\ --module hello/hello.Main    
或者
java --module-path .\jar\ --module hello

4.1.4生成模块

指定生成模块的jar是hello.jar,生成模块 hello.jmod

jmod create --class-path hello.jar hello.jmod

4.1.5生成运行环境

将hello.jmod 放到jdk安装目录下的jmods目录下(windows下module-path指定多个路径分隔符是半角分号【;】,Linux分隔符是半角冒号【:】我的环境是windows,尝试多次均为未成功,所以粘贴这个模块到JDK的基础模块中,指定module-path 为当前目录即可)并在此目录执行以下命令,指定模块路径为当前目录,--add-modules添加java.base和hello模块 ,--launcher定义一个入口点直接运行模块 --output 指定生成的运行时环境的目录名称。

jlink --module-path . --add-modules java.base,hello --launcher hello=hello --output jre/

4.1.6运行

打开jre目录,可以看到如图6所示,bin目录下生成可运行hello和 hello.bat,windows下命令行运行  .\hello.bat,控制台打印,hello world

图片

图6

4.1.7小结

以上项目生成的文件是一个完整的可运行的Java运行环境即Java Runtime Environment 即jre,而这个可运行的环境大小只有35.9 MB,完整的jre是215M(我的环境中),这也就是模块化的一大优点,可按需打包依赖,从jdk层支持,应用依赖也可以按照如此按需打包,减少浪费资源,以上是模块化从编译到生成jre的过程,下面我们进行模块化的完整项目开发。

4.2多模块项目实践

一个完整项目如何模块化?模块之间如何依赖使用?怎么对外开放服务?如何对外允许反射的服务和以及隐式的依赖传递,下面项目深入展示模块化的项目使用基本要点。着重展示了模块化的使用以各关键字的详细解释。

假设场景是每天的生活,新建一个项目,建四个模块,eat、transportation、work、console 项目如下,eat模块模拟吃喝,transportation模块模拟交通,work模块模拟工作,console 模块模拟生活,项目目录如图7所示。

图片

图7

4.2.1eat模块

eatapi目录下,对外提供服务接口,吃饭喝水两个方法,

public interface EatApi {
void eat();
void drink();
}

eatservice目录下,实现EatApi接口,

public class EatApiImpl implements EatApi {
@Override
public void eat(){
System.out.println("吃饭了");
}
@Override
public void drink(){
System.out.println("喝水了");
}
}

模块化 module-info 类,定义名称为eat,exports对外暴露eatapi接口,接口的实现为EatApiImpl类,provides  with 可被ServiceLoader根据SPI的方式加载到,但是反射并不能获取实现类。

module eat {
exports eatapi;
provides eatapi.EatApi with eatservice.EatApiImpl;
}

4.2.2.transportation模块

transportapi目录下,对外提供服务,模拟交通,

public interface Transportation {
void transport();
}

transportservice目录下,实现transportapi接口

public class TransportationImpl implements Transportation {
@Override
public void transport(){
System.out.println("开车出去");
}
}

模块化 module-info 类,定义名称为transportation,exports对外暴露transportapi接口,接口的实现为TransportationImpl类,opens关键字,可以加在module关键字之前,表明整个模块都可以被深度反射,opens transportservice 只表明该包下的类可以被深度反射。

module transportation {
exports transportapi;
provides transportapi.Transportation with transportservice.TransportationImpl;
opens transportservice;
}

4.2.3.work模块

workapi目录下,对外提供服务,模拟工作,

public interface Work {
void work() throws Exception;
}

workservice目录下,实现接口,通过ServiceLoader获取eat模块EatApi,通过反射获取 Transportation实现了类。

public class WorkImpl implements Work {
@Override
public void work() throws Exception {
System.out.println("开始工作了");
//获取服务
EatApi eatApi = ServiceLoader.load(EatApi.class).findFirst().get();
//喝口水
eatApi.drink();
//反射获取 Transportation实现了类
Transportation transportation = getTransportation();
//出去一趟
transportation.transport();
//吃点东西
eatApi.eat();
//喝口水
eatApi.drink();
}

private Transportation getTransportation() throws Exception{
Class<Transportation> transportationClass = (Class<Transportation>) Class.forName("transportservice.TransportationImpl");
Transportation transportation = transportationClass.getDeclaredConstructor().newInstance();
return transportation;
}
}

模块化module,workapi可对外暴露,实现类是WorkImpl,requires 表示依赖模块, 依赖模块eat、transportation,调用了这两个模块的服务,transitive 关键字表示该依赖会被传递,引用本服务的服务也会引用transitive修饰的模块,不用在主服务中在引一次,uses表示使用模块中的具体服务。

module work {
exports workapi;
provides workapi.Work with workservice.WorkImpl;
requires transitive eat;
requires transitive transportation;
uses eatapi.EatApi;
}

4.2.4.console模块

该模块调用work模块以及work transitive 的模块,

模块化配置如下,依赖模块work,使用workapi.Work和eatapi.EatApi

module console {
requires work;
uses workapi.Work;
uses eatapi.EatApi;
}

day1目录下新建Main,模块Work的依赖隐式传递,最终打印出结果如图8所示。

public class Main {
public static void main(String[] args) throws Exception {
//获取work 服务
ServiceLoader<Work> load = ServiceLoader.load(Work.class);
Work work = load.findFirst().get();
//调用
work.work();
//其他服务
ServiceLoader<EatApi> eatLoader = ServiceLoader.load(EatApi.class);
EatApi eatApi = eatLoader.findFirst().get();
eatApi.eat();
eatApi.drink();
//反射获取
Transportation transportation = getTransportation();
transportation.transport();

}
private static Transportation getTransportation() throws Exception {
Class<Transportation> transportationClass = (Class<Transportation>) Class.forName("transportservice.TransportationImpl");
Transportation transportation = transportationClass.getDeclaredConstructor().newInstance();
return transportation;
}
}

图片

图8

5.总结

以上便是使用模块化生成需要jre环境和在项目中使用多模块服务的践行。

模块化核心原则模块必须强封装性,隐藏部分代码,只对外提供指定服务,也就需要良好的接口定义并且显示依赖,声明式的服务依赖,不是使用了但不知道依赖来自哪里的糊涂账。可以提高模块的可读性,明确服务的入口和依赖,减少服务循环依赖,按需打包,解决反射带来的全可见危害,提高安全性。但是就目前而言模块化带来的收益远低于迁移工作,目前大家都在用spring的全家桶应用项目,使用很方便,但是真正按照模块化将其切分出来,并且能够完全理清楚项目依赖,也是有一定门槛的,不过模块化的方法和工具,jdk已然提供,模块化的思维和想法是很值得学习的,相信在不久的将来,模块化会更智能和完善。

6.附录 

[1]项目hello  https://gitee.com/lifutian66/java9/tree/master/hello

[2]项目java9 https://gitee.com/lifutian66/java9/tree/master/java9

[3]生成hello.jmod https://gitee.com/lifutian66/java9/hello.jmod

[4]生成jre    https://gitee.com/lifutian66/java9/tree/master/jre

[5]jdk9 地址:https://www.oracle.com/java/technologies/javase/javase9-archive-downloads.html

[6]Modular Java: What Is It?https://www.infoq.com/articles/modular-java-what-is-it/

[7]参考文档:java9模块化开发核心原则和实践

作者简介

图片

李福田,主机厂技术部-数科技术团队。

2022年加入汽车之家,目前任职于数科品牌私享家后端技术团队,主要负责品牌私享家后端相关业务技术开发。

责任编辑:武晓燕 来源: 之家技术
相关推荐

2022-10-28 07:27:17

Netty异步Future

2024-01-02 09:09:03

枚举规范化管理

2023-06-09 07:48:20

数字化转型工具

2023-08-10 08:28:46

网络编程通信

2023-08-04 08:20:56

DockerfileDocker工具

2022-05-24 08:21:16

数据安全API

2023-09-10 21:42:31

2023-06-30 08:18:51

敏捷开发模式

2021-08-27 07:06:10

IOJava抽象

2024-02-20 21:34:16

循环GolangGo

2023-07-24 09:41:08

自动驾驶技术交通

2023-03-26 23:47:32

Go内存模型

2021-08-12 07:49:24

mysql

2022-10-08 00:00:05

SQL机制结构

2022-02-23 08:41:58

NATIPv4IPv6

2022-09-22 08:06:29

计算机平板微信

2022-02-14 07:03:31

网站安全MFA

2022-11-12 12:33:38

CSS预处理器Sass

2023-12-28 09:55:08

队列数据结构存储

2022-01-04 12:08:46

设计接口
点赞
收藏

51CTO技术栈公众号