远程服务调用
学习目标
在这里,你将系统学习了解 远程服务调用
的具体代码实现
我们将以最简单直接
的方式为您呈现内容!
# 🍚 需求分析
在先前的网关实现中,我们已实现基于路由配置的请求转发到后端接口。但随着新需求的发展,我们希望 memory-gateway 服务的代码量保持简洁。为此,我们考虑将核心业务逻辑,如用户身份验证、接口存在性检查等,移至其他微服务中实现,而 memory-gateway 仅作为外部服务的调用者。
这涉及到分布式服务架构下的跨服务通信问题,关键在于如何在微服务间实现跨服务方法调用。为满足这一需求,我们将引入 RPC 框架进行远程调用。通过 RPC 框架,各微服务可以相互调用方法,实现高效、可靠的跨服务通信。
# 🍜RPC 框架
# 核心功能
- RPC(Remote Procedure Call,远程过程调用)框架是一种实现远程过程调用的技术,它允许在不同的计算机或网络中的进程或应用程序相互通信和交互。RPC 框架通常提供了一种机制,使得客户端可以像调用本地函数或方法一样调用远程服务。
# RPC 框架实现
# 🥣 Dubbo
# 介绍
- Dubbo 是一个高性能、轻量级的 Java RPC 框架,由阿里巴巴公司开发并开源。它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。
- Dubbo 框架使得服务提供者能够轻松地暴露服务,而服务消费者能够方便地调用远程服务。它简化了分布式系统的开发,使得系统更加灵活、可扩展和高效。
接下来我们将在本项目中引入 Dubbo 框架
进行远程调用。通过 Dubbo 框架,各微服务可以相互调用方法,实现高效可靠的跨服务通信
。
# 服务实现
在 memory-backend-core-service
服务中,分别实现以下核心功能:
- 内部接口服务,判断请求的模拟接口是否存在。
/**
* 内部接口服务实现类
*
* @author memory
*/
@DubboService
public class InnerInterfaceInfoServiceImpl implements InterfaceInfoDubboService {
@Resource
private InterfaceInfoMapper interfaceInfoMapper;
/**
* 请求的模拟接口是否存在
*
* @param url 请求 url
* @param method 请求 方法
* @return 接口信息
*/
@Override
public InterfaceInfo getInterfaceInfo(String url, String method) {
if (StringUtils.isAnyBlank(url, method)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
QueryWrapper<InterfaceInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("url", url);
queryWrapper.eq("method", method);
// 获取接口信息
return interfaceInfoMapper.selectOne(queryWrapper);
}
}
- 内部用户服务,判断用户调用接口所使用的 accessKey 和 secretKey 是否存在。
/**
* 内部用户服务实现类
*
* @author memory
*/
@DubboService
public class UserDubboServiceImpl implements UserDubboService {
@Resource
private UserMapper userMapper;
/**
* 数据库中查是否已分配给用户秘钥(accessKey)
* @param accessKey
* @return
*/
@Override
public User getInvokeUser(String accessKey) {
if (StringUtils.isAnyBlank(accessKey)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("accessKey", accessKey);
return userMapper.selectOne(queryWrapper);
}
}
- 内部用户接口服务,实现接口调用次数统计。
/**
* 内部用户接口信息服务实现类
*
* @author memory
*/
@DubboService
public class UserInterfaceInfoDubboServiceImpl implements UserInterfaceInfoDubboService {
@Resource
private UserInterfaceInfoService userInterfaceInfoService;
/**
* 接口调用统计
*
* @param interfaceInfoId 接口 id
* @param userId 用户 id
* @return 是否调用成功
*/
@Override
public boolean invokeCount(long interfaceInfoId, long userId) {
return userInterfaceInfoService.invokeCount(interfaceInfoId, userId);
}
}
我们将使用 Nacos 作为注册中心,权限校验相关代码逻辑全部统一放到 Gateway 网关过滤器中完成,并使用 Dubbo 实现内部服务调用。
在保证 memory-backend-core-service
服务的代码量简洁的同时,实现分布式服务架构下的跨服务通信
功能。
请确保 该项目的 Nacos 正常启动并已经能够投入使用。
# 依赖导入
在 memory-backend-core-service
和memory-backend-gateway
服务中均导入以下依赖坐标;
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo</artifactId>
<version>3.1.8</version>
</dependency>
# 配置管理
memory-backend-core-service
服务作为服务提供者,在其项目配置文件 appication.yml 中作如下配置:
# Dubbo
dubbo:
application:
name: api-invoke-server
protocol:
name: dubbo
port: -1
registry:
id: api-invoke
address: nacos://localhost:8848
memory-backend-gateway
服务作为服务消费者,在其项目配置文件 appication.yml 中作如下配置:
# Dubbo
dubbo:
application:
name: api-invoke-customer
protocol:
name: dubbo
port: -1
registry:
id: api-invoke
address: nacos://localhost:8848
并分别在memory-backend-core-service
服务和memory-backend-gateway
服务启动类上添加 @EnableDubbo
注解,如下:
@EnableDubbo
public class CoreApplication {
public static void main(String[] args) {
ConfigurableApplicationContext ctx = SpringApplication.run(CoreApplication.class, args);
long start = System.currentTimeMillis();
long time = (System.currentTimeMillis() - start) / 1000;
String info = "启动完成,耗时%d秒,swagger访问链接:http://%s:%s%s/doc.html";
String address = ctx.getEnvironment().getProperty("server.address");
String port = ctx.getEnvironment().getProperty("server.port");
String path = ctx.getEnvironment().getProperty("server.servlet.context-path");
log.info(String.format(info, time, address, port, path));
}
}
@EnableDubbo
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
# 服务注册
分别在memory-backend-core-service
服务实现的内部接口服务,内部用户服务和内部用户接口服务中,添加 DubboService
注解,如下:
/**
* 内部接口服务实现类
*
* @author memory
*/
@DubboService
public class InnerInterfaceInfoServiceImpl implements InterfaceInfoDubboService {
................................
}
/**
* 内部用户服务实现类
*
* @author memory
*/
@DubboService
public class UserDubboServiceImpl implements UserDubboService {
................................
}
/**
* 内部用户接口信息服务实现类
*
* @author memory
*/
@DubboService
public class UserInterfaceInfoDubboServiceImpl implements UserInterfaceInfoDubboService {
................................
}
用最简洁的代码成功实现了服务注册,服务注册完成后即可在 Gateway 网关处实现服务消费。
# 服务消费
在 Gateway 全局过滤器 CustomGlobalFilter
下,使用 Spring 提供的 IOC 功能
,实现远程服务注入:
/**
* GateWay 全局过滤器
*/
@Slf4j
@Component
public class CustomGlobalFilter implements GlobalFilter, Ordered {
/**
* 使用 Dubbo 实现接口远程调用
*/
@DubboReference
private UserDubboService;
@DubboReference
private InterfaceInfoDubboService;
@DubboReference
private UserInterfaceInfoDubboService;
................................
}
在 Gateway 全局过滤器 CustomGlobalFilter
中,分别实现用户身份校验
,接口信息校验
和接口调用次数统计
功能:
// 3.2.校验accessKey
// todo 从数据库中查询, accessKey是否分配给该用户
if (accessKey == null || !accessKey.equals("memory")) {
return handleNoAuth(response);
}
// accessKey 是否分配给该用户
User invokeUser = userDubboService.getInvokeUser(accessKey);
// 4. 请求的模拟接口是否存在
InterfaceInfo interfaceinfo = interfaceInfoDubboService.getInterfaceInfo(path, method);
// 6. 接口调用统计,接口调用次数 + 1, 用户积分 - 1
try {
// userInterfaceInfoDubboService.invokeCount(interfaceInfoId, userId);
userInterfaceInfoFeignClient.invokeCount(interfaceInfoId, userId);
} catch (Exception e) {
log.error("invokeCount error", e);
}
启动 Nacos,可以看到这样的页面,说明服务生产者和服务消费者成功连接至 本地启动的 Nacos 注册中心:
至此,服务业务逻辑实现,服务生产者以及服务消费者已全部配置完毕,成功使用 Dubbo 实现分布式服务架构下的跨服务通信
功能。
# 🍜 OpenFeign
经过前面的学习,我们基本了解在实际项目中,如何正确使用 Dubbo 框架进行远程服务调用,实现跨服务间通信。
与之相比,使用 OpenFeign 实现远程服务调用,二者之间还存在些许差距。
# 依赖导入
在 memory-backend-core-service
和memory-backend-gateway
服务中均导入以下依赖坐标;
<!-- 添加 openfeign 框架依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
# Feign 客户端
接下来,完成内部间调用服务(用户身份校验
,接口信息校验
和接口调用次数统计
)的相关接口编写,即完善 Feign 客户端
:
- 内部接口服务,判断请求的模拟接口是否存在:
/**
* 内部接口信息服务
*/
@FeignClient(name = "memory-core", path = "/api/interface/inner")
public interface InterfaceInfoFeignClient {
/**
* 从数据库中查询模拟接口是否存在(请求路径、请求方法、请求参数)
*
* @param path
* @param method
* @return
*/
@GetMapping("/get/interfaceInfo")
InterfaceInfo getInterfaceInfo(@RequestParam(value = "path") String path, @RequestParam(value = "method") String method);
}
- 内部用户服务,判断用户调用接口所使用的 accessKey 和 secretKey 是否存在:
/**
* 内部用户服务
*/
@FeignClient(name = "memory-core", path = "/api/user/inner")
public interface UserFeignClient {
/**
* 数据库中查是否已分配给用户秘钥(accessKey)
*
* @param accessKey
* @return
*/
@GetMapping("/get/invoke")
User getInvokeUser(@RequestParam(value = "accessKey") String accessKey);
}
- 内部用户接口服务,实现接口调用次数统计:
/**
* 内部用户接口信息服务
*/
@FeignClient(name = "memory-core", path = "/api/user/interface/inner")
public interface UserInterfaceInfoFeignClient {
/**
* 调用接口统计
*
* @param interfaceInfoId
* @param userId
* @return
*/
@GetMapping("/get/invoke/count")
boolean invokeCount(@RequestParam(value = "interfaceInfoId") long interfaceInfoId, @RequestParam(value = "userId") long userId);
}
这里要特别注意 @FeignClient
注解的使用方式:
name
属性值为注册中心内所注册的服务名,与该微服务在 Nacos 中的服务配置相对应。path
属性值为将来访问该 Feign 客户端时的访问路径,用来标记服务资源地址。
比方说 memory-backend-core-service
服务的 Nacos 相关配置是这样的:
# core
spring:
application:
name: memory-core
# dev
profiles:
active: dev
# session
session:
# store-type: redis
timeout: 2592000
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
config:
import-check:
enabled: false
在 memory-backend-core-service
服务中,根据已经完善的 Feign 客户端,构造实现对应的服务接口:
- 内部接口信息服务接口:
/**
* 内部接口信息服务
* 该服务仅内部调用,不是给前端的
*/
@RestController
@RequestMapping("/interface/inner")
public class InterfaceInnerController implements InterfaceInfoFeignClient {
@Resource
private InterfaceInfoService interfaceInfoService;
/**
* 从数据库中查询模拟接口是否存在(请求路径、请求方法、请求参数)
*
* @param path
* @param method
* @return
*/
@Override
@GetMapping("/get/interfaceInfo")
public InterfaceInfo getInterfaceInfo(String path, String method) {
return interfaceInfoService.getInterfaceInfo(path, method);
}
}
- 内部用户接口信息服务接口:
/**
* 内部用户服务
* 该服务仅内部调用,不是给前端的
*/
@RestController
@RequestMapping("/user/inner")
public class UserInnerController implements UserFeignClient {
@Resource
private UserService userService;
/**
* 数据库中查是否已分配给用户秘钥(accessKey)
* @param accessKey
* @return
*/
@Override
@GetMapping("/get/invoke")
public User getInvokeUser(@RequestParam(value = "accessKey") String accessKey) {
return userService.getInvokeUser(accessKey);
}
}
- 内部用户接口信息服务接口:
/**
* 内部用户接口信息服务
* 该服务仅内部调用,不是给前端的
*/
@RestController
@RequestMapping("/user/interface/inner")
public class UserInterfaceInnerController implements UserInterfaceInfoFeignClient {
@Resource
private UserInterfaceInfoService userInterfaceInfoService;
/**
* 接口调用统计
*
* @param interfaceInfoId 接口 id
* @param userId 用户 id
* @return 是否调用成功
*/
@Override
@GetMapping("/get/invoke/count")
public boolean invokeCount(long interfaceInfoId, long userId) {
return userInterfaceInfoService.invokeCount(interfaceInfoId, userId);
}
}
至此,一个完整的 Feign 客户端编写完成,业务代码核心功能基本实现。
我们将使用 Nacos 作为注册中心,权限校验相关代码逻辑全部统一放到 Gateway 网关过滤器中完成,并使用 OpenFeign 实现内部服务调用。
在保证 memory-backend-core-service
服务的代码量简洁的同时,实现分布式服务架构下的跨服务通信
功能。
请确保 该项目的 Nacos 正常启动并已经能够投入使用。
# 服务注册
并分别在memory-backend-core-service
服务和memory-backend-gateway
服务启动类上添加 @EnableDubbo
注解,如下:
@EnableFeignClients(basePackages = "com.memory.client.feignClient")
public class CoreApplication {
public static void main(String[] args) {
ConfigurableApplicationContext ctx = SpringApplication.run(CoreApplication.class, args);
long start = System.currentTimeMillis();
long time = (System.currentTimeMillis() - start) / 1000;
String info = "启动完成,耗时%d秒,swagger访问链接:http://%s:%s%s/doc.html";
String address = ctx.getEnvironment().getProperty("server.address");
String port = ctx.getEnvironment().getProperty("server.port");
String path = ctx.getEnvironment().getProperty("server.servlet.context-path");
log.info(String.format(info, time, address, port, path));
}
}
@EnableFeignClients(basePackages = "com.memory.client.feignClient")
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
已经成功注册上述服务至 Nacos 注册中心,接下来尝试在 memory-backend-gateway
服务中使用 Feign 客户端调用memory-backend-core-service
服务提供的内部接口。
# 服务消费
在 Gateway 全局过滤器 CustomGlobalFilter
下,实现 Feign 客户端
注入:
/**
* GateWay请求过滤器
*/
@Slf4j
@Component
public class CustomGlobalFilter implements GlobalFilter, Ordered {
/**
* 我们使用OpenFeign 实现接口远程调用
*/
@Resource
private UserFeignClient userFeignClient;
@Resource
private InterfaceInfoFeignClient interfaceInfoFeignClient;
@Resource
private UserInterfaceInfoFeignClient userInterfaceInfoFeignClient;
}
在 Gateway 全局过滤器 CustomGlobalFilter
中,分别实现用户身份校验
,接口信息校验
和接口调用次数统计
功能:
// accessKey 是否分配给该用户
User invokeUser = userFeignClient.getInvokeUser(accessKey);
if (invokeUser == null) {
return handleNoAuth(response);
}
// 4. 请求的模拟接口是否存在
InterfaceInfo interfaceInfo = interfaceInfoFeignClient.getInterfaceInfo(path, method);
if (interfaceInfo == null) {
return handleNoAuth(response);
}
// 6. 接口调用统计,接口调用次数 + 1, 用户积分 - 1
try {
userInterfaceInfoFeignClient.invokeCount(interfaceInfoId, userId);
} catch (Exception e) {
log.error("invokeCount error", e);
}
启动 Nacos,可以看到这样的页面,说明服务生产者和服务消费者成功连接至 本地启动的 Nacos 注册中心:
至此,服务业务逻辑实现,服务生产者以及服务消费者已全部配置完毕,成功使用 OpenFeign 实现分布式服务架构下的跨服务通信
功能。
# 抽象服务
经过考虑,我最终决定抽象远程调用服务,即抽象 Feign 客户端为该项目内的memory-backend-server-client
服务,并注册服务到 Nacos 注册中心中,管理更加便捷:
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
想要了解更多实践 Dubbo 和 Openfeign 实现跨服务通信时的踩坑记录和经验技巧,可以尝试访问:从零开始构建分布式服务架构:用 Dubbo
和注册中心实现远程调用、服务注册与发现、配置管理 - Memory's blog (atomgit.net) (opens new window)