本文主要讲解微服务基础框架的设计与实践,包括:项目代码组织、单体仓库、接口参数校验及统一异常处理、实现强类型接口调用、分环境配置、以及主流服务框架对比。
Spring Boot 是业界微服务应用开发的主流框架之一,但是在真正的开发生产级微服务之前,我们仍然需要对 Spring Boot 进行一些必要的封装,以适应我们开发业务的需求,这种封装沉淀下来就是一个企业的微服务基础框架。标准化和重用这个框架可以大大提升我们后续开发微服务的效率。
接下来的内容,主要讲解如何基于 Spring Boot 做一些简单的扩展,定制出一个轻量级的微服务基础框架,这个框架将作为我们后续微服务开发的基础。
微服务项目采用 Maven 多模块项目进行组织。有一个父pom,它规范了整个项目的依赖,每个模块优先使用父pom规范的依赖,然后,再根据需要引入一些模块专门的依赖。
common-lib这个模块,它是一个公共共享模块,其他的所有的微服务都依赖这个common-lib公共共享模块,其是基础框架类的封装,我们的服务开发框架相关的代码大部分都在这个common-lib里。
<modules>
<module>common-libmodule>
<module>account-apimodule>
<module>account-svcmodule>
<module>company-apimodule>
<module>company-svcmodule>
<module>mail-apimodule>
<module>mail-svcmodule>
<module>sms-apimodule>
<module>sms-svcmodule>
<module>bot-apimodule>
<module>bot-svcmodule>
<module>ical-svcmodule>
<module>whoami-apimodule>
<module>whoami-svcmodule>
<module>web-appmodule>
<module>faradaymodule>
modules>
整个项目的依赖关系:
整个有一个父pom,它规范了整个应用的依赖,这些微服务account、company等等,他们都依赖这个父pom,根据需要再引入一些自己定制的一些依赖。
有一个common-lib,也是依赖于父pom,它是我们的基础框架的一些封装类,所有的微服务也直接的依赖于common-lib,这就是总体的项目依赖关系。
接口和实现分离的组织方式
我们再看每一个微服务模块,每一个微服务都由两个子模块组成,一个是服务接口模块,一个是服务实现模块。
比方说account服务,对应的有 account-api 就是接口模块,account-svc就是服务实现模块。并且,account-svc也是依赖account-api的,这种接口和实现分离的组织方式,一方面代码结构比较清晰,另一方面也益于这个接口模块的重用。
一般我们会把这些这个接口模块 account-api 等这种接口模块上传到Maven仓库进行管理。
如果某个应用依赖于某个服务,只要引用对应的接口模块就可以了,因为这个接口模块它是强类型的,依赖方就可以直接使用强类型的接口直接调目标服务,不需要去手动的解析JSON。
大家知道手动解析JSON是非常繁琐和容易出错的,所以使用这个强类型客户端可以大大提升我们开发和测试的效率,是微服务开发的最佳实践之一。
再看项目代码组织,除了这些服务有两个子项目接口和实现,也有一些其他的服务,比方说ical-svc是日程表下载站点,还有web-app产品营销站点,这两个是web mvc应用,没有独立的接口模块。
前端资源应用
frontend:
这是前端相关的代码。
依赖管理
UTF-8
1.8
2.1.18.RELEASE
Greenwich.SR6
org.springframework.boot
spring-boot-dependencies
${spring.boot.version}
pom
import
org.springframework.cloud
spring-cloud-dependencies
${spring.cloud.version}
pom
import
项目依赖JDK1.8、Spring Boot 版本是2.1.2.RELEASE、Spring Cloud 的依赖是 Greenwich.RELEASE,这个项目当中也用到了 Spring Cloud ,主要是 OpenFeign 相关的一些依赖。
因为 Spring Boot 和 Spring Cloud 版本迭代比较快,如果你要开发或者运行项目的话,建议不要随意变更这些依赖版本。
Spring 提供了 spring-boot-dependencies、spring-cloud-dependencies 依赖管理的pom,这种依赖管理pom可以规范依赖版本,还有效地减少依赖版本冲突的问题。重用这些pom,是 Spring 应用开发的最佳实践之一。
到这,我们就把整个项目的代码组织梳理了一遍。
本项目虽然采用微服务架构,但是,所有代码都在一个仓库当中。这种组织方式也称为单体仓库 mono-repo 。
那么,什么是单体仓库?为什么谷歌采用单体仓库?
单体应用和微服务相比,单体应用最大的好处就是可以独立开发、测试、部署和扩展。单体应用一般采用单体仓库,但是微服务的代码仓库该如何组织呢?一定是每个服务一个仓库吗?其实也不一定。
针对微服务的代码组织,业界主要有两种主要的实践:
那么多仓库呢?就是把项目拆分成微服务以后,每个子项目都用一个git仓库,这叫多仓库。
那么什么是单体仓库呢?就是我们把微服务化后的这些子模块,它都放在一个git仓库里面。
多仓库的好处? 是显而易见的,每个服务独立仓库,职责单一、代码量和复杂性受控、服务由不同的团队独立维护,边界清晰、单个服务也益于自制开发,测试部署和扩展,不需要集中管理协调,这些基本上就是微服务的好处。
多仓库也有问题:
那么单体仓库可以部分的解决上面提到的多仓库的问题,它有下面的一些好处。
单体仓库的好处:
所以单体仓库和多仓库都是有利有弊的,那么在业界实际上采用单体仓库管理的公司其实并不少。像谷歌、facebook 等这些互联网的巨头,虽然这些公司系统庞大,服务众多,内部研发团队人数众多,一般过千的,但是他们还是采用单体仓库,而且都很成功。
当然了,单体仓库也是有弊端的,就是随着公司业务团队规模的变大,单一的代码库会变得越来越庞大,复杂性也急速上升,所以这些互联网巨头之所以能够玩转单体仓库,一般都有独立的代码管理和集成团队进行支持,也有配套的自动化构建工具来支持。比方说谷歌,他自研了面向单体仓库的构建工具bazel。
那么初创公司如果采用微服务架构,一般早期服务不是特别多,采用单体仓库会比较合适。
Staffjoy 应用规模小,虽然采用微服务架构,但是服务数量不多。为了简化和规范化代码管理,也为了支持一键构建、一键部署这些云原生架构的特性,所以采用单体仓库。
微服务架构并不是主张所有的东西都要独立自治,至少代码仓库就可以集中管理,而且这也是业界的最佳实践之一。
接口参数校验
开发人员必须重视参数校验,因为参数校验和具体的业务场景相关,一般无法完全做到自动化。因此参数校验主要是业务开发人员的职责,当然服务框架应该提供必要的支持,方便开发人员对请求参数进行必要的校验。
Hibernate-validator 可实现对数据的验证,它是对 JSR(Java Specification Requests) 标准的实现。
支持一整套基于标准 JSR 303 和 JSR 380 的参数校验注解。
主要是在 控制(Controller) 层对参数进行校验,以保证这些入参都是经过校验的。
统一异常处理
在 Spring Boot 框架中,对 RESTful 服务 支持 RestControllerAdvice 的方式,进行统一异常处理。
RestControllerAdvice 工作原理:在一个请求响应周期当中,如果 Controller/Service/Repository 这些层出现任何异常,都会被 RestControllerAdvice 机制所捕获,进行分类和统一处理,然后将处理后的响应返回给调用方。
@RestControllerAdvice
public class GlobalExceptionTranslator {
@ExceptionHandler(MissingServletRequestParameterException.class)
public BaseResponse handleError(MissingServletRequestParameterException e) {
//...
}
@ExceptionHandler(Throwable.class)
public BaseResponse handleError(Throwable e) {
//...
}
}
Spring MVC 应用统一异常处理与 RESTful 服务统一异常处理稍有不同,因为 RESTful 服务输出的主要是 JSON 消息,Spring MVC 当中输出的主要是 HTML 页面。
如果是 Spring MVC 应用当中,一般通过实现 ErrorController 来实现统一异常处理和捕获。
@Controller
public class GlobalErrorController implements ErrorController {
@RequestMapping("/error")
public String handleError(HttpServletRequest request, Model model) {
Object statusCode = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
Object exception = request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);
//...
return "error";
}
}
具体详细情况可参考:
来源:《如何基于 Spring Boot 实现接口参数验证及全局异常处理》https://mp.weixin.qq.com/s/KH6pH9iUPuKV9sn8iGsw5w
传统的RPC服务一般是强类型的,RPC通常采用定制的二进制协议对消息进行编码和解码,采用TCP传输消息。
RPC服务通常有严格的契约,也就是 contract 这样一个概念;开发服务前,先要定义 IDL(Interface description language,接口描述语言) ,用 IDL 定义契约,再通过这个契约自动生成强类型的服务器端和客户端的接口,服务调用的时候直接使用强类型客户端,不需要再手动进行消息的编码和解码。
GRPC 和 Thrif 是目前主流的两种强类型的 RPC 框架,而现代的 RESTful 服务一般是弱类型的。
RESTful 服务 通常采用 JSON 作为传输消息,然后使用 HTTP 作为传输协议。 RESTful 服务一般没有严格的契约的概念,它使用普通的 http client 就可以调用,但是调用方通常需要对这个JSON消息进行手动编码和解码工作。
强类型服务接口的优点: 是接口规范、自动代码生成、自动编码解码、编译期自动类型检查。
强类型服务接口的缺点:
弱类型服务接口的优点: 是客户端和服务器端不强耦合,不需要开发特别的代码生成工具,一般的 http client 就可以调用;
开发测试友好,普通的浏览器、Postman可以轻松访问。
弱类型服务接口的缺点: 是需要调用方手动的编码解码消息;没有自动代码生成,没有编译期接口类型检查,代码不容易规范;开发效率相对低,而且容易出现运行期的错误。
问题: 那么我们在服务开发过程中如何设计强类型接口?有没有办法结合强弱类型服务接口各自的好处,同时又规避他们的不足呢?
方案: 在 Spring RESTful 服务弱类型接口的基础上,借助 Spring Cloud Feign 支持的强类型接口调用的特性,实现了强类型 REST 接口调用机制,同时兼备强弱类型服务接口的好处。
Spring Cloud Feign 本质上是一种动态代理机制,你只要给出一个 RESTful API 对应的 Java 接口,它就可以在运行期动态的拼装出对应接口的强类型客户端。
虽然我们开发出来的服务是弱类型的 RESTful 的服务,但是因为有 Spring Cloud Feign 的支持,我们只要简单的给出一个强类型的 Java API 接口(这个工作量不大的),就自动获得了强类型的客户端。
也就是说,利用 Spring Cloud Feign ,我们可以同时获得强弱类型的好处,包括:编译期自动类型检查、不需要手动编码解码、不需要开发代码生成工具、而且客户端和服务器端不强耦合。这样可以同时规范代码风格,提升开发测试效率。
具体详细情况可参考:
来源:《如何基于 Spring Cloud Feign 实现强类型接口调用RESTful服务》https://mp.weixin.qq.com/s/bYZXhf9BfAfL5aC3bSyiHQ
多环境支持是现代互联网应用研发和交互的基本需求,通过规范多环境和对应的研发流程,可以同时提升交互质量和效率。
本项目所采用的环境规范:
有了规范的环境就可以规范研发流程,从 DEV->TEST->UAT->PROD 上线的过程中,每个环境可以设置一些质量阶段门,如必须经过测试环境测试才能上到UAT环境,经过UAT环境测试才能上到PROD环境。
基于规范化的环境和研发流程,再配合自动化的集成、测试和发布工具,就可以构建持续集成和交互,即 CI/CD 流水线。
CI/CD 是现代互联网软件交互的最佳实践,它的基础标准化的环境、交互流程及自动化工具。
目前比较流行的服务开发框架主要有:Spring Boot、Dubbo、Motan、gRpc 。
编程模型:
Spring Boot
Spring Boot 的亮点:就是社区生态好。因为 Spring 框架已经有十几年的历史了,整个社区生态非常好,然后它使用注解的方式,开发、测试都比较友好,效率比较高。
Dubbo
Dubbo的亮点: 是阿里背书,经过了阿里大流量的考验,另外它的服务治理做得也是比较有特色。
Motan
Motan 其实是新浪模仿 Dubbo 开发的一个框架,可以认为是一个轻量版的 Dubbo ,如果你觉得 Dubbo 太重,想找一个轻量级的框架,类似于 Dubbo 的,那么可以考虑 Motan 。另外,它还支持 sidecar 的方式去对接其他的语言,这也是它的一个亮点。
gRpc
gRpc框架的亮点: 首先,它是谷歌背书,支持多语言;另外,它还支持一些高级的,像HTTP2 ,其它大部分语言都是支持HTTP或者TCP,gRpc 它是支持 HTTP2 的,在某些场景下它的性能会比较好。
综合选型
本项目在做技术选型的时候,我们不会去选 Dubbo ,因为我们的应用最后是要支持一键部署K8s环境里面,但 Dubbo 它自己有一套治理体系,然后有一个服务发现,跟 K8s 有点不太相容,你如果要去让 Dubbo 集成到 K8s 里面,你可能还要做一些改造。所以没有用 Dubbo 。
Motan 它跟 Dubbo 是类似的,也有一套自己的治理体系,也有自己的服务发现,所以也没有采用。
gRpc 其实是有重点考虑的,因为它整体的设计比较先进,多语言支持。而且是强类型契约驱动的开发方式,而且还支持HTTP2。原本考虑用gRpc来开发本项目的服务的。
gRpc的问题是:它最终是个 RPC 框架,它内部调用挺好的,通过强类型生成出来的客户端直接去调用。但是,如果要把 RPC 服务暴露成 HTTP/REST 服务,就要引入一个对应的 gRpc API Gateway 转换层服务,会多引入一层复杂性,需要做转换,所以就比较麻烦。
Spring Boot 它天生就是 RESTful的,它不支持契约优先的方式,也就说不能生成强类型的客户端。一般情况下,我们都是直接写代码,如果要开发强类型的客户端,只需要写一个接口并添加 Spring Feign 的注解,但这个工作量不大。
所以综合考虑,最后还是倾向于使用 Spring Boot RESTful 比较简单,我们也可以简单的做一些强类型的客户端,然后这个框架可以很容易的部署到 K8s 环境里,这就是最后选择 Spring Boot 的依据。
教学版 Java/Spring:https://github.com/spring2go/staffjoy
实践版 Java/Spring:https://gitee.com/chentian114/staffjoy-chen
《Spring Boot 与 Kubernetes 云原生微服务实践 ~ 全面掌握云原生应用的架构设计与实现》 杨波