一、微服务介绍
1、系统架构演变-上
(1)系统架构演变
随着互联网的发展,网站应用的规模不断扩大。需求的激增,带来的是技术上的压力。系统架构也因此也不断的演进、升级、迭代。
从互联网早起到现在,系统架构大体经历了下面几个过程:单体应用架构--->垂直应用架构--->分布式架构--->SOA架构--->微服务架构,当然还有在Google带领下来势汹涌的Service Mesh(服务网格化)。
这里讲解微服务架构,相比前面要难的多,是目前大型项目的主流开发方式。为了便于大家理解微服务架构,先为大家讲解系统架构演变的过程。
(2)单体应用架构
互联网早期,一般的网站应用的规模较小时,只需一个应用将所有功能都部署在一起,可以减少开发、部署和维护的成本。这时用于简化增删改查工作量的数据访问框架(ORM)是影响项目开发的关键。我们前面学习的SSM应用就是这种集中式架构。
以网上商城为例,其中有商品管理、订单管理、用户管理、购物车管理等很多模块,这些模块都在一个Web项目中进行开发和部署。这种方式结构简单,易于开发,易于部署。
单体应用架构的优点:
1、项目架构简单,小型项目的话,开发成本低
2、项目部署在一个节点上,维护方便
单体应用架构的缺点:
1、全部功能集成在一个工程中,对于大型项目来讲不易开发和维护。
我们在一个项目中开发所有功能时,这些功能之间的耦合度非常高。某个开发人员提交了有问题的代码,会导致所有开发人员的项目中有大量的冲突和问题。单体应用架构不仅会导致开发维护困难,而且开发效率也低。
2、无法针对不同模块进行针对性能优化和水平扩展。
比如订单模块的数据量是最大的,希望对它的存储进行优化,但是所有模块耦合在一起,很难进行针对性的优化。
3、项目模块之间紧密耦合,单点容错率低,并发能力差。
所有功能在一个项目中实现,也会部署在一台服务器中,这样就会导致单点故障,并且无法通过集群等技术来扩展系统。
(3)垂直应用架构
随着访问量的逐渐增大,单一应用只能依靠增加节点来应对,但是这时候会发现并不是所有的模块都会有比较大的访问量。
还是以上面的电商为例子,用户访问量的增加可能影响的只是用户和订单模块,但是对消息模块的影响就比较小。那么此时我们希望只多增加几个订单模块,而不增加消息模块。此时单体应用就做不到了,垂直应用就应运而生了。
所谓的垂直应用架构,就是将原来的一个应用拆成互不相干的几个应用,独立开发,独立部署,以提升效率。比如我们可以将上面电商的单体应用拆分成:
电商系统(用户管理、商品管理、订单管理)
后台系统(用户管理、订单管理、客户管理)
CMS系统(广告管理、营销管理)
这样拆分完毕之后,一旦用户访问量变大,只需要增加电商系统的节点就可以了,而无需增加后台和CMS的节点。
垂直拆分的优点:
1、系统拆分实现了流量分担,解决了并发问题
2、可以针对不同模块进行优化
3、方便水平扩展,负载均衡,容错率提高
这种方式就解决了集中式架构中的问题,所有模块都是独立开发的,互不干扰。
垂直拆分的缺点:
1、系统之间相互独立,无法进行相互调用
2、系统间相互独立,会有很多重复开发工作,影响开发效率
比如订单管理、购物车管理等都会调用商品管理,但是这些模块都是独立开发的,不同模块之间可能有很多重复开发工作。
2、系统架构演变-中
(1)分布式架构
当垂直应用越来越多,重复的业务代码就会越来越多。这时候,我们就思考可不可以将重复的代码抽取出来,作为独立的服务,逐渐形成统一的服务层,使得前端应用能更快速的响应多变的市场需求。这种方式中提高业务复用及整合的分布式调用是关键。
这就产生了新的分布式系统架构。它将把工程拆分成表现层和服务层两个部分,服务层中包含业务逻辑。表现层只需要处理和页面的交互,业务逻辑都是调用服务层的服务来实现。
分布式服务的优点:
1、抽取公共的功能为服务层,提高代码复用性和开发效率。
分布式服务的缺点:
1、系统间耦合度变高,调用关系错综复杂,难以维护
(2)SOA架构
在分布式架构下,当服务越来越多,容量的评估、小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心对集群进行实时管理,提高集群利用率。此时,用于资源调度和治理的服务注册中心(SOA Service Oriented Architecture,面向服务的架构)就是关键。
以前分布式服务出现了什么问题?
1、服务越来越多,需要记住每个服务的地址。
2、调用关系错综复杂,难以理清依赖关系。
3、服务过多,服务状态难以管理,无法根据服务情况动态管理。
服务治理(SOA)要做什么?
1、服务注册中心,实现服务自动注册和发现,无需人为记录服务地址。
2、服务自动订阅,服务列表自动推送,服务调用透明化,无需关心依赖关系。
3、动态监控服务状态监控报告,人为控制服务状态。
我们以中介公司为例。务工人员到中介公司进行登记,如个人信息、技能、要求等,公司向中介公司提出用工要求,中介公司根据要求,筛选出合适的务工人员推荐给公司。这个过程中,中介公司相当于“注册中心”,务工人员相当于“服务”,所有服务在启动之后先在注册中心中进行登记。公司相当于“客户端”,它不用记住所需服务的地址,直接在注册中心中查找合适的服务,然后调用这个服务即可。
公司在用工过程中,如果务工人员出现辞工等异常情形,公司直接找中介公司,中介公司再推荐合适的人员过来务工。这个过程中,中介公司相当于“治理中心”,可以对服务进行监控和调度,如对异常服务的管理。
服务治理(SOA)优点:
1、使用注册中心解决了服务间调用关系的自动调节
服务治理(SOA)缺点:
1、服务间会有依赖关系(拆分不彻底),一旦某个环节出错会影响较大(服务雪崩)
2、服务关系复杂,运维、测试部署困难
3、系统架构演变-下
(1)SOA和微服务架构
前面说的SOA,英文翻译过来是面向服务的架构。微服务,似乎也是服务,都是对系统进行拆分。因此两者非常容易混淆,但其实有一些差别:
SOA刚出来的时候,开发人员把服务的粒度划分的比较大。一个服务中通常有多个业务功能,不仅服务内部耦合较大,而且服务之间通常有强依赖关系,如果某个被依赖服务出现异常,所有依赖于它的服务也无法正常工作,甚至无法启动服务,容易引起雪崩效应。
微服务是在SOA基础上发展起来的,也是对系统进行拆分,但是微服务有自己的特点:
1、单一职责:每个微服务都对应唯一的业务能力(不是CRUD),做到单一职责。
2、微:微服务的服务拆分粒度很小,例如一个用户管理就可以作为一个服务。每个服务虽小,但“五脏俱全”。微服务的粒度到底有多大?一个查询功能能不能作为微服务,整个商品管理能不能作为微服务,这里没有定论,具体问题要具体判断,如果功能过细会导致运维复杂,如果功能过大则导致耦合过高,因此微服务的设计更多取决于经验。
3、面向服务:面向服务是说每个服务都要对外暴露服务接口API。并不关心服务的技术实现,做到与平台和语言无关,也不限定用什么技术实现,只要提供REST的接口即可。
4、自治:自治是说服务间互相独立,互不干扰
(2)微服务架构
微服务架构在某种程度上是面向服务的架构SOA继续发展的下一步,它更加强调服务的"彻底拆分"。以前面案例为例,微服务的实现如下:
微服务架构的优点:
1、服务原子化拆分,独立打包、部署和升级,保证每个微服务清晰的任务划分,利于扩展。
2、微服务之间采用Restful等轻量级http协议相互调用。
微服务架构缺点:
1、分布式系统开发的技术成本高(容错、分布式事务等)。
4、微服务架构的常见解决方案
(1)SpringCloud
Spring Cloud是一套生态,也是一系列框架的集合。它利用Spring Boot的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等,都可以用Spring Boot的开发风格做到一键启动和部署。
Spring Cloud并没有重复制造轮子,它只是将目前各家公司开发的比较成熟、经得起实际考验的服务框架组合起来,通过Spring Boot风格进行再封装屏蔽掉了复杂的配置和实现原理,用很少的配置就能完成微服务框架的搭建,最终给开发者留出了一套简单易懂、易部署和易维护的分布式系统开发工具包。
(2)Spring Cloud Netflix
我们知道Spring Cloud本身只是一套微服务规范,并不是一个拿来即可用的框架,而Spring Cloud Netflix为开发者们提供了这套规范的实现方式,也是Spring Cloud提供的第一套微服务架构解决方案。
下面是Spring Cloud Netflix的官方介绍:
Spring Cloud Netflix provides Netflix OSS integrations for Spring Boot apps through autoconfiguration and binding to the Spring Environment and other Spring programming model idioms. With a few simple annotations you can quickly enable and configure the common patterns inside your application and build large distributed systems with battle-tested Netflix components. The patterns provided include Service Discovery (Eureka), Circuit Breaker (Hystrix), Intelligent Routing (Zuul) and Client Side Load Balancing (Ribbon)..
简单翻译一下:Spring Cloud Netflix提供了Netflix OSS集成到Spring Boot应用,通过自动化配置、绑定到spring环境和其他的习惯的spring编程模型。通过少量的注解你就可以快速启用和配置通用模式到你的应用,使用经过Netflix实战检验的组件去构建大型分布式系统。这个模式提供了包含服务注册与发现(Eureka)、智能路由网关(Zuul)、客户端负责均衡(Ribbon)、服务调用(Feign)、断路器(Hystrix)等组件。
(3)Spring Cloud Alibaba
先来看一下官方是怎么定义Spring Cloud Alibaba的:
Spring Cloud Alibaba致力于提供微服务开发的一站式解决方案。此项目包含开发分布式应用微服务的必需组件,方便开发者通过Spring Cloud编程模型轻松使用这些组件来开发分布式应用服务。
依托Spring Cloud Alibaba,您只需要添加一些注解和少量配置,就可以将Spring Cloud应用接入阿里微服务解决方案,通过阿里中间件来迅速搭建分布式应用系统。
5、SpringCloud Alibaba介绍
我们知道Spring Cloud本身只是一套微服务规范,并不是一个拿来即可用的框架,而Spring Cloud Alibaba为开发者们提供了这套规范的实现方式。同时,Spring Cloud Alibaba提供的完整的微服务组件、中文文档和本地化的开源服务提高了开发者们接入微服务的速率,并降低了后续的运维难度。
(1)Alibaba vs Netflix
我们可以将Spring Cloud Alibaba和Spring Cloud Netflix做一个比较,他们俩都是Spring Cloud标准实现的微服务一站式解决方案提供商。下图是阿里官方开发者社区在介绍关于Spring Cloud Alibaba文章时,将其与各大提供商做比较的对比图:
(2)Spring Cloud Netflix进入维护模式
2018年12月12日,Netflix官方宣布了旗下大部分的微服务组件进入了Maintenance Mode(维护模式)。官方还特意解释了什么是Maintenance Mode(维护模式)。
1、将模块组件置于维护模式,意味着Spring Cloud团队将不会再为这些模块组件添加新的功能。但是我们将修复blocker级别的bug以及安全问题,我们也会考虑并审查社区的小型pull request。
2、我们打算继续为这些模块提供至少一年的支持,直到Greenwich发布版本基本可用为止。
以下Spring Cloud Netflix模块组件和相应的Starter将进入维护模式:
1.spring-cloud-netflix-archaius
2.spring-cloud-netflix-hystrix-contract
3.spring-cloud-netflix-hystrix-dashboard
4.spring-cloud-netflix-hystrix-stream
5.spring-cloud-netflix-hystrix
6.spring-cloud-netflix-ribbon
7.spring-cloud-netflix-turbine-stream
8.spring-cloud-netflix-turbine
9.spring-cloud-netflix-zuul
Netflix的组件短期内使用还是没有问题的,毕竟是已经被业界广泛使用过的,是经得住大规模生产环境的考验的,对于还在使用Netflix组件的企业来说也不必惊慌失措,但是由于不再会有新特性的出现,维护力度持续下降,长期使用肯定是不被推荐的了。
对于国内的Java开发者来说,对Spring Cloud Alibaba的开源组件可能会更加的期盼,它将会是Spring Cloud Netflix的完美替代者,并且Spring Cloud Alibaba的组件一定会更加符合国情,对于国内的开发者会更加人性化。
(3)Spring Cloud Alibaba简介
2018年10月31日,Spring Cloud Alibaba正式入驻Spring Cloud官方孵化器并发布了第一个预览版本。2019年7月24日Spring Cloud Alibaba正式从Spring Cloud Incubator孵化器毕业,成为了Spring社区的正式项目。
Spring Cloud for Alibaba项目包含了阿里巴巴开源组件和多个云产品。该项目旨在实现并公开众所周知的Spring Framework模式和抽象,以便给使用阿里巴巴产品的Java开发者带来使用Spring Boot和Spring Cloud的更多便利。
下面四个是开源组件:
Nacos:一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。
Sentinel:把流量作为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
RocketMQ:一款开源的分布式消息系统,基于高可用分布式集群技术,提供低延时的、高可靠的消息发布与订阅服务。
Dubbo:Apache Dubbo™ 是一款高性能 Java RPC 框架。
Seata:阿里巴巴开源产品,一个易于使用的高性能微服务分布式事务解决方案。
下面四个是收费组件:
Alibaba Cloud ACM:一款在分布式架构环境中对应用配置进行集中管理和推送的应用配置中心产品。
Alibaba Cloud OSS: 阿里云对象存储服务(Object Storage Service,简称 OSS),是阿里云提供的海量、安全、低成本、高可靠的云存储服务。您可以在任何应用、任何时间、任何地点存储和访问任意类型的数据。
Alibaba Cloud SchedulerX: 阿里中间件团队开发的一款分布式任务调度产品,提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。
Alibaba Cloud SMS: 覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速搭建客户触达通道。
6、微服务架构的常见问题和解决思路
微服务架构,简单的说就是将单体应用进一步拆分,拆分成更小的服务,每个服务都是一个可以独立运行的项目。
(1)微服务架构的常见问题
一旦采用微服务系统架构,就势必会遇到这样几个问题:
1、这么多小服务,如何管理他们?(服务治理[服务注册、发现、剔除、配置])
2、这么多小服务,他们之间如何通讯?(restful、rpc)
3、这么多小服务,客户端怎么访问他们?(网关)
4、这么多小服务,一旦出现问题了,应该如何自处理?(熔断、限流、服务降级)
5、这么多小服务,一旦出现问题了,应该如何排错? (链路追踪)
(2)微服务架构的解决方案
对于上面的问题,是任何一个微服务设计者都不能绕过去的,因此大部分的微服务产品都针对每一个问题提供了相应的组件来解决它们。
1、Nacos提供服务治理
服务治理就是进行服务的自动化管理,其核心是服务的自动注册与发现。
(1)服务注册:服务实例将自身服务信息注册到注册中心。
(2)服务发现:服务实例通过注册中心,获取到注册到其中的服务实例的信息,通过这些信息去请求它们提供的服务。
(3)服务剔除:服务注册中心将出问题的服务自动剔除到可用列表之外,使其不会被调用到。
2、OpenFeign调用就是基于RESTful接口
在微服务架构中,通常存在多个服务之间的远程调用的需求。目前主流的远程调用技术有基于HTTP的RESTful接口。
3、Spring Cloud GateWay
随着微服务的不断增多,不同的微服务一般会有不同的网络地址,而外部客户端可能需要调用多个服务的接口才能完成一个业务需求,如果让客户端直接与各个微服务通信可能出现:
(1)客户端需要调用不同的url地址,增加难度
(2)在一定的场景下,存在跨域请求的问题
(3)每个微服务都需要进行单独的身份认证
针对这些问题,API网关顺势而生。API网关直面意思是将所有API调用统一接入到API网关层,由网关层统一接入和输出。一个网关的基本功能有:统一接入、安全防护、协议适配、流量管控、长短链接支持、容错能力。有了网关之后,各个API服务提供团队可以专注于自己的的业务逻辑处理,而API网关更专注于安全、流量、路由等问题。
4、Sentinel容错处理
在微服务当中,一个请求经常会涉及到调用几个服务,如果其中某个服务不可用,没有做服务容错的话,极有可能会造成一连串的服务不可用,这就是雪崩效应。
我们没法预防雪崩效应的发生,可以用Sentinel去做好容错。
5、Sleuth链路追踪
随着微服务架构的流行,服务按照不同的维度进行拆分,一次请求往往需要涉及到多个服务。互联网应用构建在不同的软件模块集上,这些软件模块,有可能是由不同的团队开发、可能使用不同的编程语言来实现、有可能布在了几千台服务器,横跨多个不同的数据中心。因此,就需要对一次请求涉及的多个服务链路进行日志记录,性能监控即链路追踪。
7、微服务项目架构示例
(1)微服务架构
为了满足移动互联网对大型项目及多客户端的需求,软件架构也作了升级和优化,将一个独立的系统拆分成若干小的服务,每个小服务运行在不同的进程中,服务与服务之间采用RESTful、RPC等协议传输数据,每个服务所拥有的功能具有独立性强的特点,这样的设计就实现了单个服务的高内聚,服务与服务之间的低耦合效果,这些小服务就是微服务,基于这种方法设计的系统架构即微服务架构。
微服务架构的特点:
1、服务层按业务拆分为一个一个的微服务。
2、微服务的职责单一。
3、微服务之间采用RESTful、RPC等轻量级协议传输。
4、有利于采用前后端分离架构。
微服务架构优点:
1、服务拆分粒度更细,有利于资源重复利用,提高开发效率。
2、可以更加精准的制定每个服务的优化方案,按需伸缩。
3、适用于互联网时代,产品迭代周期更短。
微服务架构缺点:
1、开发的复杂性增加,因为一个业务流程需要多个微服务通过网络交互来完成。
2、微服务过多,服务治理成本高,不利于系统维护。
(2)技术架构
采用当前流行的前后端分离架构开发,由用户层、UI层、微服务层、数据层等部分组成,为PC、H5等客户端用户提供服务。下图是系统的技术架构图:
(3)技术栈
微服务技术栈基于Spring Boot构建,采用Spring Cloud Alibaba微服务架构。
微服务基于SpringBoot开发,数据库连接池采用Druid,POJO构建采用Lombok,日志系统采用Log4j2,Guava工具类库,Mybatis Plus持久层接口实现,Sharding-jdbc分库分表组件,Swagger接口规范组件,XXL-job分布式任务调度组件,Sentinel限流组件等。
接入网关完成客户端认证、路由转发等功能,Dubbo RPC完成微服务远程调用,JWT提供前后端令牌管理方案。
(4)微服务部署架构
项目采用前后端分离设计,系统架构采用微服务,前端使用Vue,数据库使用MySQL。
二、微服务设计
1、项目需求
我们模拟一个服务调用的场景,方便后面学习微服务架构。这里使用电商项目中的商品、订单、用户为案例进行讲解。
注意:为了避免不同微服务项目使用相同的端口,这里规定如下:
shop-application: 57020
shop-product-service: 57021
shop-order-service: 57022
shop-user-service: 57023
2、微服务项目的组织
(1)微服务开发模式
在微服务架构中,一个完整的项目被拆分成很多独立的微服务,例如一个电商项目,可能分为商品管理、商家管理、用户管理、交易管理、SEO管理、App管理、财务管理、系统管理等很多微服务。
这些微服务都是一个个独立的项目,由不同的团队负责开发维护。不同的团队独立开发、独立维护、独立测试(看情况)、独立上线,这样可以有效提高项目的开发效率。
结合项目的实际情况,不同的团队甚至可以选择不同的技术栈,比如商品管理模块用 Java、交易管理可能用Go、门户网站可能用PHP等等,从微服务架构上来说,这些都是支持的,这也是微服务的优势之一,即同一系统不必拘泥于同一种语言,当然在具体实践中,还需要结合团队的技术栈以及语言的特性来选择。
(2)统一版本管理
理论上,在微服务架构中各个独立的微服务可以是不同语言实现的,像Eureka注册中心就是支持多种语言的,这样可以充分发挥各种语言的优势。如果是这样就没有必要从项目整体上进行版本管理了,我们也无需进行统一版本管理了。
但是在实际操作中,考虑到团队的技术栈,以及现有的技术生态等因素,大部分情况下我们不会在项目中掺杂其他语言进来,比如就用Java开发,相信大部分都是这么做的。
既然统一都使用Java语言开发,有一个需求就随之浮出水面,就是项目依赖统一管理。
大型的微服务项目分属不同的团队开发,每个团队维护好自己的项目,然后通过RPC或者HTTP的方式互相之间进行交互,这种情况下版本号可以交由各个团队自行维护,这样版本升级的时候就不必一起升级,可以各个团队独自完成,然后逐个升级。
但是这种方式又可能会带来另外一个问题,就是依赖版本的碎片化,在经过N多次迭代之后,可能会存在两个项目所依赖的微服务版本差异非常大。
因此,在实际操作中,有的团队会倾向于将项目版本统一管理。统一版本管理也很简单,就是基于Maven的父子工程就行了。
注意:基于父子项目中子项目是无法独立打包的,因为子项目之间可能存在依赖,所以只能从父项目中打包。
(3)微服务模块的类型
每个微服务都分为两个模块,即API模块和服务模块。API模块封装了对外提供的服务接口,服务模块提供了业务实现,这样服务之间依赖于API接口,而不会依赖于服务实现,可以实现不同微服务之间的解耦。
(4)微服务的命名规则
1、项目名由多个单词组成,则使用“-”分隔,如“shop-user”。
2、不同项目类型有不同后缀名,API模块后缀是“-api”,服务模块后缀是“-service”,应用模块后缀是“-application”,公共模块没有后缀。
3、项目的应用名称(即spring.application.name)和项目名称相同,这样便于在Nacos中注册和查找。
spring:
application:
name: @artifactId@
4、REST微服务的根路径都是 /。因为每个微服务的端口不同,所以可以通过端口来区分不同的微服务,以后在网关中可以为不同应用加URI前缀。
server:
servlet:
context-path: /
5、OpenFeign接口上使用项目名称作为服务名(@artifactId@)。
6、微服务的路由id都有后缀-route,即“@artifactId@-route”。
(5)微服务组织
下面是一个微服务项目,基于父子项目来组织所有微服务。
1、climb-common是公共模块,所有子项目就是一个普通Java项目,打包为Jar文件。
2、climb-merchant和climb-tenant是微服务,都分为API和Service两个子模块。
3、创建父项目
(1)SpringCloud项目
在父项目中统一了所有微服务的groupId和version。但是有个问题,就是父项目中如何引用SpringBoot依赖和SpringCloud依赖?
官方推荐直接使用SpringBoot的spring-boot-starter-parent作为<parent>,其中设置了构建环境,各种依赖包,以及常用Maven插件的配置。
官方推荐在<dependencyManagement>中以pom方式导入SpringCloud依赖。
<project>
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>spring-boot-starter-parent</artifactId>
<groupId>org.springframework.boot</groupId>
<version>2.5.2</version>
</parent>
<groupId>com.springclimb.web</groupId>
<artifactId>springboot-web-qickstart</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring.cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${alibaba.cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
(2)创建父项目
Spring Cloud Alibaba项目是基于Spring Cloud开发,而Spring Cloud项目是基于Spring Boot开发,都是使用Maven做项目管理工具。在实际开发中,一般都会创建一个Maven的Parent项目,包含所需的依赖管理,这样做可以极大的方便对jar包的统一管理。
这里使用Maven创建聚合项目。我们创建一个Maven项目作为父项目,然后创建多个Maven模块作为子项目,在每个Maven模块中作为一个微服务的功能实现。
这个父项目的pom.xml中有以下几点注意事项:
1、父项目中定义了自己的<groupId>和<version>,从而统一管理所有微服务的版本。
2、父项目使用官方推荐的spring-boot-starter-parent,其中包括了常规配置和常见依赖。
3、使用<properties>统一管理所有外部依赖的版本号。
4、使用<dependencies>引入项目中使用的公共依赖项。其中:
spring-cloud-starter-bootstrap用于加载bootstrap.yml。
spring-boot-configuration-processor用于IDEA中配置管理文件的编辑器。
spring-boot-starter-actuator和spring-boot-admin-starter-client用于监控SpringBoot应用。
spring-boot-starter-test引入单元测试。
lombok用于POJO的编写,mapstruct用于对象转换,hutool-all是一个工具包。
5、<dependencyManagement>用于统一管理所有依赖项。
spring-boot-dependencies引入SpringBoot依赖。
spring-cloud-dependencies引入官方的SpringCloud依赖。
spring-cloud-alibaba-dependencies引入SpringCloud alibaba依赖。
6、<pluginManagement>用于统一配置Maven插件。
父项目必须使用<pluginManagement>来配置spring-boot-maven-plugin,只有子项目使用SpringBoot方式打包时才会用<plugin>在引入spring-boot-maven-plugin。这样所有其它子项目就会默认使用maven-jar-plugin来打包。
maven-compiler-plugin中配置了在编译时lombok和mapstruct对源代码的修改兼容性。
spring-boot-maven-plugin中配置了SpringBoot打包插件。
<project>
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>spring-boot-starter-parent</artifactId>
<groupId>org.springframework.boot</groupId>
<version>2.5.2</version>
</parent>
<groupId>com.climbcloud.shop</groupId>
<artifactId>climbcloud-shop</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<!-- maven配置 -->
<!--这些属性可以不需要,在spring-boot-starter-parent中已配置好-->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<!-- SpringBoot -->
<spring-boot.version>2.5.2</spring-boot.version>
<spring-cloud.version>2020.0.3</spring-cloud.version>
<spring-cloud-alibaba.version>2021.1</spring-cloud-alibaba.version>
<spring-boot-admin.version>2.4.3</spring-boot-admin.version>
<!-- swagger的SpringDoc实现,支持OpenAPI3 -->
<springdoc.version>1.5.0</springdoc.version>
<!-- 数据库 -->
<mysql.version>8.0.22</mysql.version>
<druid.version>1.2.3</druid.version>
<mybatis-plus.version>3.4.3</mybatis-plus.version>
<mybatis-plus-generator.version>3.4.1</mybatis-plus-generator.version>
<velocity.version>1.7</velocity.version>
<!-- 工具库 -->
<!-- 工具库 -->
<mapstruct.version>1.4.2.Final</mapstruct.version>
<hutool.version>5.7.3</hutool.version>
</properties>
<dependencies>
<!--bootstrap 启动器-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<!--配置文件处理器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!--监控-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--监控客户端-->
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-client</artifactId>
<version>${spring-boot-admin.version}</version>
</dependency>
<!--测试依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- API文档生成 -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-common</artifactId>
</dependency>
<!-- 公共的工具库,避免到入引入 -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-jdk8</artifactId>
<version>${mapstruct.version}</version>
</dependency>
<!--Lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<!-- SpringCloud -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- REST API文档 -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>${springdoc.version}</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-security</artifactId>
<version>${springdoc.version}</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-common</artifactId>
<version>${springdoc.version}</version>
</dependency>
<!-- 数据源 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>${mybatis-plus-generator.version}</version>
</dependency>
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity</artifactId>
<version>${velocity.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<finalName>${project.name}</finalName>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<!-- mybatis-plus映射文件 -->
<include>**/*.xml</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<includes>
<include>**/application*.yml</include>
<include>**/application*.yaml</include>
<include>**/application*.properties</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory>
<excludes>
<exclude>**/application*.yml</exclude>
<exclude>**/application*.yaml</exclude>
<exclude>**/application*.properties</exclude>
</excludes>
</resource>
</resources>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</dependency>
</annotationProcessorPaths>
<compilerArgs>
<compilerArg>
-Amapstruct.defaultComponentModel=spring
</compilerArg>
</compilerArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<configuration>
<finalName>${project.build.finalName}</finalName>
<layers>
<enabled>true</enabled>
</layers>
<!-- spring-boot:run 解决控制台中的中文乱码 -->
<jvmArguments>-Dfile.encoding=UTF-8</jvmArguments>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
4、搭建微服务环境
(1)创建微服务
这里创建的微服务项目如下:
父项目climbcloud-shop用于统一配置整个项目的环境。
微服务shop-application对外提供服务,不会被别的微服务调用,所以没有API模块。
微服务shop-order、shop-product和shop-user都是内部微服务,可以被别的微服务所调用,所以都分为API和Service两个子项目。
(4)微服务shop-application
微服务shop-application对外提供服务,不会被别的微服务调用,所以没有API模块。
1、这个项目只有控制逻辑,业务逻辑是调用其它微服务来实现的。
2、vo包是与前端交换的数据对象,convert包是将vo对象转换为DTO对象。
3、ShopApplication是启动类。
@SpringBootApplication
public class ShopApplication {
public static void main(String[] args) {
SpringApplication.run(ShopApplication.class, args);
}
}
4、每个应用都会提供一个应用配置类。
微服务配置最好不要都放在应用主类上,而是放在这个单独的配置类上,这样使于维护。
@Configuration
public class ShopConfiguration implements WebMvcConfigurer {
}
5、bootstrap.yml提供微服务的配置。这里有微服务的应用名称,以及监控点配置。
spring:
application:
name: shop-application
main:
allow-bean-definition-overriding: true # 需要设置
# 健康监控:服务下线后自动删除注册实例
management:
endpoints:
web:
exposure:
include: "*"
6、pom.xml配置。
<project>
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.climbcloud.shop</groupId>
<artifactId>climbcloud-shop</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>shop-application</artifactId>
<dependencies>
<!-- Web环境 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
(5)微服务shop-product的API模块
这类项目为应用程序向调用方提供API,从而隐藏微服务的实现。
1、dto包存放与调用方交换的数据对象,feign包存放对外提供的接口。
2、API模块都是普通Java项目,这里使用默认的maven-compiler-plugin插件打包。
<project>
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.climbcloud.shop</groupId>
<artifactId>climbcloud-shop</artifactId>
<version>1.0-SNAPSHOT</version>
<!-- lookup parent from repository -->
<relativePath/>
</parent>
<artifactId>shop-product-api</artifactId>
<dependencies>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-common</artifactId>
</dependency>
</dependencies>
</project>
(6)微服务shop-product的Service模块
这是业务的真正实现地方。这个微服务项目实际就是一个SSM项目。
1、entity包存放持久对象,convert包用于将DTO对象转换为PO对象。
2、controller包中接口需要按照feign包中接口的要求开发。
3、ProductApplication是启动类。
@SpringBootApplication
public class ProductApplication {
public static void main(String[] args) {
SpringApplication.run(ProductApplication.class, args);
}
}
4、每个应用都会提供一个应用配置类。
微服务配置最好不要都放在应用主类上,而是放在这个单独的配置类上,这样使于维护。
@Configuration
public class ProductConfiguration implements WebMvcConfigurer {
}
5、bootstrap.yml提供微服务的配置。这里有微服务的应用名称,以及监控点配置。
spring:
application:
name: shop-product-service
main:
allow-bean-definition-overriding: true # 需要设置
# 健康监控:服务下线后自动删除注册实例
management:
endpoints:
web:
exposure:
include: "*"
6、pom.xml配置。注意:微服务使用spring-boot-maven-plugin来打包。
<project>
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.climbcloud.shop</groupId>
<artifactId>climbcloud-shop</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>shop-product-service</artifactId>
<dependencies>
<dependency>
<groupId>com.climbcloud.shop</groupId>
<artifactId>shop-product-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
5、数据库设计
(1)创建数据库
通常一个微服务对应于一个数据库。这里创建三个数据库,在数据库中分别创建商品表、订单表和用户表,这三张表都使用雪花算法来生成主键。
(2)创建数据表
创建数据库的注意事项:
1、表的命名“业务名称_表的作用”,不同业务表用不同前缀加以区分,如“sys_”等。
2、逻辑值必须使用is_xxx的方式命名,数据类型是tinyint(1表示是,0表示否)。
3、主键名为id,数据类型为bigint,值为雪花算法的值。
4、每个字段都应当用COMMENT解释该字段的含义,以便后期维护。
5、逻辑删除字段名是is_deleted,值为tinyint。
(3)用户库
创建用户库shop_user。数据表shop_user中user_name列有唯一性约束。
create table if not exists shop_user
(
id bigint not null comment '用户id',
user_name varchar(45) not null comment '用户名',
password varchar(45) null comment '密码',
telephone varchar(11) null comment '手机号',
photo varchar(100) null comment '像片',
birthday datetime null comment '生日',
is_deleted tinyint default 0 null comment '是否删除(0-正常,1-删除)',
constraint shop_user_id_uindex unique (id),
constraint shop_user_user_name_uindex unique (user_name)
);
alter table shop_user add primary key (id);
(4)商品库
创建商品库shop_product,其中有数据表shop_product。
create table if not exists shop_product
(
id bigint not null comment '商品id',
name varchar(45) null comment '商品名称',
price decimal(8,3) null comment '商品价格',
stock int null comment '库存数量',
is_deleted tinyint default 0 null comment '是否删除(0-正常,1-删除)',
constraint shop_product_id_uindex unique (id)
);
alter table shop_product add primary key (id);
(5)订单库
创建订单库shop_order,其中有数据表shop_order。
create table if not exists shop_order
(
id bigint not null comment '订单id',
user_id bigint null comment '用户id',
user_name varchar(45) null comment '用户名',
product_id bigint null comment '商品id',
product_name varchar(45) null comment '商品名称',
product_price decimal(8,3) null comment '商品单价',
number int null comment '购买数量',
is_deleted tinyint default 0 null comment '是否删除(0-正常,1-删除)',
constraint shop_order_id_uindex unique (id)
);
alter table shop_order add primary key (id);
三、公共项目开发
1、核心模块shop-common-rest
(1)创建核心模块
这个模块提供了REST微服务开发时的常用配置。
其中pom.xml配置如下:
<project>
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.climbcloud.shop</groupId>
<artifactId>shop-common</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>shop-common-rest</artifactId>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- REST API文档 -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
</dependency>
</dependencies>
</project>
(2)核心数据结构
1、自定义响应状态ResultCode
2、自定义响应类R<T>
3、分页类PageVO<T>
(3)统一异常处理
微服务系统是一个分布式系统,这些微服务之间调用时可能会抛出不同类型的异常,我们在程序中如何统一的处理这么多种类的异常呢?
1、统一异常类型
我们可以设计自己的异常体系,例如根异常为ApplicationException,然后为特殊情形(如ResourceNotFoundException)派生一些特定的子类。ApplicationException中用ResultCode来区分不同的异常类型,message记录异常消息,cause记录原始异常。
2、每个微服务内部使用传统异常处理方式
如果程序中需要通过异常类型进行区分异常情形,则保留这个异常类型,然后用try/catch等来处理。
所有不需要通过异常类型进行区分的异常情形,以及业务异常使用ApplicationException及子类来表示,然后用ResultCode表示不同的异常情形。如果底层中其它异常则需要转换为ApplicationException,这样程序中可以提供统一异常处理。
(4)日志配置文件
Spring-boot-starter-logging默认集成Logback,配置文件是resources/logback-spring.xml。本模块中提供了统一的日志配置文件logback-spring.xml,微服务开发时不用创建这个配置。
(5)Jackson日期转换
@Configuration
@ConditionalOnClass(ObjectMapper.class)
@AutoConfigureBefore(JacksonAutoConfiguration.class)
public class ClimbJacksonAutoConfiguration {
private String DEFAULT_DATETIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
private String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
private String DEFAULT_TIME_FORMAT = "HH:mm:ss";
/**
* Json序列化和反序列化转换器,用于转换Post请求体中的json,以及将返回对象序列化为响应json
* 注意:java.util.Date被Jackson的ObjectMapper对象默认支持的,通过spring.jackson.date-format来配置
*/
@Bean
public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
return builder -> builder.serializerByType(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN)))
.serializerByType(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
.serializerByType(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))
.deserializerByType(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN)))
.deserializerByType(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
.deserializerByType(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))
// 解决JavaScript无法表示Long类型的问题,因为Long超出了JavaScript中number类型的范围
.serializerByType(Long.class, ToStringSerializer.instance);
}
}
(6)Web通用配置
@Configuration
@ConditionalOnWebApplication(type = SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
public class ClimbWebMvcAutoConfiguration implements WebMvcConfigurer {
/**
* 请求参数中时间类型转换
* <ul>
* <li>HH:mm:ss -> LocalTime</li>
* <li>yyyy-MM-dd -> LocalDate</li>
* <li>yyyy-MM-dd HH:mm:ss -> LocalDateTime</li>
* </ul>
*/
@Override
public void addFormatters(FormatterRegistry registry) {
DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
// LocalTime转换器,用于转换RequestParam和PathVariable参数
registrar.setTimeFormatter(DateTimeFormatter.ofPattern(DatePattern.NORM_TIME_PATTERN));
// LocalDate转换器,用于转换RequestParam和PathVariable参数
registrar.setDateFormatter(DateTimeFormatter.ofPattern(DatePattern.NORM_DATE_PATTERN));
// Date和LocalDateTime转换器,用于转换RequestParam和PathVariable参数
registrar.setDateTimeFormatter(DateTimeFormatter.ofPattern(DatePattern.NORM_DATETIME_PATTERN));
registrar.registerFormatters(registry);
}
/**
* 全局异常处理
*/
@Bean
@ConditionalOnMissingBean
public ClimbRestExceptionHandler climbRestExceptionHandler() {
return new ClimbRestExceptionHandler();
}
/**
* 跨域访问
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
// 添加映射路径,“/**”表示允许所有路径进行跨域访问
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.exposedHeaders(HttpHeaders.SET_COOKIE).maxAge(3600L);
}
}
(7)Swagger配置
@Configuration
@ConditionalOnClass(OpenAPI.class)
public class SwaggerAutoConfig {
@Bean
public OpenAPI ShopOpenAPI() {
// 名字和创建的SecuritySchemes一致
SecurityRequirement sr = new SecurityRequirement().addList("bearerAuth");
List<SecurityRequirement> list = new ArrayList<>();
list.add(sr);
return new OpenAPI()
.info(new Info().title("Shop API")
.description("网上商城")
.version("v0.0.1")
.license(new License().name("Apache 2.0").url("http://climbcloud.com")))
.externalDocs(new ExternalDocumentation()
.description("Shop Wiki Documentation")
.url("https://climbcloud.com/docs"))
// 这是基于HTTP协议的bearer认证实现
.components(new Components().addSecuritySchemes("bearerAuth",
new SecurityScheme().type(SecurityScheme.Type.HTTP)
.scheme("bearer").bearerFormat("JWT")))
// 应用安全需要
.security(list);
}
}
(8)RestTemplate配置
微服务之间的远程调用在底层使用RestTemplate来实现,这也是OpenFeign调用的底层实现。RestTemplate默认依赖JDK提供http连接的能力(HttpURLConnection)。
@Configuration
@ConditionalOnClass(RestTemplate.class)
public class ClimbRestTemplateConfiguration {
@Bean
@ConditionalOnMissingBean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
//消息转换器列表
List<HttpMessageConverter<?>> messageConverters = restTemplate.getMessageConverters();
//配置消息转换器StringHttpMessageConverter,并设置utf‐8支持中文字符集
messageConverters.set(1, new StringHttpMessageConverter(StandardCharsets.UTF_8));
return restTemplate;
}
}
(9)SpringBoot自动配置
我们开发的模块中需要注册一些Bean对象。当这个模块被其它模块依赖时有一个问题,就是在其它模块中需要扫描这个模块中的Bean对象,这样每个模块中都要记得扫描这个模块中的Bean对象,程序就会变得很难维护,怎么解决这个问题呢?
Spring提供了自动配置功能,可以使用自动配置类来注册模块中的所有Bean对象。如果没有自动配置类,也可以直接将Bean对象注册在spring.factories中。
@EnableAutoConfiguration开启自动配置,默认加载配置文件META-INF/spring.factories。这个文件内容就是可以自动加载的一些配置类,例如:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.climbcloud.common.rest.config.ClimbJacksonAutoConfiguration,\
com.climbcloud.common.rest.config.ClimbWebMvcAutoConfiguration,\
com.climbcloud.common.rest.config.SwaggerAutoConfig,\
com.climbcloud.common.rest.config.ClimbRestTemplateConfiguration
2、mybatis模块shop-common-mybatis
(1)创建Mybatis模块
这个模块提供了Mybatis开发时的常用配置。
<project>
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.climbcloud.shop</groupId>
<artifactId>shop-common</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>shop-common-mybatis</artifactId>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<exclusions>
<exclusion>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-extension</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity</artifactId>
</dependency>
</dependencies>
</project>
(2)Mybatis-Plus封装
BaseEntity定义了数据表中的公共字段。
(3)AutoGenerator封装
这里将所有自动生成代码的参数都抽取出来,让生成代码成为一个函数调用。
注意:这里没有更改Mybatis-plus自已的代码模板。
public class MpGenerator {
public void generator(String projectPath, String projectName, String author, String url, String username, String password, String[] table_names, String[] tablePrefix, boolean hasSuperEntity, String[] superEntityColumns, String packageName, String moduleName) {
// 1. 创建代码生成器
AutoGenerator mpg = new AutoGenerator();
// 2. 全局配置
GlobalConfig gc = new GlobalConfig();
gc.setOutputDir(projectPath + projectName + "/src/main/java");
gc.setAuthor(author);
gc.setFileOverride(true);
gc.setOpen(false);
gc.setBaseResultMap(true);
gc.setBaseColumnList(true);
gc.setIdType(IdType.ASSIGN_ID);
gc.setServiceName("%sService");
gc.setServiceImplName("%sServiceImpl");
gc.setSwagger2(false);
mpg.setGlobalConfig(gc);
// 3. 数据库配置
DataSourceConfig dsc = new DataSourceConfig();
dsc.setDriverName("com.mysql.cj.jdbc.Driver");
dsc.setUrl(url);
dsc.setUsername(username);
dsc.setPassword(password);
dsc.setDbType(DbType.MYSQL);
// 设置mysql中的字段类型和java中属性类型的对应关系
dsc.setTypeConvert(new MySqlTypeConvertCustom());
mpg.setDataSource(dsc);
// 4. 策略配置
StrategyConfig strategy = new StrategyConfig();
// 设置表名
strategy.setInclude(table_names);
// 表名前缀,映射到实体名称去掉
strategy.setTablePrefix(tablePrefix);
strategy.setNaming(NamingStrategy.underline_to_camel);
strategy.setColumnNaming(NamingStrategy.underline_to_camel);
// 实体类使用lombok注解
strategy.setEntityLombokModel(true);
if(hasSuperEntity) {
strategy.setSuperEntityClass("com.climbcloud.common.mybatis.base.BaseEntity");
// 写于父类中的公共字段
strategy.setSuperEntityColumns(superEntityColumns);
}
// 生成REST控制器
strategy.setRestControllerStyle(true);
//url中驼峰转连字符
strategy.setControllerMappingHyphenStyle(true);
// 逻辑删除字段名
strategy.setLogicDeleteFieldName("is_deleted");
// Boolean字段是否移除is前缀
strategy.setEntityBooleanColumnRemoveIsPrefix(true);
mpg.setStrategy(strategy);
// 5. 包配置
PackageConfig pc = new PackageConfig();
pc.setParent(packageName);
pc.setModuleName(moduleName);
pc.setEntity("entity");
pc.setMapper("mapper");
pc.setXml("mapper");
mpg.setPackageInfo(pc);
mpg.execute();
}
}
/*
MyBatis-Plus 3.3.1中自动生成代码tinyint(1)无法自动转换为Boolean的解决办法。
1.在测试类中新建一个类MySqlTypeConvertCustom,继承MySqlTypeConvert并实现ITypeConvert后覆盖processTypeConvert方法。
2.在generator中使用自己创建的类的对象。
*/
class MySqlTypeConvertCustom extends MySqlTypeConvert implements ITypeConvert {
@Override
public IColumnType processTypeConvert(GlobalConfig globalConfig, String fieldType) {
String t = fieldType.toLowerCase();
if (t.contains("tinyint")) {
return DbColumnType.BOOLEAN;
}
return super.processTypeConvert(globalConfig, fieldType);
}
}
(4)自动配置类
为了简化微服务的配置,这里使用自动配置类在微服务中自动配置Mybatis-plus。
@Configuration
@ConditionalOnClass(MybatisPlusInterceptor.class)
@AutoConfigureBefore(MybatisPlusAutoConfiguration.class)
public class ClimbMybatisPlusAutoConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 乐观锁
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
// 分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
(5)SpringBoot自动配置
@EnableAutoConfiguration开启自动配置,默认加载配置文件META-INF/spring.factories。这个文件内容就是可以自动加载的一些配置类,例如:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.climbcloud.common.mybatis.config.ClimbMybatisPlusAutoConfig
四、商品微服务开发
1、配置商品微服务
这是一个典型的SSM程序,对外提供RESTful风格的服务。
(1)引入依赖
在父项目的pom.xml中已经引入了基础依赖,如spring-cloud-starter-bootstrap、spring-boot-configuration-processor、spring-boot-starter-actuator、spring-boot-starter-test、springdoc-openapi-common,所以各个子模块中就不必再引入了。
这里在pom.xml中引入Web和Mybatis依赖,重点是引入shop-common-rest和shop-common-mybatis,程序运行时会自动加载其中的自动配置类。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.climbcloud.shop</groupId>
<artifactId>climbcloud-shop</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>shop-product-service</artifactId>
<dependencies>
<dependency>
<groupId>com.climbcloud.common</groupId>
<artifactId>shop-common-rest</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.climbcloud.common</groupId>
<artifactId>shop-common-mybatis</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.climbcloud.shop</groupId>
<artifactId>shop-product-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- Web依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- REST文档 -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
</dependency>
<!-- mybatis-plus不要与mybatis同时被依赖 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<!--nacos客户端-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
(2)配置项目
在bootstrap.yml中配置数据库连接和Mybatis。
注意:bootstrap.yml在application.yml之前被加载。
server:
port: 57021
spring:
application:
name: shop-product-service
main:
allow-bean-definition-overriding: true # 需要设置
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/shop-product?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false
username: root
password: 123456
type: com.alibaba.druid.pool.DruidDataSource
mvc:
format:
# http数据的日期格式
date: yyyy-MM-dd HH:mm:ss
# 出现错误时直接抛出异常,交由SpringMVC处理
throw-exception-if-no-handler-found: true
web:
resources:
# SpringMVC不要为资源建立 /** 的映射配置,否则请求都被处理就没有404
add-mappings: false
jackson:
# 只控制java.util.Date的序列化format
date-format: yyyy-MM-dd HH:mm:ss
locale: zh # 当地时区
time-zone: GMT+8
default-property-inclusion: NON_NULL # Google建议属性为NULL则不序列化
mybatis-plus:
# 别名配置,多个package用逗号或者分号分隔
typeAliasesPackage: com.climbcloud.**.entity
# mapperLocations是mapper文件位置
mapper-locations: classpath:com/climbcloud/**/mapper/*.xml
configuration:
#配置JdbcTypeForNull
jdbc-type-for-null: 'null'
# 显示SQL语句
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 健康监控:服务下线后自动删除注册实例
management:
endpoints:
web:
exposure:
include: "*"
(3)应用配置类
在climb-common-core中提供了数据转换和封装,跨域访问,以及统一异常处理。这里只需要关心应用本身的配置。
在配置类ProductConfiguration中提供应用程序配置,这里是Mybatis的包扫描路径。
@Configuration
@MapperScan(basePackages = {"com.climbcloud.**.mapper"})
public class ProductConfiguration implements WebMvcConfigurer {
}
2、业务代码开发
(1)生成代码
1、创建一个工具类并生成代码。
public class CodeGenerator {
public static void main(String[] args) {
// 工程所在目录
String projectPath = System.getProperty("user.dir");
String projectName = "/shop-product/shop-product-service";
String author = "filteraid";
// 目前支持MySQL
String url = "jdbc:mysql://localhost:3306/shop-product?useUnicode=true&characterEncoding=UTF8&serverTimezone=GMT%2b8&useSSL=false&allowPublicKeyRetrieval=true";
String username = "root";
String password = "123456";
// 按驼峰命名的数据表名
String[] tableNames = new String[]{ "shop_product" };
// 去掉前缀后按驼峰命名来生成类名
String [] tablePrefix = new String[] { "shop" };
// 是否有父实体
boolean hasSuperEntity = Boolean.TRUE;
String[] superEntityColumns = {"id"};
// 包配置
String packageName = "com.climbcloud";
String moduleName = "product";
MpGenerator mpGenerator = new MpGenerator();
mpGenerator.generator(projectPath, projectName, author, url, username, password, tableNames, tablePrefix, hasSuperEntity,
superEntityColumns, packageName, moduleName);
}
}
2、下面是生成的代码。
(2)业务开发
服务实现类继承了BaseServiceImpl,父类中可以定义所有服务类的公共方法。
@Service
public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> implements ProductService {
}
(3)编写查询服务
@RestController
@RequestMapping("/products")
public class ProductController {
@Autowired
private ProductService productService;
@ResponseStatus(HttpStatus.OK)
@GetMapping("/{id}")
public R<Product> getById(@PathVariable("id") Long id) {
Product product = productService.getById(id);
return R.success().data(product);
}
}
(4)事务问题
1、在数据库中添加一条id为1的记录。
2、在Postman中访问“http://localhost:57021/products/1”。
3、在控制台中输出了下面的信息,这是服务方法没有被Spring事务所管理。
JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@582e6650] will not be managed by Spring
4、在ProductServiceImpl上加了@Transactional注解还是有同样的问题。
@Service
@Transactional
public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> implements ProductService {
}
5、ServiceImpl中的getById方法是IService接口中的默认方法。
我们发现ServiceImpl中所有read和delete方法都没有@Transactional注解,但是有save和update方法有@Transactional注解。
这里@Transactional只是作用于ProductServiceImpl中定义的方法,所以getById方法是不会自己开启Spring事务的,只能加入到Spring事务中。
default T getById(Serializable id) {
return getBaseMapper().selectById(id);
}
6、我们可以在ProductServiceImpl中重写getById方法,并加上@Transactional注解。
@Service
public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> implements ProductService {
@Transactional(rollbackFor = Exception.class)
@Override
public Product getById(Serializable id) {
return getBaseMapper().selectById(id);
}
}
在控制台中输出日志里可以看到Spring事务起作用了。
Creating a new SqlSession
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@ed63ac1]
JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@4cb6cd6d] will be managed by Spring
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@ed63ac1]
Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@ed63ac1]
7、理解@Transactional(rollbackFor = Exception.class)
默认情况下,只在抛出RuntimeException或其子类的实例时才回滚Spring事务。也就是说,从事务方法中抛出的Checked exceptions将不会回滚Spring事务。
(1)支持Checked exceptions回滚:在方法前加上:
@Transactional(rollbackFor=Exception.class)
(2)让unchecked例外不回滚:
@Transactional(notRollbackFor=RuntimeException.class)
(3)不需要事务管理的(只查询的)方法:
@Transactional(propagation=Propagation.NOT_SUPPORTED)
(5)访问API文档
在浏览器中使用“http://localhost:57021/swagger-ui.html”访问API文档。
3、服务调用方式
无论微服务或SOA,都有服务间的远程调用。那么服务间的远程调用方式有哪些呢?
(1)RPC和HTTP
RPC:Remote Produce Call远程过程调用。这种方式是自定义数据格式,基于原生TCP通信,因此速度快,效率高。现在热门的Dubbo就是RPC的典型代表。
Http:其实是一种基于TCP的网络传输协议,规定了数据传输的格式。现在浏览器与服务端通信基本都是采用Http协议,也可以用来进行远程服务调用。缺点是消息封装臃肿,优势是对服务的提供和调用方没有任何技术限定,自由灵活,更符合微服务理念。
这里微服务采用RESTful风格对外提供API,服务与服务之间采用HTTP协议传输数据。
(2)微服务之间的数据传输
微服务之间的远程调用会传递数据。以前Web应用中直接使用实体类(PO),微服务环境中使用了三种数据对象,即实体类(PO)、数据传输对象(DTO)和值对象(VO)。
实体类(PO)只用于持久层的数据操作,只在服务内部使用,一般不会让外部依赖。
值对象(VO)只用于与前端交互,用于接收前端请求及响应前端数据。
数据传输对象(DTO)用于微服务之间的数据交互,用于服务层的传入及响应数据。
4、远程接口开发
(1)API模块
单体应用中Order类可以直接使用Product类,但是在微服务shop-order-service中直接引入了微服务shop-product-service。这种依赖方式是不正确的,因为两个微服务耦合太紧了。为了解耦两个微服务间的依赖,我们为微服务shop-product-service创建了一个API模块,其中定义了其它微服务调用微服务shop-product-service所需的全部内容。
我们看到在模块shop-product-api中有dto和feign包,其中dto包存放DTO对象,feign包存放对外提供的访问接口。在分布式系统中,通常在系统内部使用PO来表示数据,而远程交互时使用可序列化的DTO对象。
(2)DTO
DTO类不要继承实体类,因为实体类可能有关联属性,通常不需传输这些关联属性。
@Data
@NoArgsConstructor
@AllArgsConstructor
@Schema(description="Product对象")
public class ProductDto implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "商品id")
private Long id;
@Schema(description = "商品名称")
private String name;
@Schema(description = "商品价格")
private BigDecimal price;
@Schema(description = "库存数量")
private Integer stock;
@Schema(description = "删除标识")
private Boolean deleted;
}
(3)更改API接口
在微服务shop-product-service中返回结果时,需要将PO转换为DTO。这里在ProductController中将queryProductById方法的返回类型改为R<ProductDto>。
@RestController
@RequestMapping("/products")
public class ProductController {
@Autowired
private ProductService productService;
@ResponseStatus(HttpStatus.OK)
@GetMapping("/{id}")
public R<ProductDto> getById(@PathVariable("id") Long id) {
Product product = productService.getById(id);
// 将Product转为ProductDto
return R.success().data(product);
}
}
(4)MapStruct
MapStruct是一个对象与对象之间的属性映射工具。只要定义一个Mapper接口,就会自动实现这个映射接口,避免了复杂繁琐的映射实现。
注意:MapStruct默认按照属性名进行匹配,然后将源对象中属性值赋给目标对象。
/**
* 定义dto和entity之间的转换规则
*/
@Mapper
public interface ProductConvert {
/**
* 转换类实例
*/
ProductConvert INSTANCE = Mappers.getMapper(ProductConvert.class);
/**
* 把dto转换成entity
*/
Product dto2entity(ProductDto productDTO);
/**
* 把entity转换成dto
* 这里定制了两个对象的转换规则
*/
@Mappings({
@Mapping(source="id", target="id"),
@Mapping(source="name", target="name"),
@Mapping(source="price", target="price"),
@Mapping(source="stock", target="stock")
//@Mapping(source="date", target="date",dateFormat="yyyy-MM-dd HH:mm:ss")
})
ProductDto entity2dto(Product product);
/**
* list之间也可以转换,把entity的List转成MerchantDTO list
*/
List<ProductDto> entityList2dtoList(List<Product> products);
/**
* list之间也可以转换,把的MerchantDTO List转成entity list
*/
List<Product> dtoList2entityList(List<ProductDto> productDtos);
public static void main(String[] args) {
// 将dto转成entity
Product product = new Product();
product.setId(1L);
product.setName("abc");
System.out.println(product);
ProductDto productDto = ProductConvert.INSTANCE.entity2dto(product);
System.out.println(productDto);
// 将entity转成dto
productDto.setName("商户名称");
Product product1 = ProductConvert.INSTANCE.dto2entity(productDto);
System.out.println(product1);
// 定义的list
List entityList = new ArrayList();
entityList.add(product);
// 将lIST转成包含dto的list
List list = ProductConvert.INSTANCE.entityList2dtoList(entityList);
System.out.println(list);
}
}
(5)应用MapStruct和测试
1、使用MapStruct将Product转换为ProductDto。
@RestController
@RequestMapping("/products")
public class ProductController {
@Autowired
private ProductService productService;
@ResponseStatus(HttpStatus.OK)
@GetMapping("/{id}")
public R<ProductDto> getById(@PathVariable("id") Long id) {
Product product = productService.getById(id);
// 将Product转为ProductDto
ProductDto productDto = ProductConvert.INSTANCE.entity2dto(product);
return R.success().data(productDto);
}
}
2、启动程序,在Postman中访问“http://localhost:57021/products/1”。
5、实现CRUD
(1)控制类
@RestController
@RequestMapping("/products")
public class ProductController {
@Autowired
private ProductService productService;
@ResponseStatus(HttpStatus.OK)
@GetMapping("/{id}")
public R<ProductDto> getById(@PathVariable("id") Long id) {
Product product = productService.getById(id);
// 将Product转为ProductDto
ProductDto productDto = ProductConvert.INSTANCE.entity2dto(product);
return R.success().data(productDto);
}
@ResponseStatus(HttpStatus.OK)
@GetMapping("")
public R<List<ProductDto>> listAll() {
List<Product> productList = productService.list();
// 将product转为ProductDto
List<ProductDto> productDtoList = ProductConvert.INSTANCE.entityList2dtoList(productList);
return R.success().data(productDtoList).message("获取商品列表成功");
}
@ResponseStatus(HttpStatus.OK)
@GetMapping(value = "", params = {"page"})
public R<PageVo<ProductDto>> listPage(@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "2") Integer size) {
IPage<Product> pageParam = new Page<>(page, size);
IPage<Product> pageModel = productService.page(pageParam);
// 将product转为ProductDto
List<ProductDto> productDtoList = ProductConvert.INSTANCE.entityList2dtoList(pageModel.getRecords());
PageVo<ProductDto> pageVo = new PageVo(page, size, (int) pageModel.getTotal(), productDtoList);
return R.success().data(pageVo);
}
@ResponseStatus(HttpStatus.CREATED)
@PostMapping("")
public R save(@RequestBody ProductDto productDto) {
// 将ProductDto转为Product
Product product = ProductConvert.INSTANCE.dto2entity(productDto);
boolean result = productService.save(product);
if (result) {
return R.success().message("保存成功");
} else {
throw new ApplicationException("保存失败");
}
}
@ResponseStatus(HttpStatus.CREATED)
@PutMapping("/{id}")
public R updateById(@PathVariable Long id, @RequestBody ProductDto productDto) {
productDto.setId(id);
// 将ProductDto转为Product
Product product = ProductConvert.INSTANCE.dto2entity(productDto);
boolean result = productService.updateById(product);
if (result) {
return R.success().message("修改成功");
} else {
throw new ResourceNotFoundException("数据不存在");
}
}
// 逻辑数据删除,初始值在数据库中用默认值表示
@ResponseStatus(HttpStatus.OK)
@DeleteMapping("/{id}")
public R removeById(@PathVariable Long id) {
// 将ProductDto转为Product
boolean result = productService.removeById(id);
if (result) {
return R.success().message("删除成功");
} else {
throw new ResourceNotFoundException("数据不存在");
}
}
}