当前位置: 首页 > 科技观察

说说服务模块化

时间:2023-03-22 15:55:31 科技观察

服务模块化实践Jdk9于2017年9月正式发布,带来了很多新特性,其中之一就是模块化。JDK模块化的前身是项目Jigsaw,2008年发布,开始孵化,最初计划在jdk7中使用,部分内容推迟到jdk8。其实项目的所有目标都在jdk9完成了,就是实现一个模块系统,实现jdk本身的模块化。本文主要讲解模块化的概念,为什么要关注模块化,基于jdk9的模块化原理和项目实践。1、什么是模块化?模块化是一个广泛的概念。它用于软件编程,将系统分解成独立且相互关联的模块。拆分后的模块通常需要预先定义标准化的接口,以便每个模块可以独立开发。在某些情况下,它们仍然可以相互呼叫而不受影响。事实上,面向对象语言中对象之间的关注点分离与模块化的概念基本一致。在实际应用开发中,将复杂的业务系统按照业务逻辑划分为多个独立的模块,每个模块都预先定义好。服务接口,各模块独立开发,可根据依赖模块独立完成业务模块测试和交付。Java语言并不是按照模块化的思想设计的(除了package,在Java语言各版本和虚拟机规范的第7章package中,程序被组织成一组packages。package的成员是classes,interfacesandsub-packages,为编译单元声明而打包)但是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,文件中定义了模块的名称和依赖模块,open类,接口实现类等。其实module-info就是一个模块化的声明文件。除了组织形式的变化,真正的区别在哪里?图3是jdk.internal.loader.BuiltinClassLoader的loadClassOrNull方法中的代码片段,是类加载的方法。代码展示首先搜索LoadedModule(模块信息),如果有则加载该类,否则按照双亲委托方式向上委托进行类加载,后一步是为了向前兼容,前一步是核心原理模块化实施。类加载机制不再向上委托,而是根据LoadedModule限制类加载。它的初始化在java.lang.System#initPhase2中,如图3.1所示。主要是为虚拟机初始化系统模块化,返回ModuleLayer,称为层(layer,代表一组类加载器)。有两层,虚拟机提供的引导层和用户定义层,用于将基础模块和用户定义模块与类加载器(层)相关联。图3图3.1模块的定义在Module#defineModules中,详细的解释可以参考java9se虚拟机规范5.3.6。Java虚拟机支持将类和接口组织成模块,调用defineModules,连接模块和层(类加载器)关联,设置模块可以访问、开放和依赖的资源(从而限制模块的访问),访问控制由类的运行时模块管理,而不是由创建类的类加载器或类加载器服务的层管理,至此,模块化初始化和对核心功能的限制访问已经实现。您也可以按照下面的代码来了解模块化的组织和实现。BuiltinClassLoader的实现类有AppClassLoader、BootClassLoader、PlatformClassLoader、jdk9三个类加载器。//初始化layerModuleLayerboot=ModuleLayer.boot();Configurationconfiguration=boot.configuration();//获取解析模块Setmodules=configuration.modules();modules.forEach(resolvedModule->{//获取模块handleModuleReferencereference=resolvedModule.reference();//模块化名称System.out.println(reference.descriptor().name());try(ModuleReaderreader=reference.open()){//模块化Reader.list().forEach(System.out::println);}catch(IOExceptionioe){thrownewUncheckedIOException(ioe);}});jdk9之前的类加载机制就是大家熟悉的parentdelegation三层模型,bootstrapclassloader<--extensionclassloader<--applicationclassloader,这里不再赘述。下面展示了jdk9带来的变化。三层模型保持不变。为了向前兼容,JEP220.extension类加载器更改为平台类加载器。应用类加载器不是URLClassLoader的实现,而是在其内部存储了LoadedModule,优先根据模块信息自己进行Classloading,否则委托给父类,平台类加载器也可以委托给应用程序类加载器。实际的加载机制如下图4所示。模块化的类加载机制打破了双亲委托,更加高效。以上就是模块化实现的核心原理。模块控制模块下的类和接口的可访问性。模块化类加载不再是双亲委派。运行时模块根据模块之间的关系与层(一组类加载器)相关联。)关联,如下图进行类加载。图44.模块化实践下面的实践是基于jdk9模块化项目编译运行全过程目录4.1和使用完成多模块项目4.2。4.1模块化项目从helloproject开始运行并生成运行环境的过程,深刻理解模块化按需打包的优势。重点展示模块化项目从建立到可运行环境输出的过程,项目名称为hello,项目目录如图5所示:在图5的src目录下新建module-info.java,并模块名称是你好。在hello目录下,新建一个Main.java并添加代码,实际上打印了一个helloworld。编译、运行并镜像以下输出。publicstaticvoidmain(String[]args){System.out.println("helloworld");}4.1.1编译编译java文件,out是一个目录,编译生成文件到out的目录:javac-d出来。\src\hello\Main.java.\src\module-info.java4.1.2将out目录(*)下的所有文件打包成一个hello.jar文件,存放在jar目录下,指定入口应用程序要为hello.Main创建一个新的存档,-c指定存档文件名,-e指定应用程序入口点。cd.\out\mkdirjarjar-cfehello.jarhello.Main*4.1.3运行生成的jar,--module-path指定模块路径,jar为hello.jar文件存放目录,consoleoutputshelloworldjava--module-path.\jar\--modulehello/hello.Main或java--module-path.\jar\--modulehello4.1.4生成模块指定生成模块的jar为hello。jar,以及生成的模块hello.jmodjmodcreate--class-pathhello.jarhello.jmod4.1.5生成运行环境将hello.jmod放在jdk安装目录下的jmods目录下(module-path在windows下指定多个路径分隔符)是半角分号【;】,linux分隔符是半角冒号[:]我的环境是windows,试了很多次都不成功,所以把这个模块粘贴到JDK的基本模块中,指定module-路径为当前目录)并在该目录下执行以下命令指定模块路径为当前目录,--add-modules添加java.base和hello模块,--launcher定义一个入口点直接运行模块--output指定生成的运行环境的目录名。jlink--模块路径。--add-modulesjava.base,hello--launcherhello=hello--outputjre/4.1.6运行并打开jre目录,如图6,bin目录生成Runhello和hello.bat,运行。\hello.batWindows下命令行,控制台打印,helloworld图64.1.7总结上面工程生成的文件是一个完整的可执行的Java运行环境,即JavaRuntimeEnvironment,即jre,这个文件的大小可运行环境只有35.9MB,完整的jre为215M(我的环境),这是模块化的一大优势,可以按需打包依赖,jdk层的支持,应用依赖也可以跟随这样的按需包装减少了资源浪费。以上就是模块化从编译到生成jre的过程。接下来,我们将开发一个完整的模块化项目。4.2多模块项目实践如何将一个完整的项目模块化?如何使用模块之间的依赖关系?如何对外开放服务?如何对外允许反射服务和隐式依赖转移,下面的项目深入演示了模块化项目使用的基本要点。通过对每个关键字的详细解释突出显示模块化的使用。假设场景是日常生活,新建一个项目,搭建四个模块。饮食、交通、工作和控制台项目如下。eat模块模拟吃喝,transportation模块模拟交通,work模块模拟工作,console模块模拟生活。工程目录如图7所示。图74.2.1eat模块eatapi目录下,对外提供服务接口,有吃和喝两种方法。公共接口EatApi{voideat();voiddrink();}eatservice目录实现EatApi接口,publicclassEatApiImpl实现EatApi{@Overridepublicvoideat(){System.out.println("Drink");}@Overridepublicvoiddrink(){System.out.println("喝水");}}模块化的module-info类,定义名称为eat,exports对外暴露了eatapi接口,接口的实现是EatApiImpl类,提供了可以通过ServiceLoader根据SPI方法加载,但是反射不能获取实现类。模块吃{出口eatapi;在transportapi目录下提供eatapi.EatApi和eatservice.EatApiImpl;}4.2.2.transportation模块,对外提供服务,模拟交通,publicinterfaceTransportation{voidtransport();}transportservice目录,实现transportapi接口publicclassTransportationImplimplementsTransportation{@Overridepublicvoidtransport(){System.out.println("驱出");}}模块化的module-info类,定义名称为transportation,exports对外暴露了transportapi接口,接口的实现为TransportationImpl类,可以在module关键字前加上opens关键字,表示整个模块都可以被深度体现,开启transportservice只是表示这个包下的类可以被深度体现。模块运输{出口transportapi;提供transportapi.Transportation和transportservice.TransportationImpl;openstransportservice;}4.2.3.workmodule模块workapi目录下,对外提供服务,模拟工作,publicinterfaceWork{voidwork()throwsException;}workservice目录下,实现接口,获取eat模块EatApi通过ServiceLoader,通过反射获取Transportation实现类。publicclassWorkImplimplementsWork{@Overridepublicvoidwork()throwsException{System.out.println("开始工作");//获取服务EatApieatApi=ServiceLoader.load(EatApi.class).findFirst().get();//喝eatApi.drink();//反射获取Transportation实现类Transportationtransportation=getTransportation();//出去旅游transportation.transport();//吃东西eatApi.eat();//喝水eatApi.drink();}privateTransportationgetTransportation()throwsException{ClasstransportationClass=(Class)Class.forName("transportservice.TransportationImpl");运输transportation=transportationClass.getDeclaredConstructor().newInstance();返程运输;}}模块化模块,可以对外暴露workapi,实现类为WorkImpl,requires表示依赖模块,依赖模块eat和transportation调用这两个模块的服务,transitive关键字表示会传递依赖,以及引用此服务的服务也将传递性修饰的模块不需要在主服务中被引用一次,使用方式在模块中使用具体的服务modulework{exportsworkapi;提供workapi.Work和workservice.WorkImpl;需要传递吃;需要过境运输;useseatapi.EatApi;}4.2.4.console模块该模块调用工作模块和工作传递模块。模块化配置如下,依赖work模块,使用workapi.Work和eatapi.EatApimoduleconsole{requireswork;使用workapi.Work;useseatapi.EatApi;}在day1目录下新建一个Main,隐式调用Work模块的依赖,最后打印出如图8所示的结果。publicclassMain{publicstaticvoidmain(String[]args)throwsException{//获取工作serviceServiceLoaderload=ServiceLoader.load(Work.class);工作工作=load.findFirst().get();//调用work.work();//其他服务ServiceLoadereatLoader=ServiceLoader.load(EatApi.class);EatApieatApi=eatLoader.findFirst().get();eatApi.eat();eatApi.drink();//反射获取Transportationtransportation=getTransportation();运输.运输();}privatestaticTransportationgetTransportation()throwsException{ClasstransportationClass=(Class)Class.forName("transportservice.TransportationImpl");运输transportation=transportationClass.getDeclaredConstructor().newInstance();返程运输;}}图85.总结以上就是在项目中使用jre环境进行模块化生成和使用多模块服务的实践。模块化的核心原则。模块必须有很强的封装性,隐藏部分代码,只对外提供指定的服务,这就需要良好的接口定义和显示依赖。没有使用声明式服务依赖但不知道依赖来自哪里。可以提高模块的可读性,明确服务入口和依赖,减少服务周期依赖,按需打包,解决反射带来的全可见危害,提高安全性。但就目前而言,模块化带来的收益远低于迁移工作。目前大家都在用spring全家桶应用项目,用起来很方便,但是真的是按照模块化来划分的,项目完全可以清空。依赖也有一定的门槛,但是jdk已经提供了模块化的方法和工具。模块化的思维和思路值得学习。相信在不久的将来,模块化会更加智能和完善。6.附录[1]项目hellohttps://gitee.com/lifutian66/java9/tree/master/hello[2]项目java9https://gitee.com/lifutian66/java9/tree/master/java9[3]生成hello.jmodhttps://gitee.com/lifutian66/java9/hello.jmod[4]生成jrehttps://gitee.com/lifutian66/java9/tree/master/jre[5]jdk9地址:https:///www.oracle.com/java/technologies/javase/javase9-archive-downloads.html[6]模块化Java:它是什么?https://www.infoq.com/articles/modular-java-what-is-it/[7]参考文档:Java9模块化开发核心原理与实践2022年加入汽车之家,目前就职于数字品牌私享家后端技术团队,主要负责后端相关开发品牌私享家的商务科技。