如何从零实现属于自己的 API 网关?
序言
上一篇文章:你连对外接口签名都不会知道?有时间还是要学习学习。
有很多小伙伴反应,对外的 API 中相关的加签,验签这些工作可以统一使用网关去处理。
说到网关,大家肯定比较熟悉。市面上使用比较广泛的有:spring cloud/kong/soul。
API 网关的作用
(1)对外接口中的权限校验
(2)口调用的次数限制,频率限制
(3)微服务网关中的负载均衡,缓存,路由,访问控制,服务代理,监控,日志等。
实现原理
一般的请求时直接通过 client 访问 server 端,我们需要在中间实现一层 api 网关,外部 client 访问 gateway,然后 gateway 进行调用的转发。
核心流程
网关听起来非常复杂,最核心的部分其实基于 Servlet 的 javax.servlet.Filter
进行实现。
我们让 client 调用网关,然后在 Filter 中统一对消息题进行解析转发,调用服务端后,再封装返回给 client。
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
/**
* @author binbin.hou
* @since 1.0.0
*/
@WebFilter
@Component
public class GatewayFilter implements Filter {
private static final Logger LOGGER = LoggerFactory.getLogger(GatewayFilter.class);
public void init(FilterConfig filterConfig) throws ServletException {
}
public void doFilter(ServletRequest servletRequest,
ServletResponse servletResponse,
FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) servletRequest;
LOGGER.info("url={}, params={}", req.getRequestURI(), JSON.toJSONString(req.getParameterMap()));
//根据 URL 获取对应的服务名称
// 进行具体的处理逻辑
// TODO...
} else {
filterChain.doFilter(req, servletResponse);
}
}
public void destroy() {
}
}
接下来,我们只需要重点看一下如何重写 doFilter 方法即可。
具体实现
获取 appName
网关是面对公司内部所有应用的,我们可以通过每一个服务的唯一 appName 作为区分。
比如应用名称为 test,则调用网关的请求:
https://gateway.com/test/version
这个请求,对应的 appName 就是 test,实际请求的 url 是 /version。
具体实现也比较简单:
@Override
public Pair<String, String> getRequestPair(HttpServletRequest req) {
final String url = req.getRequestURI();
if(url.startsWith("/") && url.length() > 1) {
String subUrl = url.substring(1);
int nextSlash = subUrl.indexOf("/");
if(nextSlash < 0) {
LOGGER.warn("请求地址 {} 对应的 appName 不存在。", url);
return Pair.of(null, null);
}
String appName = subUrl.substring(0, nextSlash);
String realUrl = subUrl.substring(nextSlash);
LOGGER.info("请求地址 {} 对应的 appName: {}, 真实请求地址:{}", url, appName, realUrl);
return Pair.of(appName, realUrl);
}
LOGGER.warn("请求地址: {} 不是以 / 开头,或者长度不够 2,直接忽略。", url);
return Pair.of(null, null);
}
请求头信息
根据 HttpServletRequest 构建出对应的请求头信息:
/**
* 构建 map 信息
* @param req 请求
* @return 结果
* @since 1.0.0
*/
private Map<String, String> buildHeaderMap(final HttpServletRequest req) {
Map<String, String> map = new HashMap<>();
Enumeration<String> enumeration = req.getHeaderNames();
while (enumeration.hasMoreElements()) {
String name = enumeration.nextElement();
String value = req.getHeader(name);
map.put(name, value);
}
return map;
}
服务发现
当我们解析出请求的应用时 appName = test 时,就可以去查询配置中心中 test 应用中对应的 ip:port 信息。
@Override
public String buildRequestUrl(Pair<String, String> pair) {
String appName = pair.getValueOne();
String appUrl = pair.getValueTwo();
String ipPort = "127.0.0.1:8081";
//TODO: 根据数据库配置查询
// 根据是否启用 HTTPS 访问不同的地址
if (appName.equals("test")) {
// 这里需要涉及到负载均衡
ipPort = "127.0.0.1:8081";
} else {
throw new GatewayServerException(GatewayServerRespCode.APP_NAME_NOT_FOUND_IP);
}
String format = "http://%s/%s";
return String.format(format, ipPort, appUrl);
}
这里暂时固定写死,最后返回实际服务端的请求地址。
这里也可以结合具体的负载均衡/路由策略,做进一步的服务端选择。
不同 Method
HTTP 支持的方式是多样的,我们暂时支持一下 GET/POST 请求。
本质上就是针对 GET/POST 请求,构建形式的请求调用服务端。
这里的实现方式可以非常多样,此处以 ok-http 客户端为例作为实现。
接口定义
为了便于后期拓展,所有的 Method 调用,实现相同的接口:
public interface IMethodType {
/**
* 处理
* @param context 上下文
* @return 结果
*/
IMethodTypeResult handle(final IMethodTypeContext context);
}
GET
GET 请求。
@Service
@MethodTypeRoute("GET")
public class GetMethodType implements IMethodType {
@Override
public IMethodTypeResult handle(IMethodTypeContext context) {
String respBody = OkHttpUtil.get(context.url(), context.headerMap());
return MethodTypeResult.newInstance().respJson(respBody);
}
}
POST
POST 请求。
@Service
@MethodTypeRoute("POST")
public class PostMethodType implements IMethodType {
@Override
public IMethodTypeResult handle(IMethodTypeContext context) {
HttpServletRequest req = (HttpServletRequest) context.servletRequest();
String postJson = HttpUtil.getPostBody(req);
String respBody = OkHttpUtil.post(context.url(), postJson, context.headerMap());
return MethodTypeResult.newInstance().respJson(respBody);
}
}
OkHttpUtil 实现
OkHttpUtil 是基于 ok-http 封装的 http 调用工具类。
import com.github.houbb.gateway.server.util.exception.GatewayServerException;
import com.github.houbb.heaven.util.util.MapUtil;
import okhttp3.*;
import java.io.IOException;
import java.util.Map;
/**
* @author binbin.hou
* @since 1.0.0
*/
public class OkHttpUtil {
private static final MediaType JSON
= MediaType.parse("application/json; charset=utf-8");
/**
* get 请求
* @param url 地址
* @return 结果
* @since 1.0.0
*/
public static String get(final String url) {
return get(url, null);
}
/**
* get 请求
* @param url 地址
* @param headerMap 请求头
* @return 结果
* @since 1.0.0
*/
public static String get(final String url,
final Map<String, String> headerMap) {
try {
OkHttpClient client = new OkHttpClient();
Request.Builder builder = new Request.Builder();
builder.url(url);
if(MapUtil.isNotEmpty(headerMap)) {
for(Map.Entry<String, String> entry : headerMap.entrySet()) {
builder.header(entry.getKey(), entry.getValue());
}
}
Request request = builder
.build();
Response response = client.newCall(request).execute();
return response.body().string();
} catch (IOException e) {
throw new GatewayServerException(e);
}
}
/**
* get 请求
* @param url 地址
* @param body 请求体
* @param headerMap 请求头
* @return 结果
* @since 1.0.0
*/
public static String post(final String url,
final RequestBody body,
final Map<String, String> headerMap) {
try {
OkHttpClient client = new OkHttpClient();
Request.Builder builder = new Request.Builder();
builder.post(body);
builder.url(url);
if(MapUtil.isNotEmpty(headerMap)) {
for(Map.Entry<String, String> entry : headerMap.entrySet()) {
builder.header(entry.getKey(), entry.getValue());
}
}
Request request = builder.build();
Response response = client.newCall(request).execute();
return response.body().string();
} catch (IOException e) {
throw new GatewayServerException(e);
}
}
/**
* get 请求
* @param url 地址
* @param bodyJson 请求体 JSON
* @param headerMap 请求头
* @return 结果
* @since 1.0.0
*/
public static String post(final String url,
final String bodyJson,
final Map<String, String> headerMap) {
RequestBody body = RequestBody.create(JSON, bodyJson);
return post(url, body, headerMap);
}
}
调用结果处理
请求完服务端之后,我们需要对结果进行处理。
第一版的实现非常粗暴:
/**
* 处理最后的结果
* @param methodTypeResult 结果
* @param servletResponse 响应
* @since 1.0.0
*/
private void methodTypeResultHandle(final IMethodTypeResult methodTypeResult,
final ServletResponse servletResponse) {
try {
final String respBody = methodTypeResult.respJson();
// 重定向(因为网络安全等原因,这个方案应该被废弃。)
// 这里可以重新定向,也可以通过 http client 进行请求。
// GET/POST
//获取字符输出流对象
servletResponse.setCharacterEncoding("UTF-8");
servletResponse.setContentType("text/html;charset=utf-8");
servletResponse.getWriter().write(respBody);
} catch (IOException e) {
throw new GatewayServerException(e);
}
}
完整实现
我们把上面的主要流程放在一起,完整的实现如下:
import com.alibaba.fastjson.JSON;
import com.github.houbb.gateway.server.util.exception.GatewayServerException;
import com.github.houbb.gateway.server.web.biz.IRequestAppBiz;
import com.github.houbb.gateway.server.web.method.IMethodType;
import com.github.houbb.gateway.server.web.method.IMethodTypeContext;
import com.github.houbb.gateway.server.web.method.IMethodTypeResult;
import com.github.houbb.gateway.server.web.method.impl.MethodHandlerContainer;
import com.github.houbb.gateway.server.web.method.impl.MethodTypeContext;
import com.github.houbb.heaven.support.tuple.impl.Pair;
import com.github.houbb.heaven.util.lang.StringUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
/**
* 网关过滤器
*
* @author binbin.hou
* @since 1.0.0
*/
@WebFilter
@Component
public class GatewayFilter implements Filter {
private static final Logger LOGGER = LoggerFactory.getLogger(GatewayFilter.class);
@Autowired
private IRequestAppBiz requestAppBiz;
@Autowired
private MethodHandlerContainer methodHandlerContainer;
public void init(FilterConfig filterConfig) throws ServletException {
}
public void doFilter(ServletRequest servletRequest,
ServletResponse servletResponse,
FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) servletRequest;
LOGGER.info("url={}, params={}", req.getRequestURI(), JSON.toJSONString(req.getParameterMap()));
//根据 URL 获取对应的服务名称
Pair<String, String> pair = requestAppBiz.getRequestPair(req);
Map<String, String> headerMap = buildHeaderMap(req);
String appName = pair.getValueOne();
if(StringUtil.isNotEmptyTrim(appName)) {
String method = req.getMethod();
String respBody = null;
String url = requestAppBiz.buildRequestUrl(pair);
//TODO: 其他方法的支持
IMethodType methodType = methodHandlerContainer.getMethodType(method);
IMethodTypeContext typeContext = MethodTypeContext.newInstance()
.methodType(method)
.url(url)
.servletRequest(servletRequest)
.servletResponse(servletResponse)
.headerMap(headerMap);
// 执行前
// 执行
IMethodTypeResult methodTypeResult = methodType.handle(typeContext);
// 执行后
// 结果的处理
this.methodTypeResultHandle(methodTypeResult, servletResponse);
} else {
filterChain.doFilter(req, servletResponse);
}
}
public void destroy() {
}
/**
* 处理最后的结果
* @param methodTypeResult 结果
* @param servletResponse 响应
* @since 1.0.0
*/
private void methodTypeResultHandle(final IMethodTypeResult methodTypeResult,
final ServletResponse servletResponse) {
try {
final String respBody = methodTypeResult.respJson();
// 重定向(因为网络安全等原因,这个方案应该被废弃。)
// 这里可以重新定向,也可以通过 http client 进行请求。
// GET/POST
//获取字符输出流对象
servletResponse.setCharacterEncoding("UTF-8");
servletResponse.setContentType("text/html;charset=utf-8");
servletResponse.getWriter().write(respBody);
} catch (IOException e) {
throw new GatewayServerException(e);
}
}
/**
* 构建 map 信息
* @param req 请求
* @return 结果
* @since 1.0.0
*/
private Map<String, String> buildHeaderMap(final HttpServletRequest req) {
Map<String, String> map = new HashMap<>();
Enumeration<String> enumeration = req.getHeaderNames();
while (enumeration.hasMoreElements()) {
String name = enumeration.nextElement();
String value = req.getHeader(name);
map.put(name, value);
}
return map;
}
}
网关验证
网关应用
我们把拦截器加好以后,定义对应的 Application 如下:
@SpringBootApplication
@ServletComponentScan
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
然后把网关启动起来,启动端口号为 8080
服务端应用
然后启动服务端对应的服务,端口号为 8081。
查看版本号的控制器实现:
@RestController
public class MonitorController {
@RequestMapping(value = "version", method = RequestMethod.GET)
public String version() {
return "1.0-demo";
}
}
请求
我们在浏览器上直接访问 api 网关:
http://localhost:8080/test/version
页面返回:
1.0-demo
小结
API 网关实现的原理并不难,就是基于 servlet 对请求进行转发。
虽然看起来简单,但是可以在这个基础上实现更多强大的特性,比如限流,日志,监控等等。
如果你对 API 网关感兴趣的话,不妨关注一波,后续内容,更加精彩。
备注:涉及的代码较多,文中做了简化。如果你对全部源码感兴趣,可以關註【老马啸西风】,後臺回復【网关】即可获得。
我是老马,期待与你的下次重逢。
如何从零实现属于自己的 API 网关?的更多相关文章
- API 网关的选型和持续集成
2019 年 8 月 31 日,OpenResty 社区联合又拍云,举办 OpenResty × Open Talk 全国巡回沙龙·成都站,APISIX 作者温铭在活动上做了< API 网关的选 ...
- API 网关知识看这篇就足够了!
本文已经收录自 JavaGuide (60k+ Star[Java学习+面试指南] 一份涵盖大部分Java程序员所需要掌握的核心知识.) 本文授权转载自:https://github.com/java ...
- 【转帖】使用了 Service Mesh 后我还需要 API 网关吗?
使用了 Service Mesh 后我还需要 API 网关吗? https://www.kubernetes.org.cn/6762.html api gateway和istio 是不一样的 追求不一 ...
- 用API网关把API管起来
最开始只是想找个API网关防止API被恶意请求,找了一圈发现基于Nginx的OpenResty(Lua语言)扩展模块Orange挺好(也找了Kong,但是感觉复杂了点没用),还偷懒用Vagrant结合 ...
- Tyk API网关介绍及安装说明
Tyk API网关介绍及安装说明 Tyk是一个开源的轻量级API网关程序. 什么是API网关 API网关是一个各类不同API的前置服务器.API网关封装了系统内部架构,对外提供统一服务.此外还可以实现 ...
- API网关
API网关 最开始只是想找个API网关防止API被恶意请求,找了一圈发现基于Nginx的OpenResty(Lua语言)扩展模块Orange挺好(也找了Kong,但是感觉复杂了点没用),还偷懒用Vag ...
- 使用API网关构建微服务
使用传统的异步回调方法编写API组合代码会让你迅速坠入回调地狱.代码会变得混乱.难以理解且容易出错.一个更好的方法是使用响应式方法以一种声明式样式编写API网关代码.响应式抽象概念的例子有Scala中 ...
- baas & API 网关
最近一段时间一直在做API 网关的工作.清晰看到当前云下Baas将会是主要方向,而API网关会是一把利剑. 本人正在规划API网关,有兴趣的可以一起探讨:hotwheels_bo@163.com
- 理解WEB API网关
*:first-child { margin-top: 0 !important; } body>*:last-child { margin-bottom: 0 !important; } /* ...
- Net分布式系统之六:微服务之API网关
本人建立了个人技术.工作经验的分享微信号,计划后续公众号同步更新分享,比在此更多具体.欢迎有兴趣的同学一起加入相互学习.基于上篇微服务架构分享,今天分享其中一个重要的基础组件“API网关”. 一.引言 ...
随机推荐
- 问题--C中结构体想要嵌套一个该结构体指针,但是系统无法识别该类型
代码如下: typedef struct_Person{ char name[64]; int age; //Person* person; //这里会出现一个问题,由于Person是在末尾定义的,那 ...
- Oracle数据库同时建立和使用两个监听器
1.问题 我分别对两个数据库实例(Lib和Orcl)各自建立了一个监听器,端口号分别为1520和1521,但是默认只启动一个,导致我切换数据库实例的时候, 出现以下问题:状态: 失败 -测试失败: I ...
- Go-获取密码的sha值
// 对用户密码进行加密 func EncodePwd(pwd string) string { s := sha256.New() s.Write([]byte(pwd)) data := s.Su ...
- 百度网盘(百度云)SVIP超级会员共享账号每日更新(2023.12.18)
一.百度网盘SVIP超级会员共享账号 可能很多人不懂这个共享账号是什么意思,小编在这里给大家做一下解答. 我们多知道百度网盘很大的用处就是类似U盘,不同的人把文件上传到百度网盘,别人可以直接下载,避免 ...
- [转帖]--build=arm-linux
今天在arm上用configure生成makefile时报错:configure: error: cannot guess build type; you must specify one 问题: 不 ...
- [转帖]调试springboot数据库系统应用时常用debug日志配置, 解决问题缩小范围时常用
https://www.yihaomen.com/article/1853.html 摘要: 用 spring boot 开发应用时,在遇到麻烦问题时,经常会打开debug日志,下面记录一个通用的思路 ...
- 如何从0开始搭建 Vue 组件库
作者:京东零售 陈艳春 前言: 组件设计是通过对功能及视觉表达中元素的拆解.归纳.重组,并基于可被复用的目的,形成规范化的组件,通过多维度组合来构建整个设计方案,將这些组件整理在一起,便形成组件库.本 ...
- 【解决了一个小问题】es query返回数据中, int64类型精度丢失的问题
作者:张富春(ahfuzhang),转载时请注明作者和引用链接,谢谢! cnblogs博客 zhihu Github 公众号:一本正经的瞎扯 首先定义了一个简单的结构体来接收es query返回的数据 ...
- c++全局变量extern
extern extern 是 C++ 中的一个关键字,用于声明一个变量或函数是在其他文件中定义的.它的作用是告诉编译器在链接时在其他文件中寻找该变量或函数的定义. 在 C++ 中,如果一个变量或函数 ...
- VRAR概念的定义和要素以及技术定义和应用
1.概念 一.三个概念的定义和要素. 1.VR,Virtual Reality,虚拟现实 是一种通过计算机模拟真实感的图像,声音和其他感觉,从而复制出一个真实或者假想的场景,并且让人觉得身处这个场景之 ...