本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!
Hello,这里是爱 Coding,爱 Hiphop,爱喝点小酒的 AKA 柏炎。
本篇是手把手搭建基础架构专栏的第五篇,👇🏻是专栏历史文章,依次读取效果更佳。
第一篇:从零到一搭建基础架构(1)-玩转maven依赖版本管理 第二篇:从零到一搭建基础架构(2)-如何构建基础架构模块划分 第三篇:从零到一搭建基础架构(3)-base模块搭建上篇 第四篇:从零到一搭建基础架构(4)-base模块搭建下篇
微服务的生态体系下,RPC是服务之间不可缺少的通信方式。
在庞大的微服务生态下,可能存在成百上千的服务。越上层的业务服务,对于一些基础服务的依赖就会越多。比如订单服务会依赖支付服务、用户服务、商品服务等等。
这么多的依赖,我们如何让服务之间的依赖松散化,开箱即用化?
这将是本文为大家所解释的,废话不多说,Let‘s get it.
本文无特殊说明,均已Spring Cloud Open-Feign作为RPC框架做演示
你需要先clone common-dependency
然后执行mvn clean install 将 common-dependency包打到你本地仓库
否则你拉下来common-frame工程后会报找不到
<parent> <groupId>com.baiyan</groupId> <artifactId>common-dependency</artifactId> <version>1.0.0-SNAPSHOT</version> </parent>
我在看过很多大的单体服务做微服务改造的时候,从业务出发,将工程划分为多个微服务。
原有业务之间的关联与调用关系从系统内变为系统外。
服务之间的通信方式从service调用改为rpc调用,从spring的事件通知改为MQ的消息通知。
跨服务之间消息通信也是遵守增删改查的逻辑。
我们以订单服务call用户服务获取用户信息为例👇🏻
这种比较常见的方式就是使用订单服务访问用户服务的RPC接口。
在同一注册中心下,使用openFeign作为RPC框架的调用下,比较常见的开发步骤是怎么样的?
1.用户服务在UserController里面定义一个detail的方法。
@ApiOperation(value = "用户详情")
@GetMapping("{id}")
public Result<UserDetailDTO> detail(@PathVariable Long id) {
return Result.success(userService.detail(id));
}
2.订单服务在工程内定义UserClient的OpenFeign定义。
@FeignClient(
name = "user",
url = "https://www.baiyan.com/user",
fallback = UserClient.UserFallback.class,
configuration = UserClient.UserFallback.class
)
public interface UserClient {
@GetMapping("/{id}")
Result<UserDTO> getUserDetail(@PathVariable("id") Long id);
@Component
class UserFallback {
public Result<UserDTO> getUserDetail(Long id){return null;}
}
}
3.订单服务启动类增加 @EnableFeignClients 注解扫描UserClient,将UserClient定义为一个bean。
4.订单服务在需要获取用户数据的地方直接注入订单工程内定义的UserClient使用。
应该有很大一部分同学都是这么接入openFeign的。从功能角度来说,这种编码方式能够实现订单服务获取用户服务所提供的的用户信息。但是从编码角度,上面的代码存在很多问题
问题 | 描述 |
---|---|
POJO重复定义 | 细心的小伙伴应该发现,UserController里面返回的实体是UserDetailDTO,UserClient接收的结构是UserDTO。服务间的对接需要通过文档来进行对齐,用户服务已经定义的结构,在订单服务还要再定义一遍。用户服务一旦对于用户信息结构做了改动,增加字段或者修改字段等等,服务的请求方都感知不到,只能通过沟通,文档对接的方式拉平 |
过多的用户接口 | 用户服务提供的是web层面的controller接口。由于web接口都是对用户开放的,内部的RPC接口需求也会被暴露给用户 |
鉴权的冗余 | 同样的,内部服务之间的互相调用,理论上不需要再经过网关的鉴权,应该是互相信任的。但由于提供方定义的接口是Controller接口,暴露给了用户,因此鉴权也成为了必须的。对于订单服务的普通用户请求还好说,还能传递一下用户请求的身份信息。但是如果是定时任务,订单服务根本不存在用户身份,为了能够通过用户服务的鉴权,还需要去伪造一份身份信息,非常麻瓜。 |
异常感知弱 | 我们需要依据标准Result里面的code字段来感知当前RPC接口出现了什么样的问题。但实际上,对于调用方来说,直接处理RPC接口的异常比解析RPC接口返回的errorCode是什么,再做出什么样的处理更加方便。 |
接入RPC不够丝滑 | 增加一个服务的接口依赖,我们就要在依赖工程内增加一个openFeign的定义,太难受了。 |
为了解决上述粗暴的openFeign使用方式所带来的问题,基础架构能做点什么?
我们先来回顾一下api包与rpc包的定义
Maven模块 | 模块定义 | 备注 |
---|---|---|
api | 定义微服务提供者的接口定义,将openFeign相关的接口定义,所必须的交互实体,枚举等定义在此处 | Common-Frame工程中所定义的api包仅做模块分层指导,并无实际意义 |
rpc | api包的openFeign定义实现,这里如果嫌麻烦api包跟rpc包可以合并,我习惯分开,接口结构更加清晰 | Common-Frame工程中所定义的rpc包提供统一的序列化、熔断、异常处理等配置 |
先说为什么在架构指导上我们要把api包与rpc包分开来。
api包的定位更像是一个三方的结构声明包。里面将会包含所有可以暴露给外部工程的工具类、常量、POJO、枚举等。而rpc包的定位是将openFeign提供starter,让服务的依赖方引入了rpc的maven依赖后,可以直接使用注入openFeign开箱即用。在有的场景下,A服务只需要依赖B服务的某个工具类或者某个枚举定义,并不想要调用B服务所提供的RPC接口。这种时候就可以减少RPC定义的引入到A服务中。把包的职责划分清楚。
对于业务服务来说,RPC包中包含了api包中定义的rpc接口的openFeign定义,那在基础架构中,rpc包能做什么呢?
答案是:公共配置
在使用openFeign作为RPC框架之后,随之会引入很多常见的微服务问题:熔断、限流、降级等等。而这些配置对于所有业务服务来说都是互通的,无非是熔断的时间,限流的规则等差异。所以我们在基础架构中可以定义上述的这些公共配置,将这些配置做成base-rpc-starter包。业务服务中的rpc依赖common-frame中的rpc包就可以获取一系列的公共配置。
定义完common-frame中的公共配置后,在业务服务中,我们该如何定义并使用rpc包呢?
以文章开头的项目Authentication为例,我们可以看到工程中,我们已经划分了api包与rpc包。
Authentication服务是比较早期我做分布式用户中心时的demo,是按照common-frame的思想搭建的,为了项目开箱即用,所以并没有直接一对一的引入common-frame中已经设定好的maven配置。这个在最后一两章的时候会为大家全局性的演示如何完美使用common-frame。 这里Authentication服务仅为大家演示在api包与rpc包是如何应用在业务服务中的。
在api包中定义了所有需要暴露给服务调用方的POJO定义、枚举定义、常量定义和接口定义。
在rpc包中,pom依赖引入了api包,将在api包中定义的接口进行openFeign定义。并且我们在rpc包中定义了一个自动配置类
/**
* auth服务RPC自动配置类
*
* @author baiyan
* @date 2020/11/26
*/
@Configuration
@Import(AuthFeignConfig.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@EnableFeignClients(basePackages = "com.baiyan.auth.rpc")
public class AuthFeignClientAutoConfiguration {
}
这个自动配置类的好处是什么?
假设订单服务为服务调用方。那么订单服务只要引入了用户服务的rpc包,订单服务无需做任何配置,就可以直接注入api包中定义UserApi或者rpc中定义的UserApiClient,就可以实现访问用户服务。
如果只是在rpc中定义openFeign的结构,订单服务仍旧无法注入上述用户服务的bean,需要在订单服务的启动类上增加 @EnableFeignClients(basePackages = "com.baiyan.auth.rpc") 才行。这还只是依赖一个服务,如果依赖的服务很多,那定义的路径也太多了。
因此每个服务提供方在所要提供出去的rpc包中定义这样一个自动配置类,让使用者无需添加任何配置,只要引入提供方的Maven依赖即可实现对提供方的访问。
在第二篇多模块的文章下面有个小老弟提问
api中定义的rpc接口是需要区分于controller中定义的web接口的。
api中定义的http接口是符合rpc结构定义规范的,在nginx层面是不会开放这部分接口的。api包内的http接口仅为其他服务提供业务支撑,并不是用户接口。而在controller层的http接口是用户接口,两者在应用场景上是有区别的。
现在我们有一个用户详情的接口,在interaction的controller包中已经定义了一个接口。
api包中对于此部分的接口返回值是一样的,我们能够直接把api中rpc接口的实现指向这个controller吗?
不能!
为啥?
1.api定义的接口可能有10个方法,controller中可能只有9个方法,在接口实现上有差异。有的同学可能会说接口可以定义默认实现,返回空值。
比如在api包中定义
@PostMapping("by_accessKey")
default UserDTO getUserDetail(@RequestParam("accessKey") String accessKey){return null;}
想想也不合理,第一api只是一层壳,你让壳做了默认实现。第二controller与api的结构无法匹配。
2.用户实现与rpc实现被捆绑。假设用户接口需要做升级,修改几个字段。但是RPC缺并不需要修改原有的逻辑与返回,这个时候rpc接口将会被迫去修改。rpc一旦deploy到中央仓库被其他服务使用,你的任意改动,其他服务都会感知到,从服务提供的了历史兼容角度来看,我们也应该将接口分开。
本篇从微服务中如何使用openFeign触发,为大家介绍了RPC提供方与RPC使用方常见的使用坑点。从坑点触发,为大家介绍基础架构架构中我们如何定义api与rpc包。从底层rpc包的作用触发,介绍了业务服务在使用rpc包时,我们需要注意的点:
基于上述的框架构造的微服务,服务之间基于openFeign的调用都不需要在调用方内定义提供方的数据结构与接口。调用方可以通过引入提供方的rpc包,开箱即用提供方的rpc接口,此刻纵享丝滑。
如果你觉得文章写得不错,点赞评论+关注,么么哒~
微信:baiyan_lou
我的第一本掘金小册《深入浅出DDD》已经在掘金上线,欢迎大家试读~ DDD的微信群我也已经建好了,由于文章内不能放二维码,大家可以加我微信,备注DDD交流,我拉你进群,欢迎交流共同进步。