项目背景介绍
这个技术我是直接在项目中运用并且学习的,所以我写笔记最优先的角度就是从项目背景出发
继上一次API网关完成了这个实现用户调用一次接口之后让接口次数增多的操作之后,又迎来了新的问题。
就是我们在调用接口的时候需要对用户进行校验,对调用的接口是否存在进行验证。
从这个需求出发,我们第一反应想到的解决办法是什么,应该是在api-gateway项目中也来引入一下这个数据库配置,包括三层架构重新写一轮,包括实体类也需要引入一下
这样做的问题其实很明显,就是代码很冗余,如果以后微服务的模块开发多了,那每个模块都需要引入这个数据库巴拉巴拉一堆配置,这就很麻烦,也不易于维护
所以从实际需求出发,我们就需要用到这个微服务中的远程调用的操作(我感觉对于微服务来说这其实应该是一个很基础的操作,但是我写这个项目的时候,微服务还没学到远程调用)
首先我们可以先来简单想一下这个我们如果想在其它项目中使用其它项目的方法,我们可以怎么做
- 我们可以用笨办法,直接cv,然后搭环境配依赖
- HTTP 请求(提供一个接口,供其他项目调用)(这个方式我没有用过,看鱼皮的笔记先进行一个记录)
- 第三个就是RPC
- 第四个其实我们也肯定知道,就是打个jar包,这也有点类似于自定义start的方式
根据项目背景我们就可以想到用RPC和Dubbo框架来实现远程调用的方式了。
简单的概念介绍:
什么是RPC?
作用:像调用本地方法一样调用远程方法。
和直接 HTTP 调用的区别:
- 对开发者更透明,减少了很多的沟通成本。
- RPC 向远程服务器发送请求时,未必要使用 HTTP 协议,比如还可以用 TCP / IP,性能更高。(内部服务更适用)
什么理解这个像调用本地方法一样调用远程方法,就是比如我们在我们的项目中的三层架构,controller直接调用mapper那样方便。
远程调用的流程:
为什么可以远程调用呢,
首先我们明白调用,肯定是有一方提供方法,一方调用你提供的方法
这里就会有三个角色,提供者调用方和注册中心
提供者根据地址先将方法注册到注册中心,
注册中心进行一个存储,
然后调用方也根据地址去注册中心里面去取你注册到里面的方法
这里的注册中心我用的是后面会记录的nacos。
什么是Dubbo框架:
这个是官方的基于springboot的案例3 - 基于 Spring Boot Starter 开发微服务应用 | Apache Dubbo
这里也有点坑,官方的这个案例里面用的是nacos,如果你没有下载和开启nacos啊,直接报错
我搞这个搞了半天才发现要下载nacos
Dubbo 是一个高性能的Java RPC(远程过程调用)框架,最初由阿里巴巴开发并开源。它主要用于构建分布式服务,可以让服务提供者和消费者之间进行高效、安全的通信。
我们简单理解为就是封装了一些方法让我们可以非常简单的使用这个RPC进行远程调用。
nacos注册中心:
Nacos 快速开始 | Nacos 官网
官网上有详细步骤:
1:详细上来说就是下载解压压缩包
2:注册服务并用startup.cmd -m standalone命令开启,注意这里开发需要到nacos的bin目录下打开cmd输入startup.cmd -m standalone开启
3:第三步我们就可以照着这上面的网址进行访问了
进入之后,如果我们有注册方法的话,这里就会有提示。
4:根据Dubbo框架的官方文档来使用nacos注册中心:
Nacos | Apache Dubbo
总体说起来就是、
引入对应依赖(包括dubbo的依赖和nacos的依赖)
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo</artifactId>
<version>3.0.9</version>
</dependency>
<dependency>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-client</artifactId>
<version>2.1.0</version>
</dependency>
在配置文件中配置
# 以下配置指定了应用的名称、使用的协议(Dubbo)、注册中心的类型(Nacos)和地址
dubbo:
application:
# 设置应用的名称
name: dubbo-springboot-demo-provider
# 指定使用 Dubbo 协议,且端口设置为 -1,表示随机分配可用端口
protocol:
name: dubbo
port: -1
registry:
# 配置注册中心为 Nacos,使用的地址是 nacos://localhost:8848
id: nacos-registry
address: nacos://localhost:8848
这些东西直接复制官网就行。
注意点:
这些配置和依赖在provider引入了,在consumer也需要引入,并且需要保持一致
讲完了一些基础概念,下面上一个Demo演示流程
先用Demo会更好入门会更好一点,我刚刚自己重新看了一遍流程,我自己在项目中将需要远程调用的方法抽象成了一个公共模块,所以,感觉理解起来会有点难。
实战:
Demo案例:
在实战之前我们需要引入上面的依赖
并且在springboot的启动类上加上
@EnableDubbo
@SpringBootApplication
@MapperScan("com.yupi.project.mapper")
@EnableDubbo
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
provider和consumer的启动类都要加
首先我们从远程调用的过程入手。
我们首先需要一个provider来提供方法
这里的提供者是master这个项目
我们在这个项目中新建了一个软件包叫provider,里面很简单就是一个接口来规定调用的方法
一个实现这个接口的实现类来重写里面的方法。
import java.util.concurrent.CompletableFuture;
public interface DemoService {
String sayHello(String name);
String sayHello2(String name);
default CompletableFuture<String> sayHelloAsync(String name) {
return CompletableFuture.completedFuture(sayHello(name));
}
}
package com.yupi.project.provider;
import org.apache.dubbo.config.annotation.DubboService;
import org.apache.dubbo.rpc.RpcContext;
@DubboService
public class DemoServiceImpl implements DemoService {
@Override
public String sayHello(String name) {
System.out.println("Hello " + name + ", request from consumer: " + RpcContext.getContext().getRemoteAddress());
return "Hello " + name;
}
@Override
public String sayHello2(String name) {
return "ljh";
}
}
很简单两个方法:输出hello和一个字符串ljh
根据流程下一步得到注册中心了:
看一下注册中心里面的信息:
下一步就是consumer了
consumer就是这里的gateway项目:
首先我同样创建了一个名字一样的软件包,里面也有一个DemoService来接收这个提供者提供的方法。
package com.yupi.project.provider;
import java.util.concurrent.CompletableFuture;
public interface DemoService {
String sayHello(String name);
String sayHello2(String name);
default CompletableFuture<String> sayHelloAsync(String name) {
return CompletableFuture.completedFuture(sayHello(name));
}
}
没有看错和上面的provider就是一模一样的。
调用该接口中的方法实在启动类中
这里放在启动类没有什么特殊含义,单纯就是懒得再开一个软件包而已
@SpringBootApplication(exclude = {
DataSourceAutoConfiguration.class,
DataSourceTransactionManagerAutoConfiguration.class,
HibernateJpaAutoConfiguration.class})
@EnableDubbo
@Service
public class ApiSpringcloudgatewayApplication {
@DubboReference
private DemoService demoService;
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(ApiSpringcloudgatewayApplication.class, args);
ApiSpringcloudgatewayApplication application = context.getBean(ApiSpringcloudgatewayApplication.class);
String result = application.doSayHello("world");
String result2 = application.doSayHello2("world");
System.out.println("result: " + result);
System.out.println("result: " + result2);
}
public String doSayHello(String name) {
return demoService.sayHello(name);
}
public String doSayHello2(String name) {
return demoService.sayHello2(name);
}
@Bean
public GlobalFilter customFilter() {
return new CustomGlobalFilter();
}
}
我们从上到下分析,首先加上@EnableDubbo这个需要记得
@DubboReference
private DemoService demoService;
我觉得着有点像spring的依赖注入,但可能这个是Dubbo框架提供的赋值的方式。
ConfigurableApplicationContext context = SpringApplication.run(ApiSpringcloudgatewayApplication.class, args);
ApiSpringcloudgatewayApplication application = context.getBean(ApiSpringcloudgatewayApplication.class);
这两行就是获取spring的bean工厂,之前在研究spring的源码的时候,就有了解过,bean工厂
然后获取了bean工厂之后再从工厂里面获取对应的bean对象。
接着就是调用provider提供的方法了。
输出结果:
接下来就是项目实战了,这个项目背景就是API开发平台的项目。
项目实战:
首先我在上面介绍的时候有提到,我对这个公共模块进行了抽取。
为什么会想到这个呢
我们仔细看上面的Demo案例,
我们的DemoService是不是在provider和consumer都写了一次。
我们基于这个操作,我们是不是就可以想到我们可以将这个模块抽取出来
再者,抽取公共模块也可以更好的就是规范我们的业务流程。
所以在项目实战之前
我们先抽取一下公共模块
抽取公共模块并在provider中提供接口:
抽象公共模块的步骤
1:想清楚什么方法需要抽象,或者说什么方法是公共,还有其它模块是需要使用的
2:想清楚实体类规整到公共模块还是原来的单体架构模块
3:打包引入依赖,引入依赖之后,需要照着公共的接口创建实现改接口的实体类
根据上面的步骤(其实这个步骤并不是什么规范步骤,就是我自己在抽取的时候遇到了问题,后面复盘总结起来的步骤)
先说一下,一般微服务抽取公共项目的名称都叫什么comon,所以我这里也取名叫api-common
想一下,我们需要抽取什么?
第一个就是我们在项目中需要其它项目引用的方法:
那得结合一下我们的业务
我们在网关的过滤器中,我们需要
1:对用户进行API签名的认证,这个的具体步骤是根据用户在请求头中的accessKey从数据库中查出用户的secretKey,然后结合用户的id+secretKey计算sign,和请求头中发过来的sign进行比对校验。具体更详细步骤在API网关认证哪一章有具体讲
2:查询接口是否存在,这个就是根据接口id到数据库中进行查询
3:调用次数+1,这个就是到UserInterfaceInfo中讲totalNum+1,讲LeftNum-1即可
分析了业务之后,发现我们的需要的方法:
/**
* 内部接口信息服务
*
*/
public interface InnerInterfaceInfoService {
/**
* 从数据库中查询模拟接口是否存在(请求路径、请求方法、请求参数)
*/
InterfaceInfo getInterfaceInfo(String path, String method);
}
/**
* 内部用户接口信息服务
*
*
*/
public interface InnerUserInterfaceInfoService {
/**
* 调用接口统计
* @param interfaceInfoId
* @param userId
* @return
*/
boolean invokeCount(long interfaceInfoId, long userId);
}
import com.yupi.model.entity.User;
/**
* 内部用户服务
*
*/
public interface InnerUserService {
/**
* 数据库中查是否已分配给用户秘钥(accessKey)
* @param accessKey
* @return
*/
User getInvokeUser(String accessKey);
}
第二个就是实体类
第二个就是上面这三个接口中要用到的实体类
想清楚实体类规整到公共模块还是原来的单体架构模块
这个点就是我在抽取的时候没搞清楚的地方了,就是我有的实体类在我master项目中,
有的在common公共模块中
我后面就索性全部改到common公共模块中
并且将master项目中的mapper和mapper,xml都进行了修改
打包引入依赖,引入依赖之后,需要照着公共的接口创建实现改接口的实体类
这里的打包引入依赖就和自定义starter一样。
照着公共的接口创建实现改接口的实体类:
这里当然也可以调用原来的service的方法
不过为了规范一点可以新建一个inner软件包
@DubboService//@DubboService注解是加在你想要暴露出去的方法上的
public class InnerInterfaceInfoServiceImpl implements InnerInterfaceInfoService {
@Resource
private InterfaceInfoMapper interfaceInfoMapper;
@Override
public InterfaceInfo getInterfaceInfo(String url, String method) {
if (StringUtils.isAnyBlank(url, method)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
LambdaQueryWrapper<InterfaceInfo> lambdaQueryWrapper = new LambdaQueryWrapper<>();
LambdaQueryWrapper<InterfaceInfo> eq = lambdaQueryWrapper.eq(InterfaceInfo::getUrl, url)
.eq(InterfaceInfo::getMethod, method);
InterfaceInfo interfaceInfo = interfaceInfoMapper.selectOne(eq);
return interfaceInfo;
}
}
/**
* 内部用户接口信息服务实现类
*
*/
@DubboService
public class InnerUserInterfaceInfoServiceImpl implements InnerUserInterfaceInfoService {
@Resource
private UserInterfaceInfoService userInterfaceInfoService;
@Override
public boolean invokeCount(long interfaceInfoId, long userId) {
return userInterfaceInfoService.invokecount(interfaceInfoId,userId);
}
}
/**
* 内部用户服务实现类
*
*/
@DubboService
public class InnerUserServiceImpl implements InnerUserService {
@Resource
private UserMapper userMapper;
@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);
}
}
注意要在这些类上加上 @DubboService注解,注册到nacos注册中心中
等抽取完公共模块之后,剩下的步骤就很简单了
毕竟RPC的宣称就是像调用本地方法一样调用远程方法
consumer调用远程接口:
@Slf4j
public class CustomGlobalFilter implements GlobalFilter, Ordered {
@DubboReference
private InnerUserService innerUserService;
@DubboReference
private InnerInterfaceInfoService innerInterfaceInfoService;
@DubboReference
private InnerUserInterfaceInfoService innerUserInterfaceInfoService;
private static ArrayList<String> IP_WHITE_LIST = new ArrayList<>();
private static final String INTERFACE_HOST = "http://localhost:8123";
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//1:请求日志
//2:访问控制 -黑白名单
//3:用户鉴权
final HttpHeaders headers = request.getHeaders();
final String accessKey = headers.getFirst("accessKey");
String random = headers.getFirst("random");
String timestamp = headers.getFirst("timestamp");
String sign = headers.getFirst("sign");
String body = headers.getFirst("body");
final User invokeUser = innerUserService.getInvokeUser(accessKey);
if(invokeUser==null){
//user==null,说明这个accessKey根本没有被分配给用户
throw new RuntimeException("accessKey不存在");
}
if(Long.parseLong(random) > 10000){
handleNoAuth(response);
}
//通过时间戳判断是否过期
long currentTimeMillis = System.currentTimeMillis()/1000;
long differenceInSeconds = (long) (currentTimeMillis/1000 - Long.parseLong(timestamp));
if(differenceInSeconds > 300){
handleNoAuth(response);
}
final String secretKey = invokeUser.getSecretKey();
String flag = SingUtils.getSign(body, secretKey);
if(!flag.equals(sign)){
handleNoAuth(response);
}
final Long userId = invokeUser.getId();
//4:从数据库中查询模拟接口是否存在
System.out.println(path);
System.out.println(method);
InterfaceInfo interfaceInfo = innerInterfaceInfoService.getInterfaceInfo(path,method);
if(interfaceInfo==null){
handleNoAuth(response);
}
final Long interfaceInfoId = interfaceInfo.getId();
//5:请求转化,调用模拟接口
final Mono<Void> filter = chain.filter(exchange);//在这个方法中调用次数+1
return handleResponse(exchange,chain,42L,userId);
}
}
源代码很多
流行需要调用远程方法的部分
在调用之前,我们需要先注入一下,这三个接口
就和之前的DemoService一样