本文主要是以实战方式来介绍微服务下多团队多服务多功能模块下的项目工程结构设计,希望读者通过参考此文章的设计方案后可以自己设计一套满足自己企业的可扩展灵活性较高的项目工程层次结构。
读者在阅读此文之前应该具备哪些前提知识呢?笔者简要的列了一下如下内容:
读者在阅读此文之后会有哪些收获呢?笔者希望读者能有如下收获:
本文以项目demo来演示工程设计,这样会便于读者理解,这里假设定出如下项目场景:
爪哇留声机公司(以下简称爪哇公司)是一家专门为第三方企业做业务系统的,对于不同第三方企业的规模大小,爪哇公司在开发部署系统的时候可能会有所变化,其中爪哇公司的业务系统包含的如下三个功能:
背景一:第三方企业A是一家规模非常大的企业,用户也很庞大,而且也不差钱,那么爪哇公司在给A企业做产品时可能为了性能扩展等需要,会将现有产品化系统微服务化成三个服务(订单服务、支付服务、物流服务)分别部署来满足A企业需求,其中订单服务后续可能会因国内外订单量太大而进一步拆分成国内订单服务和国外订单服务。
背景二:第三方企业B是一家规模小的企业,用户量并不大,那么爪哇公司在给B企业做产品时只部署一个服务(此服务同时包含订单、支付、物流三项功能)即可满足B企业需求,大大减少企业运维等成本。
针对以上场景设计出一套 Gradle 工程结构在同样的代码库情况下来满足其需求,减少研发多次开发成本。
项目-服务-功能-模块,如:javalsj-order-foreign-api、javalsj-order-foreign-impl。
项目.服务.功能.模块.领域,如:javalsj.order.foreign.api、javalsj.order.foreign.api.vo、javalsj.order.foreign.api.dto等。
整个爪哇项目根目录。
爪哇前端根目录。
爪哇vue前端项目工程。
爪哇后端根目录。
后端服务公用模块目录。
后端服务无容器概念公用模块工程,如:工具常量类、响应实体等。
后端servlet容器公用模块工程,依赖javalsj-common-base,如:每个servlet服务需要的跨域过滤器配置、web异常拦截器等。
后端webflux容器公用模块工程,依赖javalsj-common-base,如:每个webflux服务(网关)需要的webflux异常拦截器等。
后端网关服务目录。
后端网关应用工程。
后端授权认证服务目录。
后端授权认证独立启动工程,分布式部署。
后端授权认证微服务启动工程,微服务部署。
api工程,后端支付服务api接口工程,外部工程调用时注入统一依赖api工程。
impl工程,后端支付服务api的接口实现工程,依赖javalsj-auth-api工程实现controller的具体逻辑。注:api和impl组合可以用来构建单体服务架构下的程序。
client工程,后端支付服务提供给外部微服务调用的客户端工程,依赖javalsj-auth-api工程实现 Feign Client 服务调用客户端,其他微服务工程依赖该client工程实现微服务调用。
后端订单服务目录。
api工程,后端国外订单服务api接口工程,外部工程调用时注入统一依赖api工程。
impl工程,后端国外订单服务api的接口实现工程,依赖javalsj-order-foreign-api工程实现controller的具体逻辑。注:api和impl组合可以用来构建单体服务架构下的程序。
client工程,后端国外订单服务提供给外部微服务调用的客户端工程,依赖javalsj-order-foreign-api工程实现 Feign Client 服务调用客户端,其他微服务工程依赖该client工程实现微服务调用。
app-microservice工程,后端国外订单服务的微服务启动工程,依赖javalsj-order-foreign-impl,若需要调用其他微服务,则依赖其他微服务的client工程代码。
app工程,后端国外订单服务的独立启动工程,可用于分布式调用。
api工程,后端国内订单服务api接口工程,外部工程调用时注入统一依赖api工程。
impl工程,后端国内订单服务api的接口实现工程,依赖javalsj-order-internal-api工程实现controller的具体逻辑。注:api和impl组合可以用来构建单体服务架构下的程序。
client工程,后端国内订单服务提供给外部微服务调用的客户端工程,依赖javalsj-order-internal-api工程实现 Feign Client 服务调用客户端,其他微服务工程依赖该client工程实现微服务调用。
app工程,后端国内订单服务的启动工程,依赖javalsj-order-internal-impl,若需要调用其他微服务,则依赖其他微服务的client工程代码。
app工程,后端国内订单服务的独立启动工程,可用于分布式部署。
后端支付服务目录。
api工程,后端支付服务api接口工程,外部工程调用时注入统一依赖api工程,包结构:javalsj.pay.api、javalsj.pay.api.vo、javalsj.pay.api.dto。
impl工程,后端支付服务api的接口实现工程,依赖javalsj-pay-api工程实现controller的具体逻辑。注:api和impl组合可以用来构建单体服务架构下的程序,包结构:javalsj.pay.impl.controller、javalsj.pay.impl.do、javalsj.pay.impl.service、javalsj.pay.impl.service.impl、javalsj.pay.impl.dao、javalsj.pay.impl.dao.impl。
client工程,后端支付服务提供给外部微服务调用的客户端工程,依赖javalsj-pay-api工程实现 Feign Client 服务调用客户端,其他微服务工程依赖该client工程实现微服务调用,工程包结构:javalsj.pay.client、javalsj.pay.client.fallbackfactory。
app-microservice工程,后端支付服务的启动工程,依赖javalsj-pay-impl,若需要调用其他微服务,则依赖其他微服务的client工程代码。
app工程,后端支付服务的独立启动工程,可用于分布式部署。
后端物流服务目录。
api工程,后端物流服务api接口工程,外部工程调用时注入统一依赖api工程。
impl工程,后端物流服务api的接口实现工程,依赖javalsj-logistics-api工程实现controller的具体逻辑。注:api和impl组合可以用来构建单体服务架构下的程序。
client工程,后端物流服务提供给外部微服务调用的客户端工程,依赖javalsj-logistics-api工程实现 Feign Client 服务调用客户端,其他微服务工程依赖该client工程实现微服务调用。
app-microservice工程,后端物流服务的启动工程,依赖javalsj-logistics-impl,若需要调用其他微服务,则依赖其他微服务的client工程代码。
app工程,后端物流服务的独立启动工程,可用于分布式部署。
后端构建单体启动服务工程。
为了便于理解设计,现在进行工程结构层次设计说明,通过上面的一段描述,读者可以看到每个服务工程都被拆分成了app、app-microservice、api、impl、client 5个 project,读者可以试先按文字描述来预想以下几个问题:
独立服务部署应用启动工程。通过 app 的 build.gradle 来构建当前独立服务需要的工程依赖,依赖主要为 api、impl 工程。若该服务需要调用其他服务则依赖再加其他服务的client工程。通过 app 的 application.yml 配置文件设置 Feign Client 设置 name 、url 直连属性。
微服务部署启动工程。通过 app-microservice 的 build.gradle 来构建当前独立服务需要的工程依赖,依赖主要为 api、impl 工程。若该服务需要调用其他服务则依赖再加其他服务的client工程。通过 app 的 application.yml 配置文件设置 Feign Client 只设置 name 属性。
服务 api 接口工程,工程模块互相依赖时统一依赖其他服务模块的api接口工程,然后再通过构建依赖 impl 或者 client 工程,利用 Spring IOC 自动注入机制来决定该接口最终是调用服务内部的 controller,还是调用其他服务的client客户端。
服务 api 的接口实现工程,依赖api工程。该工程只用于服务内部构建,不允许其他服务做依赖,工程内容主要是包含 controller、service、dao等业务逻辑模块。
服务提供给外部服务调用的 client 客户端工程,依赖 api 工程。该工程只用于其他服务构建依赖,不允许服务内部做依赖,工程内容主要是包含 Feign Client 的通信模块,单独拎出来是为了减少其他服务调用该服务时都写一份client的冗余操作。
读者可以通过上面工程结构发现,这样拆分工程的是会增加大量的工程数量,对工程规范性要求也变得较高这是其中的一个缺点。但是在多团队工程规模较大的情况下,这种做法又带来了很大的优点和灵活性,具体如下:
在实际工作中,拆分微服务的业务边界其实是一个比较费劲的工作量,而且随着项目的不断扩大,本身拆分的边界已经不满足性能需求了,此时可能会做出如下两种场景改造。 1.对已拆分的服务再做二次拆分。 场景:一开始只是把订单业务拆分成独立的服务,但是后续发现订单业务服务扛不住了此时可能就需要再拆分成国内和国外订单两个服务)。 2.对已拆分的服务做合并。 场景:一开始拆分了国内和国外订单两个服务,但是后续发现订单业务量不大,此时为了降低运维成本可能就会把这两个服务合并成一个订单业务)。以上两个场景均可通过该工程方案解决。
现有api接口工程提供 impl 和 client 两种实现,其中 client 为 feign client 组件实现,如果后需有 Dubbo 或者 Webservice 等实现,也可以 扩展添加client-webservice或者client-dubbo工程,具体使用哪个工程,只需要在app启动工程做依赖即可。
读者可以参考下列不同服务进行 Gradle 组合构建来理解组配方案。
启动工程:javalsj-app
Gradle 依赖:javalsj-common-base、javalsj-common-web、javalsj-auth-api、javalsj-auth-impl、javalsj-order-foreign-api、javalsj-order-foreign-impl、javalsj-order-internal-api、javalsj-order-internal-impl、javalsj-pay-api、javalsj-pay-impl、javalsj-logistics-api、javalsj-logistics-impl
授权认证服务
启动工程:javalsj-auth-app
Gradle 依赖:javalsj-common-base、javalsj-common-web、javalsj-auth-api、javalsj-auth-impl
订单服务
启动工程:javalsj-order-app
Gradle 依赖:javalsj-common-base、javalsj-common-web、javalsj-order-foreign-api、javalsj-order-foreign-impl、javalsj-order-internal-api、javalsj-order-internal-impl、javalsj-pay-api、javalsj-pay-client
支付服务
启动工程:javalsj-pay-app
Gradle 依赖:javalsj-common-base、javalsj-common-web、javalsj-pay-api、javalsj-pay-impl、javalsj-logistics-api、javalsj-logistics-client
物流服务
启动工程:javalsj-logistics-app
Gradle 依赖:javalsj-common-base、javalsj-common-web、javalsj-logistics-api、javalsj-logistics-impl
备注:分布式构建app依赖时是不需要依赖服务注册发现和配置中心组件。
授权认证微服务
启动工程:javalsj-auth-app-microservice
Gradle 依赖:javalsj-common-base、javalsj-common-web、javalsj-auth-api、javalsj-auth-impl
国内订单微服务
启动工程:javalsj-order-internal-app-microservice
Gradle 依赖:javalsj-common-base、javalsj-common-web、javalsj-order-internal-api、javalsj-order-internal-impl、javalsj-pay-api、javalsj-pay-client
国外订单微服务
启动工程:javalsj-order-foreign-app-microservice
Gradle 依赖:javalsj-common-base、javalsj-common-web、javalsj-order-foreign-api、javalsj-order-foreign-impl、javalsj-pay-api、javalsj-pay-client
支付微服务
启动工程:javalsj-pay-app-microservice
Gradle 依赖:javalsj-common-base、javalsj-common-web、javalsj-pay-api、javalsj-pay-impl、javalsj-logistics-api、javalsj-logistics-client
物流微服务
启动工程:javalsj-logistics-app-microservice
Gradle 依赖:javalsj-common-base、javalsj-common-web、javalsj-logistics-api、javalsj-logistics-impl
备注:微服务构建app依赖时是需要依赖服务注册发现和配置中心组件。
上面分布式和微服务的依赖工程一样,又是如何做到划分的呢?这是利用了 Feign Client 的组件特性。我们知道 @Feign Client 有两个属性: name 和 url,其中name属性是必要的,url 属性非必须,在处理时有下列特性。若 url 属性存在,feign会直连url地址调用,此时不会走服务发现,相当于分布式调用,生产时可以把url配置集群的nginx节点地址来达到分布式的负载均衡策略。 若 url 属性是空的时候,feign会按name从服务发现找对应服务名的服务集群,并按负载均衡策略选择其中的一个节点进行调用。两者在工程级别上只是在 app 和 app-microservice 的 application.yml 做配置url参数及是否依赖服务注册与发现等组件的区别。
针对上述工程结构层次来设计Git仓库,现列出的解决方案有两种,分别是整个项目只使用一个Git仓库和每个团队服务拥有各自的Git仓库,那么两者有什么区别呢?简单列出如下区别供参考:
一个Git仓库,1个。 多个Git仓库,N个。
一个Git仓库,则 Gradle 版本控制也是统一的,不用研发人员特别关注。 多个Git仓库,则 Gradle 版本控制需要研发人员特别关注,为了版本一致,实际使用时会把依赖包版本放在统一的一个公共目录下使用,每个Git仓库在做依赖时统一引用公共版本模块来达到一致性。
一个Git仓库,则直接Clone仓库到本地即可,不用关注工程之间依赖的层次文件目录位置放的对不对。 多个Git仓库,则需要Clone多个仓库到本地,且多个仓库之间有引用的话,还需要特别注意仓库目录位置是否放的对不对,若不对的话在build时就会报未找到依赖包的错误。
一个Git仓库,每次都会拉取整个服务代码,若多团队的情况下则拉取的代码库可能会比较大,即时当前团队可能不会关注的其他团队代码也会被拉取下来,在提交代码需要Merge的概率也会大大提高。 多个Git仓库,每个团队只拉取自己团队的代码,拉取的代码相对较小,便于整个多团队的Git权限管理等。
友情提示:针对以上区别,笔者在这里推荐第二种每个团队服务拥有各自的 Git 仓库的方式,这也是我们实际生产中使用的方式,其实两者差异不是太大,只用做好仓库的管理和规范化即可。笔者为了方便简化的说明工程代码内部的核心内容,此处采取整个项目使用一个 Git 仓库,希望不会影响读者的思路。
是整个单体服务的app服务目录。
是整个多团队多服务公用的依赖工程,比如所有微服务都公用的包工具,当前其存放的包主要有以下:
是整个多团队多服务公用的Gradle构建依赖目录。其下存放 Gradle 统一版本控制 version.gradle 文件、发布程序到maven仓库的 push2maven.gradle 文件,方便对整个项目的依赖版本做控制。
是订单业务团队工程存放的目录。
是支付业务团队工程存放的目录。
是物流业务团队工程存放的目录。
从 Gradle 官网 https://gradle.org/ 下载新版 Gradle 版本并解压,并设置环境变量,安装后在D:/SoftFile/gradle-5.6.2路径下新建文件夹 userhome,用于存放 Gradle 下载的依赖文件,如图。
通过 Git Clone 实例项目(地址: https://gitee.com/wangjihong/... ),并切换分支为develop, 如图。
如图所示,我们本次实例主要业务是让用户访问获取订单 rest api 请求来获取对应的订单、支付、物流信息。订单模块提供订单的id、code信息,支付模块提供payId、payCode信息,物流模块提供logisticsId、logisticsCode信息。
如图所示,单体服务由于只集成了个模块impl工程,所以订单内部注入到PayApi和LogisticsApi的实现类即为PayController和LogisticsController,调用时也是代码类本身方法的调用,不依赖于Feign组件。
IDEA导入单体启动 app 工程: W:/Workspace/git_workspace/javalsj/javalsj_backend/javalsj-app,然后启动单体服务如图:
使用Postman工具模拟访问单体服务订单的rest接口地址: http://localhost :8001/api/order/v1/internal ,结果如下:
由上可见,单体服务集成订单、支付、物流模块后均正常访问。
在单体启动工程 javalsj-app 我们可以看到因为单体不涉及服务之间的调用所以 build.gradle 构建脚本只依赖了各模块的 impl 工程:
物流业务模块库 javalsj-logistics-impl 工程。
buildscript { ext { springBootGradlePluginVersion = '2.2.0.RELEASE' dependencyManagementPluginVersion = '1.0.8.RELEASE' junitPlatformGradlePluginVersion = '1.2.0' buildGradleVersion = '3.1.0' } repositories { mavenLocal() maven { url "http://maven.aliyun.com/nexus/content/groups/public" } mavenCentral() maven { url 'https://maven.aliyun.com/repository/public/' } maven { url 'https://maven.aliyun.com/repository/spring/' } } dependencies { classpath "org.junit.platform:junit-platform-gradle-plugin:${junitPlatformGradlePluginVersion}" classpath "io.spring.gradle:dependency-management-plugin:${dependencyManagementPluginVersion}" classpath "org.springframework.boot:spring-boot-gradle-plugin:${springBootGradlePluginVersion}" classpath "com.android.tools.build:gradle:${buildGradleVersion}" } } apply plugin: 'java' apply plugin: 'idea' apply plugin: 'eclipse' apply plugin: 'java-library' apply plugin: 'io.spring.dependency-management' apply plugin: 'org.junit.platform.gradle.plugin' apply from: "../javalsj-gradle/push2maven.gradle" apply from: "../javalsj-gradle/version.gradle" dependencies { // 订单模块库 implementation commonLib.javalsj_common_web implementation orderLib.javalsj_order_foreign_impl implementation orderLib.javalsj_order_internal_impl // 支付模块库 implementation payLib.javalsj_pay_impl // 物流模块库 implementation logisticsLib.javalsj_logistics_impl }
application.yml 配置没什么特别的
server: port: 8001 servlet: context-path: / session: timeout: 10800
如图所示,分布式服务由于集成了模块impl工程和支付物流的client工程,所以订单内部注入到PayApi和LogisticsApi的实现类即为Pay Feign Client 和Logistics Feign Client ,加上启动配置文件中设置了 Feign Client 的url属性,所以调用时是采用的直连url的方式,达到分布式调用的效果。
如图。
在分布式订单启动工程 javalsj-order-app 我们可以看到因为单体涉及服务之间的调用所以 build.gradle 构建脚本依赖了自身模块的 impl 工程以及支付、物流模块的 client 工程。client工程供订单服务使用Feign Client 对支付、物流服务接口 url 进行调用,:
分布式与单体区别如下,由于支付、物流服务没什么特殊,所以只拿订单服务的脚本和配置看下。
app启动工程增加其他模块的client工程依赖。
buildscript { ext { springBootGradlePluginVersion = '2.2.0.RELEASE' dependencyManagementPluginVersion = '1.0.8.RELEASE' junitPlatformGradlePluginVersion = '1.2.0' buildGradleVersion = '3.1.0' } repositories { mavenLocal() maven { url "http://maven.aliyun.com/nexus/content/groups/public" } mavenCentral() maven { url 'https://maven.aliyun.com/repository/public/' } maven { url 'https://maven.aliyun.com/repository/spring/' } } dependencies { classpath "org.junit.platform:junit-platform-gradle-plugin:${junitPlatformGradlePluginVersion}" classpath "io.spring.gradle:dependency-management-plugin:${dependencyManagementPluginVersion}" classpath "org.springframework.boot:spring-boot-gradle-plugin:${springBootGradlePluginVersion}" classpath "com.android.tools.build:gradle:${buildGradleVersion}" } } apply plugin: 'java' apply plugin: 'idea' apply plugin: 'eclipse' apply plugin: 'java-library' apply plugin: 'io.spring.dependency-management' apply plugin: 'org.junit.platform.gradle.plugin' apply from: "../../javalsj-gradle/push2maven.gradle" apply from: "../../javalsj-gradle/version.gradle" dependencies { implementation orderLib.javalsj_order_foreign_impl implementation orderLib.javalsj_order_internal_impl // 支付接口客户端 implementation payLib.javalsj_pay_client // 物流接口客户端 implementation logisticsLib.javalsj_logistics_client }
增加了@ Feign Client 的url属性值配置(上文也提到了分布式部署方式利用了 Feign Client url 属性值若存在,则 Feign 会直连url 进行地址调用,此时不会走服务发现,相当于分布式调用的组件特性。):
custom: service: name: order-service service-name: pay: pay-service logistics: logistics-service service-url: // 增加支付URL直连地址 pay: http://localhost:9003 // 增加物流URL直连地址 logistics: http://localhost:9004 management: endpoints: web: exposure: include: '*' server: port: 9002 servlet: context-path: / session: timeout: 10800 feign: httpclient: enabled: false okhttp: enabled: true
微服务情况下,我们首先需要搭建服务注册与发现,此处使用 Nacos 开源组件,下载安装启动过程如图。
如图所示,微服务由于集成了模块impl工程和支付物流的client工程,所以订单内部注入到PayApi和LogisticsApi的实现类即为Pay Feign Client 和Logistics Feign Client ,启动类加了服务发现注解@EnableDiscoveryClient,启动配置文件中没有设置 Feign Client 的url属性,只设置了name属性,所以调用时会走 Nacos 服务发现找到对应name的服务进行负载均衡调用,达到微服务调用的效果。
IDEA导入物流微服务 app 工程: W:/Workspace/git_workspace/javalsj/javalsj_backend/javalsj-logistics/javalsj-logistics-app-microservice并启动。
如图。
在微服务启动工程 javalsj-order-app 我们可以看到因为单体涉及服务之间的调用所以 build.gradle 构建脚本依赖了自身模块的 impl 工程以及支付、物流模块的 client 工程。client工程供订单服务使用Feign Client 对支付、物流服务接口 url 进行调用,:
物流微服务,依赖模块库 javalsj-logistics-impl、spring-cloud-starter-alibaba-nacos-discovery 工程。
微服务与分布式的区别如下:
app-microservice启动工程增加了 Nacos 服务注册与发现依赖。
buildscript { ext { springBootGradlePluginVersion = '2.2.0.RELEASE' dependencyManagementPluginVersion = '1.0.8.RELEASE' junitPlatformGradlePluginVersion = '1.2.0' buildGradleVersion = '3.1.0' } repositories { mavenLocal() maven { url "http://maven.aliyun.com/nexus/content/groups/public" } mavenCentral() maven { url 'https://maven.aliyun.com/repository/public/' } maven { url 'https://maven.aliyun.com/repository/spring/' } } dependencies { classpath "org.junit.platform:junit-platform-gradle-plugin:${junitPlatformGradlePluginVersion}" classpath "io.spring.gradle:dependency-management-plugin:${dependencyManagementPluginVersion}" classpath "org.springframework.boot:spring-boot-gradle-plugin:${springBootGradlePluginVersion}" classpath "com.android.tools.build:gradle:${buildGradleVersion}" } } apply plugin: 'java' apply plugin: 'idea' apply plugin: 'eclipse' apply plugin: 'java-library' apply plugin: 'io.spring.dependency-management' apply plugin: 'org.junit.platform.gradle.plugin' apply from: "../../javalsj-gradle/push2maven.gradle" apply from: "../../javalsj-gradle/version.gradle" dependencies { implementation orderLib.javalsj_order_internal_impl implementation payLib.javalsj_pay_client implementation logisticsLib.javalsj_logistics_client // 服务注册与发现 implementation pluginLib.spring_cloud_starter_alibaba_nacos_discovery }
去掉了@ Feign Client 的url属性值配置(上文也提到了微服务部署方式利用了 Feign Client url 属性值若不存在,则 Feign 会通过name属性进行服务发现负载均衡到目标服务,此时会调用微服务模式的组件特性。):
custom: service: name: order-internal-service service-name: pay: pay-service logistics: logistics-service spring: application: name: ${custom.service.name} cloud: nacos: discovery: server-addr: 127.0.0.1:8848 management: endpoints: web: exposure: include: '*' server: port: 9002 servlet: context-path: / session: timeout: 10800 feign: httpclient: enabled: false okhttp: enabled: true
增加了服务发现注解@EnableDiscoveryClient。
package com.javalsj.order.internal.app.microservice; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.openfeign.Enable Feign Client s; // 增加服务发现注解 @EnableDiscoveryClient @SpringBootApplication public class JavalsjOrderInternalAppMicroserviceApplication { public static void main(String[] args) { SpringApplication.run(JavalsjOrderInternalAppMicroserviceApplication.class, args); } }
本文主要介绍了同一套代码库工程结构设计以及在适配单体服务、分布式、微服务各种模式下的工程设计思路,笔者为了让读者脱离纯理论来便于理解文章,通过写的一个Demo工程实例来演示各服务模式下的配置区别,读者可以通过实例代码自行本地运行测试以便更易理解。(PS:如有设计疑问,可以在文章的评论区发表,笔者看到后会及时回复,互相学习,谢谢)。
彩蛋: