微服务架构,这个在几年前还算比较前卫的技术在如今遍地开花。得益于开源社区的支持,我们可以轻松地利用 Spring Cloud 以及 Docker 容器化快速搭建一个微服务架构的原型。不管是成熟的互联网
微服务架构,这个在几年前还算比较前卫的技术在如今遍地开花。得益于开源社区的支持,我们可以轻松地利用 Spring Cloud 以及 Docker 容器化快速搭建一个微服务架构的原型。不管是成熟的互联网公司、创业公司还是个人开发者,对于微服务架构的接纳程度都相当高,微服务架构的广泛应用也自然促进了技术本身更好的发展以及更多的实践。本文将结合项目实践,剖析在微服务的背景下,如何通过前后端分离的方式开发移动应用。
对于微服务本身,我们可以参考 Martin Fowler 对 Microservice 的阐述。简单说来,微服务是一种架构风格。通过对特定业务领域的分析与建模,将复杂的应用分解成小而专一、耦合度低并且高度自治的一组服务。微服务中的每个服务都是很小的应用,这些应用服务相互独立并且可部署。微服务通过对复杂应用的拆分,达到简化应用的目的,而这些耦合度较低的服务则通过 API 形式进行通信,所以服务之间对外暴露的都是 API,不管是对资源的获取还是修改。
微服务架构的这种理念,和前后端分离的理念不谋而合,前端应用控制自己所有的 UI 层面的逻辑,而数据层面则通过对微服务系统的 API 调用完成。以 JSP (Java Server Pages) 为代表的前后端交互方式也逐渐退出历史舞台。前后端分离的迅速发展也得益于前端 Web 框架 (Angular, React 等) 的不断涌现,单页面应用(Single Page Application)迅速成为了一种前端开发标准范式。加之移动互联网的发展,不管是 Mobile Native 开发方式,还是 React Native / PhoneGap 之流代表的 Hybrid 应用开发方式,前后端分离让 Web 和移动应用成为了客户端。客户端只需要通过 API 进行资源的查询以及修改即可。
BFF 概况及演进
Backend for Frontends(以下简称BFF) 顾名思义,是为前端而存在的后端(服务)中间层。即传统的前后端分离应用中,前端应用直接调用后端服务,后端服务再根据相关的业务逻辑进行数据的增删查改等。那么引用了 BFF 之后,前端应用将直接和 BFF 通信,BFF 再和后端进行 API 通信,所以本质上来说,BFF 更像是一种“中间层”服务。下图看到没有BFF以及加入BFF的前后端项目上的主要区别。
1. 没有BFF 的前后端架构
Response:
.[ { "id": 1, "name": "abc", "avatar": "http://cdn.image.com/image_avatar1" }, ...]
.Response:
.{ "data": { "users": [ { "id": 1, "name": "abc", "avatar": "http://cdn.image.com/image_avatar1" }, ... ] }}
.关于 GraphQL 更详细的用法,我们可以通过查看文档以及其他文章更加详细的去了解。相比于 REST 风格,GraphQL 具有如下特性:
1. 定义数据模型:按需获取
GraphQL 在服务器实现端,需要定义不同的数据模型。前端的所有访问,最终都是通过 GraphQL 后端定义的数据模型来进行映射和解析。并且这种基于模型的定义,能够做到按需索取。比如对上文 /users
资源的获取,如果客户端只关心 user.id
, user.name
信息。那么在客户端调用的时候,query
中只需要传入 users {id /n name}
即可。后台定义模型,客户端只需要获取自己关心的数据即可。
2. 数据分层
查询一组users
数据,可能需要获取 user.friends, user.friends.addr
等信息,所以针对 users 的本次查询,实际上分别涉及到对 user
, frind
, addr
三类数据。GraphQL 对分层数据的查询,大大减少了客户端请求次数。因为在 REST 模式下,可能意味着每次获取 user
数据之后,需要再次发送 API 去请求 friends 接口。而 GraphQL 通过数据分层,能够让客户端通过一个 API获取所有需要的数据。这也就是 GraphQL(图查询语句 Graph Query Language)名称的由来。
{ user(id:1001) { // 第一层 name, friends { // 第二层 name, addr { // 第三层 country, city } } }}
.3. 强类型
.const Meeting = new GraphQLObjectType({ name: 'Meeting', fields: () => ({ meetingId: {type: new GraphQLNonNull(GraphQLString)}, meetingStatus: {type: new GraphQLNonNull(GraphQLString), defaultValue: ''} })})
.GraphQL 的类型系统定义了包括 Int, Float, String, Boolean, ID, Object, List, Non-Null 等数据类型。所以在开发过程中,利用强大的强类型检查,能够大大节省开发的时间,同时也很方便前后端进行调试。
4. 协议而非存储
GraphQL 本身并不直接提供后端存储的能力,它不绑定任何的数据库或者存储引擎。它利用已有的代码和技术进行数据源的管理。比如作为在 BFF 层使用 GraphQL, 这一层的 BFF 并不需要任何的数据库或者存储媒介。GraphQL 只是解析客户端请求,知道客户端的“意图”之后,再通过对微服务API的访问获取到数据,对数据进行一系列的组装或者过滤。
5. 无须版本化
.const PhotoType = new GraphQLObjectType({ name: 'Photo', fields: () => ({ photoId: {type: new GraphQLNonNull(GraphQLID)}, file: { type: new GraphQLNonNull(FileType), deprecationReason: 'FileModel should be removed after offline app code merged.', resolve: (parent) => { return parent.file } }, fileId: {type: new GraphQLNonNull(GraphQLID)} })})
.GraphQL 服务端能够通过添加 deprecationReason,自动将某个字段标注为弃用状态。并且基于 GraphQL 高度的可扩展性,如果不需要某个数据,那么只需要使用新的字段或者结构即可,老的弃用字段给老的客户端提供服务,所有新的客户端使用新的字段获取相关信息。并且考虑到所有的 graphql 请求,都是按照 POST /graphql
发送请求,所以在 GraphQL 中是无须进行版本化的。
GraphQL 和 REST
对于 GraphQL 和 REST 之间的对比,主要有如下不同:
1. 数据获取:REST 缺乏可扩展性, GraphQL 能够按需获取。GraphQL API 调用时,payload 是可以扩展的;
2. API 调用:REST 针对每种资源的操作都是一个 endpoint, GraphQL 只需要一个 endpoint( /graphql), 只是 post body 不一样;
3. 复杂数据请求:REST 对于嵌套的复杂数据需要多次调用,GraphQL 一次调用, 减少网络开销;
4. 错误码处理:REST 能够精确返回HTTP错误码,GraphQL 统一返回200,对错误信息进行包装;
5. 版本号:REST通过 v1/v2 实现,GraphQL 通过 Schema 扩展实现;
微服务 + GraphQL + BFF 实践
在微服务下基于 GraphQL 构建 BFF,我们在项目中已经开始了相关的实践。在我们项目对应的业务场景下,微服务后台有近 10 个微服务,客户端包括针对不同角色的4个 App 以及一个 Web 端。对于每种类型的 App,都有一个 BFF 与之对应。每种 BFF 只服务于这个 App。BFF 解析到客户端请求之后,会通过 BFF 端的服务发现,去对应的微服务后台通过 CQRS 的方式进行数据查询或修改。
1. BFF 端技术栈

我们使用 GraphQL-express 框架构建项目的 BFF 端,然后通过 Docker 进行部署。BFF 和微服务后台之间,还是通过 registrator 和 Consul 进行服务注册和发现。
.addRoutes () { this.express.use('/graphql', this.resolveFromRequestScopeAndHandle('GraphqlHandler')) this.serviceNames.forEach(serviceName => { this.express.use(`/api/${serviceName}`, this.routers.apiProxy.createRouter(serviceName)) }) }
.在 BFF 的路由设置中,对于客户端的处理,主要有 /graphql
和 /api/${serviceName}
两部分。/graphql
处理的是所有 GraphQL 查询请求,同时我们在 BFF 端增加了 /api/${serviceName}
进行 API 透传,对于一些没有必要进行 GraphQL 封装的请求,可以直接通过透传访问到相关的微服务中。
2. 整体技术架构

假设 BFF 端已经发布到生产环境,提供了 inspection
相关的 tenants
和 landlords
的查询。现在需要将图一的结构变更为图二的结构,但是为了不影响老用户的 API 访问,这时候我们的 BFF API 必须进行兼容。如果在 REST 中,可能会增加 api/v2/inspections
进行 API 升级。但是在 BFF 中,为了向前兼容,我们可以使用图三的结构。这时候老的 APP 使用黄色区域的数据结构,而新的 APP 则使用蓝色区域定义的结构。
GraphQL Mutation 与 CQRS
.mutation { area { create (input: { areaId:"111", name:"test", }) }}
.如果你详细阅读了 GraphQL 的文档,可以发现 GraphQL 对 query
和 mutation
进行了分离。所有的查询应该使用 query { ...}
,相应的 mutaition 需要使用 mutation { ... }
。虽然看起来像是一个convention,但是 GraphQL 的这种设计和后端 API 的 读写职责分离(Command Query Responsibility Segregation)不谋而合。而实际上我们使用的时候也遵从这个规范。所以的 mutation 都会调用后台的 API,而后端的 API 对于资源的修改也是通过 SpringBoot EventListener 实现的 CQRS 模式。
如何做好测试

在引入了 BFF 的项目,我们的测试仍然使用金字塔原理,只是在客户端和后台之间,需要添加对 BFF 的测试。
- Client 的 integration-test 关心的是 App 访问 BFF 的连通性,App 中所有访问 BFF 的请求都需要进行测试;
- BFF 的 integration-test 测试的是 BFF 到微服务 API 的连通性,BFF 中依赖的所有 API 都应该有集成测试的保障;
- API 的 integration-test 关注的是这个服务对外暴露的所有 API,通常测试所有的 Controller 中的 API;
结语
微服务下基于 GraphQL 构建 BFF 并不是银弹,也并不一定适合所有的项目,比如当你使用 GraphQL 之后,你可能得面临多次查询性能问题等,但这不妨碍它成为一个不错的尝试。你也的确看到 Facebook 早已经使用 GraphQL,而且 Github 也开放了 GraphQL 的API。而 BFF, 其实很多团队也都已经在实践了,在微服务下等特殊场景下,GraphQL + BFF 也许可以给你的项目带来惊喜。
参考资料
【注】部分图片来自网络
- https://martinfowler.com/articles/microservices.html
- https://www.thoughtworks.com/insights/blog/bff-soundcloud
- http://philcalcado.com/2015/09/18/thebackendforfrontendpattern_bff.html
- http://samnewman.io/patterns/architectural/bff
- https://medium.com/netflix-techblog/embracing-the-differences-inside-the-netflix-api-redesign-15fd8b3dc49d
文/ThoughtWorks 龚铭
在即将举办的2018技术雷达峰会中,“微服务”概念的联合提出者——James Lewis将在现场讲述微服务的发展趋势与项目实践,即刻报名(可享七折早鸟优惠),就能获得与大牛零距离探讨的机会~
.

- 0