文章内容来自 《springcloud微服务架构开发实战》 董超 胡炽维
构建微服务架构所需要的服务治理(Eureka)、客户端负载均衡(Ribbon)、微服务容错与降级处理(Hystrix)、微服务API统一网关(Zuul)、分布式配置中心(Config)、微服务调用链追踪(Sleuth)、微服务消息驱动开发(Stream)及微服务安全(OAuth及JWT)
大型分布式系统具有复杂性、隐匿性、配合性和易变性四大难题
微服务解决了以往单体架构系统构建的困境。
微服务架构:模块化;
微服务架构从结构上来看就是将一个应用拆分成多个松耦合的服务,这些服务之间通过某种协议(REST、RPC等)进行互相协作,完成原单体架构下的业务功能。
对于微服务来说,其中一个关键点就是各服务之间的松耦合,各服务之间通过一种“标准”的协议进行沟通
服务是一个可以独立运行、提供范围有限的功能(可以是业务功能,也有可能是非业务功能)的组件。
在《架构即未来》(英文名称《The Art ofScalability》)一书中提出了一个可扩展模型:AFK扩展立方体(Scaliability Cube),将应用抽象总结出可扩展的三个维度:产品、流程和团队;
·X轴:X轴的扩展指的是服务水平复制,也就是我们可以运行多个应用实例,然后再做一个集群负载均衡。这种方式的扩展在单体应用中可以说非常典型,在系统应用性能提升中往往能够快速见效。
·Z轴:Z轴的扩展可以理解为是基于数据分区的扩展,也就是我们可以运行多个应用实例,但与X轴的扩展不同的一点是每个应用实例仅用来处理部分数据。在其之前没有负载均衡器,取而代之的是一个路由,该路由根据请求中的参数或某项属性(比如用户ID、订单ID等)将请求转发到不同的应用实例中进行处理。
·Y轴:Y轴的扩展就是接下来的微服务的功能拆分模式。基于Y轴的扩展思路可以将单体应用在业务层面上进行拆分,形成多个微服务。
2 微服务架构的优点
松耦合(小服务集合),抽象(每个微服务对其数据具有绝对控制权),独立(每个微服务都可以在不影响其他微服务的情况下进行编译、打包和部署),应对用户需求的多样性(在某个模块优化并快速部署),更高可用性和弹性(去中心化,当某个微服务出现问题时只需要将其下线即可,其他同类型的微服务将承担其功能)
3 微服务架构的缺点
可用性降低:微服务之间的远程调用不稳定,可能造成“雪崩效应”;
处理分布式事务棘手:一个请求涉及多个微服务,如何保证数据一致性;传统开发通常会使用两阶段提交的解决方案。
全能对象(God Classes)阻止业务拆分:
学习难度曲线加大,组织架构变更等。
微服务架构设计
步骤:一,应用中关键的需求定义出来;第二步,识别出应用中包含的所有服务;第三步,将第一步所定义出的关键需求作为架构需求的场景来描述服务之间如何进行协作。
第三步很像单体架构下我们所做的系统高层架构设计,通过高层架构设计会识别并定义出各个业务领域模型,这些业务领域模型包含了业务对象的关键操作流程,通过这些业务领域模型就可以辅助我们规划出整个应用架构,即各模块之间的协作关系。
通过业务逻辑将核心微服务识别出来,围绕核心服务把相关联的服务都定义出来,并可以对这些服务进行分组、合并处理,最终完整定义出应用的一系列微服务。
定义各个微服务之间协作关系最有效的方式就是根据每一个业务进行分析。有些业务场景可能只需要某一个服务就可以完成,有些业务场景则可能需要两个或多个服务才可以。这些协作可能是实时同步的,也可能是异步执行,我们可以根据这些具体需求来确定使用何种方式进行交互(是使用REST、RPC,还是消息)。
还有一个需要我们第一时间去考虑的问题就是用户的服务请求最初是由哪个服务承担的。
1 微服务粒度
先专注于各个服务之间的交互,先把它们划分成粗颗粒度的服务,然后随着系统的升级和功能的提升,再将这些粗颗粒度的服务逐渐细化,形成更为合理的微服务粒度。
衡量是否合适:
粒度粗的标志:1)该微服务一定承担了太多的职责,往往在服务中塞了过多的业务逻辑和业务规则;2)拥有众多数据的管理权限
粒度细的标志:每一个微服务几乎都需要和其他的微服务进行沟通,一个外部请求需要经过太多的微服务才能够完成处理。
.2 微服务拆分原则
·单一职责原则(SRP)
避免有多于一个原因来导致这个类的变化,要保持微服务的业务单一性
·共同封闭原则(CCP)
当需要修改某项业务时,我们需要将修改的范围限制在同一个包内,而不是遍布在很多包中。通过使用共同封闭原则可以将那些在业务上联系紧密,由于同一个原因而改变的服务组织在一个微服务中。
3 微服务自治原则
只有该微服务可以对这些数据进行操作,你构建,你运行
4 微服务交互原则
·使用REST协议:使用HTTP作为服务的调用协议,并在服务处理上使用HTTP标准动词(GET、PUT、POST和DELETE)。
·使用URI表达:服务端点的URI应该能够清晰表达出我们所要解决的问题、提供的方法、相应资源信息及资源之间的关联关系。
·使用JSON数据格式:JSON作为轻量级数据格式协议,及自带的序列化和反序列化机制,几乎已经成为通信中的数据标准协议,并且对于前端开发来说非常容易使用与整合。
·使用HTTP标准状态码:
5 微服务架构迁移
使用Martin Fowler提出绞杀(Strangler)模式。
不应使用微服务架构的情形
·构建分布式架构非常吃力时;
·服务器蔓延时;
·采用小型应用、快速产品原型时;
·对数据事务的一致性有一定要求时。
-微服务意味着更为复杂的服务编排、服务治理及服务运维
-对于大多数微服务部署来讲,每个微服务都是部署在一台服务器或虚拟容器中
-微服务架构的诞生是为了解决可复用,并且需要快速扩展的大型应用的问题
-一个业务往往需要多个微服务协作才能完成,并且对数据库事务要求也不是很严格。分布式事务管理始终是分布式架构开发的头等难题。假如开发者的应用对数据事务的一致性有要求,或者数据需要进行聚合等复杂处理,那么就需要仔细审视在应用中引入微服务架构开发的合理性了。对微服务来说,如果需要聚合多种数据,就意味着需要更多的微服务协作处理,这会造成服务效率的不可预测性,甚至会成为系统应用的一个瓶颈。
Spring Boot通过提供一系列名称以spring-boot-starter开头的启动器帮助我们简化项目的依赖。比如,当我们在项目中添加了spring-boot-starter-web时,Spring Boot就自动将相关依赖,如spring-mvc、jackson-json、tomcat等引入,从而将项目变成一个Web项目。
大家可以到这里(http://t.cn/Rn30Zr7)查看全部的Spring Boot启动器列表。在开发时我们常用的启动器有以下几个:
·spring-boot-starter-web, Web应用开发。
·spring-boot-starter-logging,日志处理;
·spring-boot-starter-jpa,数据存储管理;
·spring-boot-starter-security,安全管理;
·spring-boot-starter-actuator,应用监控;
·spring-boot-srarter-data-redis, Redis数据库集成;
·spring-boot-starter-amqp,消息中间件集成。
项目开展步骤:(1)使用Spring Boot快速构建一个最基础的应用框架,并且使用Maven脚本来管理和构建。(2)在该应用框架基础上构建一个三层应用架构的电子商城应用示例。这个示例又可以分为以下几个步骤来完成:① 通过分析电子商城需求设计出我们的业务实体对象。② 基于Spring Data JPA完成数据的存储处理,并创建数据库初始化脚本。③ 完成业务逻辑的设计与开发。④ 采用REST完成对外API的开发,这样可以通过多种方式访问到应用功能。(3)完成整个应用并打包运行示例项目。
源码:https://github.com/cd826/springcloud-demo
maven讲解:http://t.cn/zHwU8G4
编写maven脚本pom.xml文件:
父项目,本项目信息版本,所有项目的编码,项目Jdk版本,项目使用的第三方包的版本,同一在一个地方声明版本号,使用的springboot starters,guava提供缓存集合等方便工具类,maven打包版本,工程打包结果jar等
编写应用引导类:
@SpringBootApplication,对该类所属目录下的所有子包进行扫描并根据Spring Boot的自动配置机制进行配置。@ComponentScan注解中可以配置需要扫描包的位置。自此,在项目启动时构建一个Spring容器,并返回一个ApplicationContext对象
编写配置文件:
通常会有一个或多个配置文件,在配置文件中会存放应用所需的各项配置,如数据源、日志、启动端口等。properties,yml两种格式
运行:
mvn clean package命令将项目打包成一个Fat Jar,生产环境”java -jar 包名“进行运行
Fat Jar:不仅仅包含本项目中源码所编译生成的Java类文件,还会包含项目所依赖的第三方库及有关项目启动的相关信息。jar文件目录有项目代码(BOOT-INF/classes),项目依赖包(BOOT-INF/lib),应用启动相关文件(org/springframework/boot/loader).
spring-boot-starter-web默认依赖了spring-boot-starter-tomcat,会在启动时启动内嵌的tomcat容器,可以在pom.xml中配置jetty,增加spring-boot-starter-jetty即可。
三层应用架构:
三层应用:客户端UI层:用于客户交互的Web,H5,App等;应用层:业务规则指定和业务流程实现,用于交互及数据存储处理;存储层:持久化;
三层架构:业务逻辑层:定义业务领域对象Domain业务逻辑的具体实现Service;接口层(API层):Controller,使用REST方式提供API接口;数据接口层(DAO):定义为Repository。该层开发时往往会使用O/R Mapping技术,包含对非关系型数据及文件或云存储的处理.
2 设计领域对象
《领域驱动设计:软件核心复杂性应对之道》([美] Eric Evans)
清晰地识别出这些业务领域对象,以及它们之间如何交互及关联关系,是核心。对应一张数据表;
MoreObjects.toStringHelper(this).add("“id”,getId()).add(“name”,getName()).toString();
或可以借助commons-lang工具包中的ToStringBuilder通过反射机制来生成
ToStringBuilder.reflectToString(this);-属性会全部输出
3 实现数据管理(ORM解决方案)
Java持久性规范JPA(Java Persistence API),JPA是数据存储标准接口定义,有许多实现方式。将我们的业务与具体所要存储的数据库解耦,不需要为不同的数据库编写不同的处理方法,从而方便在多种数据库之间进行切换。spring-boot-starter-data-jpa。
添加数据库依赖->配置JPA和数据源部分
业务领域对象与数据库之间映射关系:通过注解
·@Entity:一个需要注意的地方就是在该类中必须有一个空的构造函数。当从数据库中加载对象时,JPA就会通过该构造函数来实例化对象,并将数据库中的值赋值给所构建的示例,若不小心覆盖会报错。·@Id:用来注解数据库主键,并且通过@GeneratedValue告诉JPA自动生成主键的值,默认情况下会使用数据库自增方式。也可以通过下面的注解来指定主键的生成方式。·实体关联关系:@OneToOne、@OneToMany、@ManyToOne和@ManyToMany·@Table:业务实体类上,自定义实体类会映射到数据库具体哪张表上。·@Column:通常我们在开发时很少使用该注解,JPA会自动将实体中的字段按照默认规则与数据库表进行一一映射,只有当默认的规则满足不了时才需要使用该注解自定义字段映射关系。
Hibernate数据库表映射规则及配置方式:
实体到物理数据库表的映射采用驼峰命名法,比如,User实体类默认映射到的数据库表表名为user,字段loginName,则默认会映射到数据表中的login_name字段上。
更改数据库表映射规则时,在项目配置中增加properties. hibernate.physical_naming_strategy属性。可以通过@Table注解来更改实体所要映射到的数据库表,通过@Column来更改字段映射规则。
最后就可以借助JPA来实现数据存储层代码了。通常,数据存储层会统一存放到一个命名为repository的包中,实现 JpaRepository
SpringData还提供了自然语义的数据访问处理机制。findTop10OrderByJoinDateDesc的方法,缺点方法名太长。Spring Data还提供了**@Query**注解,可以直接在方法中声明该方法查询时所使用的查询语句,所声明的查询语句使用JPQL语言。
还可以使用Spring Data的扩展机制,为Repository声明指定一个扩展接口并提供相应的实现类,定义UserRepositoryEx,并继承实现UserRepositoryImpl,不需要@Component,需要织入@PersistenceContext EntityManager entityManager,调用其createQuery(“from user”),seTMaxResults,query.getResultList()等;最后,UserRepository 继承JpaRepository,UserRepositoryEx{};扩展机制注意命名即可;
说明:扩展数据访问接口类的名称通常为主数据访问接口类增加一个Ex后缀,而相应实现类的名称则是为主数据访问接口类增加一个Impl后缀,需要注意的是,使用Ex只是一个默认约定,可以在项目中随便定义这个后缀,而实现类的Impl后缀是不可以随意定义的,但是可以在配置中通过repository-impl-postfix属性设置。
此外,当所继承的Repository中有些接口方法不需要时,那么可以在接口中重新声明需要的方法即可。这样SpringData在动态生成时就只会生成所重新声明的方法,而基类中所定义的方法则不会暴露。
对于一些复杂的业务来说,数据存储是我们需要构建一个数据模型来适应数据存储的需要,示例中将数据模型和业务领域对象合二为一了,在业务实体对象上增加了@Entity,@Table;
编写业务逻辑层
interface UserService{} ,class UserServiceImpl 我们在保存/更新功能中引入了一个新的类UserDto,也就是数据传输对象(Data TransferObject,简称DTO),用来处理跨进程或网络传输数据聚合容器。一般在该对象中只包含数据属性,而不包含任何业务逻辑。在这里及后续的示例中都会使用DTO对象作为前、后端分离时的数据传输。
编写RESTful API
RESTful API,一方面REST是业界非常成熟的Web服务标准之一;另一方面通过REST可以支持多种应用客户端,如Web、H5、App等都可以进行访问与交互。我们可以使用curl或Postman等工具直接访问所提供的RESTful API接口
Postman工具,读者可以到https://www.getpostman.com/上下载并了解该工具的使用方法。
@RestController注解时,该类中所有使用了@RequestMapping的方法就会返回响应体(response body),如果使用的是@Controller注解,则是会将HTML部分的代码也一起返回给调用者,也可以在使用@Controller注解的方法上同时增加@ResponseBody来达到同样的目的。
@RequestMapping两个简化的注解@GetMapping和@PostMapping,方法继承类得路径;
在编写RESTful API时还需要注意一点,当方法的返回值是一个对象时,Spring MVC就会默认使用JSON序列化方法将对象序列化为一个JSON格式的字符串返回,假如方法中返回的是基础数据类型(如boolean、int等)则无法进行序列化,会造成错误。所以在使用@RestController时需要注意返回值,最好的方式就是对返回值进行统一包装,这样方便前端进行处理。
@Api、@ApiOperation、@ApiImplicitParams属于Swagger。Swagger的目标是为REST API定义一个标准的、与语言无关的接口,使开发者在无源码或者没有API文档的情况下可以发现和理解系统所提供的各种服务。
Swagger所开发的API可根据上述注解自动生成API文档。如果要让User Endpoint代码能够通过编译,还需要在pom.xml文件中添加Swagger的依赖springfox-swagger-ui,springfox-swagger2;
还需要在项目webapp目录下创建一个swagger目录,并将从https://github.com/swagger-api/swagger-ui下载的UI代码解压到该目录下。在浏览器中通过地址http://localhost:8080/swagger/index.html访问到所生成的API文档。
https://swagger.io/
数据库初始化:
在真实生产环境中一般会采用手动方式完成,因此需要编写数据库表创建脚本、数据初始化脚本。在开发过程中可以使用Hibernate根据实体类自动创建数据库。
Spring Boot框架所提供的Spring JDBC初始化DataSource特性,是在启动系统时检测classpath根目录下是否有schema.sql和data.sql脚本文件,如果存在这两个脚本文件存在或者其中一个,将会尝试加载并执行该脚本。首先使用schema.sql创建数据库表,然后使用data.sql初始化数据。
spring.datasource.schema属性的值,定义数据库创建脚本的位置,通过spring.datasource.data属性设置数据初始化脚本的位置
spring.datasource.initialize设为false,将启动时不执行数据库初始化处理;此时需要设置hibernate.ddl-aut=none
hibernate.ddl-aut设置为none,这样就可以避免启动时Hibernate试图根据实体类创建数据库表而造成的错误。
Spring Boot特性
Spring Boot自动配置机制:总结为以下两点:·通过@EnableAutoConfiguration为基于Spring的应用开启自动配置机制;·通过一系列的@Conditional完成自动配置机制的实现。
在spring-boot-autoconfigure-[version].jar中包含了一系列注解了@Configuration的自动配置类,这些类中还有@EnableConfigurationProperties注解,用来指定自动配置的一些条件。这些自动配置类则根据所扫描到的依赖包,通过对所注解的@Condition条件来决定是否开启并注册到Spring上下文中。
@Condition则可以根据一系列条件来决定是否启用某项配置,这些条件可以是:·在classpath下是否存在指定的类;·在ApplicationContext中是否已存在指定类型的Bean;·在指定目录下是否存在指定的文件;·在配置文件中是否配置了指定的属性;·指定的系统属性是否存在或不存在;……
开箱即用的条件注解:·@ConditionOnBean:在ApplicationContext中存在指定类型的Bean时启用。·@ConditionOnMissingBean:在ApplicationContext中不存在指定类型的Bean时启用。·@ConditionOnClass:在classpath下存在指定类时启用。·@ConditionOnMissingClass:在classpath下缺少指定类时启用。·@ConditionOnProperties:当存在指定属性配置时启用。·@ConditionOnResource:当存在指定资源时启用。·@ConditionOnWebApplication:当应用是一个Web应用时启用。……
可查org.springframework.boot. autoconfigure.condition中的Condition注解定义;
单独使用Spring框架时,可以通过@PropertySource来指定这些配置文件列表
Spring Boot通过自动配置机制在系统启动时就自动创建了PropertyPlaceHolderConfigurer的Bean,并默认从class目录下的application.properties(或application.yml)文件中加载作为系统的默认配置。
关于不同环境的配置:在项目中定义多个profile配置文件,文件命名格式为application-{profile}.properties(或yml),在启动时可以通过参数spring.profile来指定所要启动的环境即可。
将配置文件中所配置的数据直接注入到指定的对象中的注解:·@Value:将配置参数的值直接注入到指定的Bean属性中。·@ConfigurationProperties:通过指定的前缀,批量将配置的值注入一个Bean中。·Spring还提供了一些属性配置校验注解,如@NotNull、@Email、@Min、@Max等,可以辅助对参数配置进行有效性检验。
在属性配置中,属性的名称并不需要与Bean中的字段名称严格保持一致,wechatAppId,对应配置的属性名称可以为wechatAppId、wechat-app-id或WECHAT_APP_ID,@Value还可以使用SpEL表达式。
springboot自定义配置:
可以排除logback的依赖,添加dependency对log4j的依赖;
Spring Boot会使用自动配置机制,根据开发者所使用的日志系统对日志进行自动配置,发现class目录下的logback.xml就会直接启用;
在开发中,Spring Boot还为我们提供了一种更为简洁的日志配置方式,就是直接在项目配置文件中进行配置。
敏捷开发:
通过快速构建一个最小可行性产品(Minimum Viable Product,简称MVP),然后通过快速的产品功能迭代(小功能的添加优化修复等),面向客户试错;如果是大的改动,需要在进行代码重构的同时添加一些必要的改进,所以此时开发者可以很好地把架构改进计划融入到多个迭代周期中。
测试驱动开发(Test Driven Development,简称TDD)可以说是现代开发所常用的一种开发驱动方式,指的是在编写业务逻辑之前先编写单元测试,让测试不断失败;优点:早发现伪需求,从而保障系统功能的正确性,另外,在保障不改变功能的情况下可以持续地对代码进行改进和重构,保证稳定性。
Controller层单元测试,重要,多个团队在进行协作开发,避免影响到其他API消费者,各微服务之间需要这些API才能够进行协作处理。
关于RESTful API设计
互联网软件的架构原则定名为REST,即Representational State Transfer的缩写(表现层状态转化)。
表现层是指资源(Resources)的表现层,网络上任何一个具体信息(如一段文本、一张图片、一首歌曲或一种服务)都可称为一个资源。即URI(统一资源标识符)。
客户端每次向服务器发送请求就代表了让服务器端资源发生“状态转化”(StateTransfer),而资源的这种转化最终会通过表现层展示出来,所以这就是REST-表现层状态转化。
把符合REST这种原则的互联网应用架构称之为RESTful架构。
RESTful API设计时:
·1)以资源为中心进行URL设计;
简洁、清晰、结构化的URL(统一资源定位符)设计是至关重要的;尽量使用复数来表示资源,而对于特定的单个资源则通过添加ID或者其他唯一标识符来表示。对于有关联关系的资源可以通过资源嵌套方式来表示;
URL大小写敏感;RESTful API的设计最好做到Hypermedia化,也就是在返回结果中包含所提供相关资源的链接,使得用户可以根据返回结果就能得到后续操作需要访问的地址。此设计称为HATEOAS(Hypermedia As The EngineOf Application State),比如https://api.github.com;
·2)正确使用HTTP方法及状态码;
GET用来获取资源,POST用来创建资源(也可以用于更新资源), PUT用来更新资源,DELETE则是用来删除资源,还有一个PATCH方法用来更新资源。状态码介绍https://httpstatuses.com/
·3)查询及分页处理原则;
URL中附加参数来实现,比如:·?state=closed:查询指定状态。·?limit=10:指定返回10条记录。·?page=2& size=25&sort=created, desc:进行分页查询,并指定每页记录数及排序方式。
当我们使用Spring Data的Pageable进行分页查询时,需要传入的分页参数的默认名称如下。·page:所要查询的页面数,也就是第几页数据。从0开始,默认值也是0。·size:每一页中最大记录数,默认值为20。·sort:分页数据的排序字段及方式,格式为property(, ASC|DESC)。其中排序方式为可选,默认排序方式为升序(ASC)。该参数可以多个,比如sort=firstname& sort=lastname, asc。如果分页参数和项目中不同,可以通过实现PageableHandlerMethodArgumentResolverCustomizer接口进行自定义。
·4)其他指导原则。
使用JSON作为响应返回格式,如果用户需要其他格式,如XML,可以通过请求头部中Accept来指定。应该尽量将API全部部署在一个专用的域名之下,如https://api.cd826dong或https://www.cd826dong/api。在设计API时最好将API的版本放入URL中,如https://api.cd826dong/v1/users。通过errmsg,errcode给出明确的信息。
新词:COBRA、EJB、ESB、SOA
Guava:这是一个非常有用的工具库
Robert C. Martin在《敏捷软件开发:原则、模式与实践》