Api-gateway-core
# Api-gateway-core
# 1、服务端处理Http请求
功能:实现将Http连接请求过程引入Netty服务端IO。核心在于通道需要配置Netty自带的Http解码器和编码器,获取Http对象后用户可以进一步处理。
channelRead0:事件触发后,封装DefaultFullHttpResponse响应对象,包括配置响应体和响应头信息,以及解决跨域问题。另外,channelRead0事件中不需要关心释放资源。
SimpleChannelInboundHandler<FullHttpRequest>:事件处理器接口,指定FullHttpRequest作为接收的消息对象。在页面和服务端之间的通道取出http请求对象,可以通过uri()方法拿到后面资源标识路径。

# 2、代理RPC泛化调用
# 网关接口映射功能
SpringBoot在Controller中通过注解RequestMapping,把Http请求路径映射到每个微服务具体方法,但同时也增加了维护成本,服务改变那么访问路径也需要改变。
此处采用Netty+Dubbo的全新微服务架构方式,所有包装提供的微服务只需要通过Dubbo暴露接口方法,不需要配置请求路径。当用户发起http请求时会发送给Api网关(此处网关基于Netty服务端与所有用户请求建立连接),网关会根据配置的映射规则,向微服务注册中心发送Dubbo-RPC请求,调用到对应的服务接口方法。因此核心就是配置HTTP和RPC的映射规则。

# 泛化调用
- 获取泛化服务调用对象
DubboBootstrap对象提供好应用名称、注册中心配置、引用服务配置,进行初始化和远程连接后,就可以从Dubbo配置缓存中取出该接口的泛化调用代理对象GenericService。
消费服务时只需要调用GenericService#$invoke方法,并提供方法名、入参类型、入参对象,即可调用RPC服务接口的方法。
- 泛化调用对象的二次封装
每个RPC服务的实现和方法名称都是不同的,因此不能直接调用GenericService#$invoke,可能还需要拦截方法实现增强。因此这里对genericService代理对象二次封装,再创建一个新的代理对象。
GenericReferenceProxyFactory:代理工厂采用Cglib的动态代理方式,其中设置了两个代理接口,一个是统一的泛化调用接口IGenericReference,给整个网关调用提供一个统一的方法。而另一个接口是手动生成创建的RPC描述性接口,标记当前代理对象间接代理的是哪个RPC接口方法。
public IGenericReference newInstance(String method) {
return genericReferenceMap.computeIfAbsent(method, k -> {
//泛化调用
GenericReferenceProxy genericReferenceProxy = new GenericReferenceProxy(genericService, method);
//创建接口,指定名称,返回类型,行参类型
InterfaceMaker interfaceMaker = new InterfaceMaker();
interfaceMaker.add(new Signature(method, Type.getType(String.class), new Type[]{Type.getType(String.class)}), null);
Class interfaceClass = interfaceMaker.create();
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Object.class);
//设置统一泛化调用接口+手动创建RPC接口
enhancer.setInterfaces(new Class[]{IGenericReference.class, interfaceClass});
enhancer.setCallback(genericReferenceProxy);
return (IGenericReference) enhancer.create();
});
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
GenericReferenceProxy:泛用代理对象,实现了MethodIntercptor接口,用于增强RPC接口方法(设置搜集入参信息),并设置代理对象的回调setCallback。当网关的泛化代理对象调用$invoke后,会进入到intercept进行拦截增强,并通过外部的method方法搜集入参信息,最后通过genericService.$invoke调用真正的RPC接口。
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
Class<?>[] parameterTypes = method.getParameterTypes();
String[] parameters = new String[parameterTypes.length];
for (int i = 0; i < parameterTypes.length; i++) {
parameters[i] = parameterTypes[i].getName();
}
return genericService.$invoke(methodName, parameters, objects);
}
2
3
4
5
6
7
8
9
# 缓存设计
①Configuration:因为ApplicationConfig,RegistryConfig,ReferenceConfig三种类型对象创建成本高,因此需要存放入内存Map中管理。后面需要从Spring配置文件当中读取。
②ReferenceConfigCache:因为ReferenceConfig对象是一个比较重的实例,其中init初始化方法里实现了通过与注册中心与提供者连接创建代理的过程。因此需要创建缓存,根据referenceConfig对象生成的key获取对应的泛化代理对象。最终Dubbo-RPC代理对象是通过Javassist字节码增强技术创建。
此处key的生成规则是group/版本/接口,可以根据业务和场景需求通过SPI修改生成策略。
③网关代理工厂中也设置了Map作为网关代理对象的单例池,根据方法名获取对应的代理对象。
# 思考
①RPC接口调用的入参在http请求路径上(REST)如何获取:通过uri()解析获得请求参数。form表单数据呢?
②网关代理单例池存放新创建的代理对象时,出现同名的RPC方法调用时会覆盖。
③SPI在dubbo的使用,加载ConfigManager,修改注册中心生成key方法
# 3、重构:使用分治方法重构会话
- 目标
对工程进行重构,拆分成bind、mapping、session、socket四部分。
①bind:负责网关代理封装与创建。(移除了泛化调用部分)
②mapping:HttpStatement对象建立了HTTP网关(uri和请求方式)到RPC服务(应用、接口、方法名)的映射关系。并且都采用String类型,避免使用比较重的Dubbo实例。
③session:会话中间层,包括缓存、泛化调用获取。
④socket:负责Netty服务端网络通信。
整个流程首先session准备配置,socket启动服务端,从session中通过mapperRegistry获取映射器对象(网关代理对象),通过代理对象调用RPC接口服务。

- 细节
各个模块功能是隔离的却又不是完全孤立的,通过调用依赖的方式实现模块之间的通信。
DefaultGatewaySessionFactory:socket通信模块启动Netty服务端依赖于session的会话工厂。
MapperRegistry:session模块Configuration依赖于bind模块的映射器注册中心。getMapper方法又会基于GatewaySession中的configuration对象创建新的代理实例。
另外bind代理部分的缓存Map中的key全部改为用uri来代替,解决了RPC接口方法重名的问题。
核心看MapperProxy#intercept拦截方法的实现。此处把RPC远程连接注册中心并从Dubbo缓存取出GenericReference对象,以及通过泛化调用对象调用RPC接口$invoke两个过程合并在一起,放到MapperMethod实现。
- 问题
①MappingRegistry和Configuration存在循环依赖问题。HttpStatement在初始化阶段就准备好了,因此可以直接放入Configuration缓存,不需要在添加映射器时再进行,所以MappingRegistry注册中心不需要依赖Configuration。
②将泛化调用对象的获取和RPC接口调用绑定的问题在于,GatewaySession#get方法没有缓存GenericReference对象,而获取GenericReference对象需要远程调用,是一个很重的方法。因此用户第一次调用服务会非常慢,此外每次调用相同RPC接口都会进行远程调用,十分损耗系统资源。
所以这个地方必须要拆分,把GenericReference对象获取和初始化过程放到Netty服务端启动之前,和Configuration一起进行初始化。不过这种做法相当于一次性往内存加载所有RPC服务,拿空间换时间,具体需要结合时机场景考虑。
# 4.重构:引入数据源重构RPC连接
- 目标
把RPC远程连接获取GenericService泛化调用对象的部分从DefaultGatewaySession会话中抽离出来,把RPC抽象成一种连接资源,整合放入数据源管理当中。这样针对不同的资源请求,可以扩展不同的数据源连接方式。而在Gatewaysession中需要做的,只需要初始化DataSource数据源,并且在需要调用远程数据或者远程服务的地方getConnection获取数据源连接,进一步拿到数据执行对应的业务处理即可。
- 细节
整个依赖关系:DefaultGatewaySession—>(DateSourceFactory—>DateSource)—>Connection
UnpooledDataSource:根据网关中的HTTP映射信息对象HttpStatement,从配置Configuration中取出建立RPC连接所需要的Dubbo配置对象,并交给连接对象(DubboConnection)进行实例化。
DataSourceType:定义数据源类型
DubboConnection:根据数据源传入的Dubbo配置对象,获取泛化调用对象并创建连接。execute方法执行RPC服务。
数据源的引入:DefaultGatewaySessionFactory#openSession中初始化数据源工厂,并通过工厂获取数据源对象,交给DefaultGatewaySession保存。
- 问题
①RPC服务调用交给DefaultGatewaySession不合适,应该直接由Mapper代理调用connection对象执行。
# 5.HTTP请求参数解析
目标:GatewayServerHandler接收到HTTP请求后,针对HTTP的GET/POST请求类型,以及该请求的参数类型Content-Type进一步细化参数解析过程。
# 请求参数解析
RequestParser#parse:请求参数解析。核心基于FullHttpRequset对象,根据不同的请求方式取出数据。
GET方式:数据可以直接从uri中解析获取。
POST方式:前端数据会以不同的请求体方式提交,因此需要区分请求头中不同的Content-Type信息。另外解析参数类型Content-Type时,过滤时需要考虑到分号后面的boundary或charset。
- multipart/form-data----form-data
- application/json----raw(json)
- application/x-www-from-urlencoded----x-www-from-urlencoded
# RPC接口入参设置
通过泛化对象调用RPC接口时,需要根据不同的入参类型传入正确的入参对象。如果传入的是JAVA八大基本数据类型,那么直接把数据填入到入参对象数组当中;如果传入的是自定义类型对象,那么需要把该对象的所有成员属性封装成一个Map对象,作为RPC接口的一个入参对象。
此处下面的params已经提前转化为Map对象。
return connection.execute(methodName, new String[]{parameterType}, new String[]{"name"}
,SimpleTypeRegistry.isSimpleType(parameterType)?params.values().toArray(): new Object[]{params});
2
另外,此处暂且规定RPC接口只传入一个入参。如果是多入参的情况下,需要拿到RPC接口的Method对应的所有入参参数名,代理调用时需要根据入参名把入参对象放入对应的位置。另外没有解决DTO转化问题。
# 6.重构:引入执行器执行RPC泛化调用
- 目标
将Session会话中获取连接源,并通过连接源执行泛化调用的整个执行过程抽离出来,由连接器负责。
会话只需要串联上下文,包括①根据外部uri信息创建数据源②获取执行器执行结果返回给外部HTTP响应。
- 细节
BaseExecutor#exec:采用模板模式定义整个执行过程:①准备入参信息②执行器通过连接源对象执行泛化调用③封装执行结果对象GatewayResult。
DefaultGatewaySessionFactory#openSession:根据连接源创建执行器对象,根据外部uri开启会话。最后把执行器存入会话中。
ResponseParser:将执行结果统一封装HTTP响应对象。

# 7.Shiro+JWT
HTTP请求进来时需要校验所携带的token,从而够验证当前HTTP请求的有效性,防止恶意请求攻击网关。
# JWT认证流程
用户在第一次登录成功后,后端会通过JWT使用服务器私钥signingKey对用户的uId进行加密编码,得到一串密文作为token,并把token在第一次登录验证成功后(一般是数据库密码匹配)返回给用户。
也就是说uId与token可以理解是一对明文密文对,只有通过服务器(校验方)的私钥才能正确解密。
每次发送HTTP请求时,先从header头部取出uId和token,然后shiro会对明文密文对进行认证校验,通过Jwt的私钥对token进行解密(解码),如果结果与明文(uId)不一致,那么说明校验信息很可能经过伪造,校验失败认证不通过。
# Shiro核心原理
- 认证授权调用链
subject->securityManager->..->Realm。前端校验信息AuthenticationToken最终会传递到Realm对象进行认证。认证时首先调用doGetAuthenticationInfo方法获取真正的账号密码信息,并封装成AuthenticationInfo对象,然后执行doCredentialsMatch方法将前端token信息和数据库账户info信息进行匹配。
而目前来看网关系统①使用认证功能②shiro通过Jwt来对token进行验证,不走数据库
GatewayAuthorizingRealm:自定义Realm对象,继承了AuthorizingRealm,重写doGetAuthenticationInfo方法,在方法中使用Jwt解码token进行校验,如果校验失败则直接抛出异常。暂不走数据库认证。
GatewayAuthorizingToken:实现了AuthenticationToken接口,用于封装前端传入的uId和token。
- 主流程
①工厂对象配置自定义Realm的配置,获取SecurityManager对象。
②在SecurityUtils中设置管理对象,并获取Subject对象。
③Subject#login:将前端校验信息封装成Shiro的token对象,并传入SecurityManager内部,进行认证授权。
public AuthService() {
IniSecurityManagerFactory factory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
this.subject = SecurityUtils.getSubject();
}
@Override
public boolean validate(String id, String token) {
try {
subject.login(new GatewayAuthorizingToken(id, token));
return subject.isAuthenticated();
} finally {
subject.logout();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 会话鉴权融入网关
- 核心
AuthService作为Shiro鉴权的门面对象,暴露validate方法实现认证功能。
另外对Netty服务端的Headler处理器进行重构,拆分成三个模块:
①GatewayServerHandler:校验uri,并根据uri拿到网关映射器HttpStatement,存入通道的AttributeMap。
②AuthorizationHandler:从request头部取出用户校验信息,进行接口鉴权。
③ProtocolDataHandler:解析获取HTTP请求的数据信息,并执行泛化调用。
- 细节
HttpStatement#isAuth:后续鉴权后,考虑到游客用户对基本欢迎页信息的get请求不需要认证校验,因此扩充auth字段用来判断当前HTTP请求是否需要进行鉴权。
消息对象:SessionResult作为执行泛化调用后的结果对象。GatewayResultMessage作为网关响应消息对象,用来反馈给用户的整个HTTP请求的结果。在HTTPResponse响应报文解析器中,会把SessionResult封装成GatewayResultMessage对象并填入data字段。
通道多处理器:①当前通道处理器处理完后,通过fireChannelRead将request请求传递给下一个处理器的read事件,当前通道放行,同时执行request#retain引用计数加一。
②通道AttributeMap:把HttpStatement对象存放在通道的AttributeMap中(ctx之间的Map是隔离的),相当于全局变量,整个通道的所有处理器都可以共享。channel.attr()通过指定key获取对应的Attribute对象。
# 8.排除内嵌Tomcat运行war包&排除数据源启动
- SpringBoot部署项目有两种方式:
①jar包
打包jar包,使用spring-boot-starter-web内置tomcat,直接通过java-jar运行。
②war包
如果需要配置定制化Tomcat服务器,那么需要使用外置的Tomcat服务器部署war包。把war包打包后放入到Tomcat文件夹下,然后启动tomcat进行发布。
因此引入Springboot依赖后需要排除掉内置的tomcat,否则会冲突。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
2
3
4
5
6
7
8
9
10
或者引入spring-boot-starter-tomcat指明scope:provided,依赖由容器或者jdk提供。
- 排除数据源
如果yml配置中没有配置数据源,那么导入的相关jar包里面如果有用到数据库,就会报错,因此springboot启动时需要排除数据源。
@SpringBootApplication(exclude= {DataSourceAutoConfiguration.class})