🌈Yu-Gateway:基于 Netty 与原生 Java 实现,使用 Nacos 作为注册与配置中心。项目实现多种过滤器,包含路由、负载均衡、鉴权、灰度发布等过滤器。
🌈项目代码地址:GitHub - YYYUUU42/YuGateway-master
如果该项目对你有帮助,可以在 github 上点个 ⭐ 喔 🥰🥰
🌈自研网关系列:可以点开专栏,参看完整的文档
目录
1、项目架构
在前面已经设定好了大致的网关架构所需要用到的一些服务,比如Common、Register Center、Config Center、Client、Core等模块,那么我们就按照这样子框架先搭建出来一个项目骨架
- Client:网关服务客户端,提供了一些注解用于注册对应的服务到网关中。
- Common:网关通用模块包,存放通用配置以及一些工具类
- ConfigCenter:网关配置中心,当前包提供基于Nacos服务的配置中心的功能,负责从配置中心拉取对应的配置
- RegisterCenter:网关注册中心,用于将网关服务注册到基于Nacos的注册中心。
- Core:网关核心模块,网关的几乎所有功能以及网关的启动都依赖于这个模块,包含了过滤器、启动容器、Netty网络处理框架等核心代码。
- HttpServer:测试网关HTTP请求服务模块。
- UserServer:测试网关JWT安全鉴权服务模块。
- Dubbo:提供网关基于Dubbo的RPC远程服务调用实现
2、核心上下文模型封装
对于核心上下文的封装,大概涉及到以下几个步骤:
- Context 上下文核心定义
- BaseContext 基础上下文实现
- 上下文参数
- GatewayRequest 实现
- GatewayResponse 实现
- Rule、FilterConfig 实现
- 最终GatewayContext实现
简单的介绍一下为什么需要这些类。
首先从 IContext 开始,这个是网关上下文的顶级接口类,定义了一些方法,这个类的作用主要是为了限定当前网关请求的一些基本的操作。比如当前网关请求是否正常执行完毕,或者是否有异常,请求是否长连接,请求是否有回调函数等等。
之后就是对 Context 内部的 Request 和 Response进行实现了,也就是网关请求和网关响应信息的实现。网关请求信息包含网关的全球开始和结束时间,请求id,请求路径,请求主机,路径参数,请求体参数,Cook,以及经过网关处理之后将要转发到的后端服务的信息。而网关响应信息包括,响应内容,响应状态码,异步响应对象。
还需要设定一个规则,进行过滤,并且规则有序。
IContext
import com.yu.gateway.common.config.Rule;
import io.netty.channel.ChannelHandlerContext;
import java.util.Map;
import java.util.function.Consumer;
/**
* @author yu
* @description 核心上下文接口定义
*/
public interface IContext {
/**
* 设置上下文状态
* @param status
*/
void setContextStatus(ContextStatus status);
/**
* 判断上下文状态
* @param status
* @return
*/
boolean judgeContextStatus(ContextStatus status);
/**
* 获取请求协议名
* @return
*/
String getProtocol();
/**
* 获取请求转换规则
* @return
*/
Rule getRule();
/**
* 获取请求对象
* @return
*/
Object getRequest();
/**
* 获取响应对象
* @return
*/
Object getResponse();
/**
* 获取异常信息
* @return
*/
Throwable getThrowable();
/**
* 获取上下文参数
* @param key
* @return
*/
Object getAttribute(Map<String, Object> key);
/**
* 获取 Netty 上下文
* @return
*/
ChannelHandlerContext getNettyContext();
/**
* 设置响应
*/
void setResponse();
/**
* 设置异常信息
* @param throwable
*/
void setThrowable(Throwable throwable);
/**
* 设置上下文参数
* @param key
* @param value
*/
void setAttribute(String key, Object value);
/**
* 是否保持长连接
* @return
*/
boolean isKeepAlive();
/**
* 资源释放
*/
void releaseRequest();
/**
* 设置回调函数
* @param consumer
*/
void setCompletedCallBack(Consumer<IContext> consumer);
/**
* 执行回调函数
*/
void invokeCompletedCallBacks();
}
BaseContext:
import com.yu.gateway.common.config.Rule;
import io.netty.channel.ChannelHandlerContext;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
/**
* @author yu
* @description IContext接口的基本实现
*/
public class BaseContext implements IContext{
/**
* 后台协议
*/
protected final String protocol;
/**
* 状态修饰
*/
protected volatile ContextStatus status = ContextStatus.Running;
/**
* 请求异常
*/
protected Throwable throwable;
/**
* 长连接
*/
protected final boolean keepAlive;
/**
* 上下文相关参数
*/
protected final Map<String, Object> attributes = new HashMap<String,Object>();
/**
* 回调函数集合
*/
protected List<Consumer<IContext>> completedCallBacks;
/**
* 资源释放
*/
protected final AtomicBoolean requestReleased = new AtomicBoolean(false);
/**
* netty上下文
*/
protected final ChannelHandlerContext nettyCtx;
public BaseContext(String protocol, boolean keepAlive, ChannelHandlerContext nettyCtx) {
this.protocol = protocol;
this.keepAlive = keepAlive;
this.nettyCtx = nettyCtx;
}
@Override
public void setContextStatus(ContextStatus status) {
this.status = status;
}
@Override
public boolean judgeContextStatus(ContextStatus status) {
return this.status == status;
}
@Override
public String getProtocol() {
return protocol;
}
/**
* 获取请求转换规则
*
* @return
*/
@Override
public Rule getRule() {
return null;
}
@Override
public Object getRequest() {
return null;
}
@Override
public Object getResponse() {
return null;
}
@Override
public Throwable getThrowable() {
return throwable;
}
@Override
public Object getAttribute(Map<String, Object> key) {
return attributes.get(key);
}
@Override
public ChannelHandlerContext getNettyContext() {
return nettyCtx;
}
@Override
public void setResponse() {
}
@Override
public void setThrowable(Throwable throwable) {
}
@Override
public void setAttribute(String key, Object value) {
}
@Override
public boolean isKeepAlive() {
return this.keepAlive;
}
@Override
public void releaseRequest() {
}
@Override
public void setCompletedCallBack(Consumer<IContext> consumer) {
if (completedCallBacks == null) {
completedCallBacks = new ArrayList<>();
}
completedCallBacks.add(consumer);
}
@Override
public void invokeCompletedCallBacks() {
if (completedCallBacks != null) {
completedCallBacks.forEach(call->call.accept(this));
}
}
}
GatewayRequest (具体实现代码在 github 中)
import org.asynchttpclient.Request;
import org.asynchttpclient.cookie.Cookie;
/**
* @author yu
* @description 请求对象
*/
public interface IGatewayRequest {
/**
* 修改域名
*
* @param host
*/
void setModifyHost(String host);
/**
* 获取修改后的域名
*
* @return
*/
String getModifyHost();
/**
* 设置/获取路径
*
* @param path
*/
void setModifyPath(String path);
/**
* 获取修改后的路径
*
* @return
*/
String getModifyPath();
/**
* 添加请求头信息
*
* @param name
* @param value
*/
void addHeader(CharSequence name, String value);
/**
* 设置请求头信息
*
* @param name
* @param value
*/
void setHeader(CharSequence name, String value);
/**
* Get 请求参数
*
* @param name
* @param value
*/
void addQueryParam(String name, String value);
/**
* POST 请求参数
*
* @param name
* @param value
*/
void addFormParam(String name, String value);
/**
* 添加或者替换Cookie
*
* @param cookie
*/
void addOrReplaceCookie(Cookie cookie);
/**
* 设置请求超时时间
*
* @param requestTimeout
*/
void setRequestTimeout(int requestTimeout);
/**
* 获取最终的请求路径
*
* @return
*/
String getFinalUrl();
/**
* 构造最终的请求对象
*
* @return
*/
Request build();
}
GatewayResponse 实现:
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.yu.gateway.common.enums.ResponseCode;
import com.yu.gateway.common.utils.JSONUtil;
import io.netty.handler.codec.http.*;
import lombok.Data;
import org.asynchttpclient.Response;
/**
* @author yu
* @description 网关响应对象
*/
@Data
public class GatewayResponse {
/**
* 响应头
*/
private HttpHeaders responseHeaders = new DefaultHttpHeaders();
/**
* 额外的响应结果
*/
private final HttpHeaders extraResponseHeaders = new DefaultHttpHeaders();
/**
* 响应内容
*/
private String content;
/**
* 异步返回对象
*/
private Response futureResponse;
/**
* 响应返回码
*/
private HttpResponseStatus httpResponseStatus;
public GatewayResponse() {
}
/**
* 设置响应头信息
*
* @param key
* @param val
*/
public void putHeader(CharSequence key, CharSequence val) {
responseHeaders.add(key, val);
}
/**
* 构建异步响应对象
*
* @param futureResponse
* @return
*/
public static GatewayResponse buildGatewayResponse(Response futureResponse) {
GatewayResponse response = new GatewayResponse();
response.setFutureResponse(futureResponse);
response.setHttpResponseStatus(HttpResponseStatus.valueOf(futureResponse.getStatusCode()));
return response;
}
/**
* 处理返回json对象,失败时调用
*
* @param code
* @param args
* @return
*/
public static GatewayResponse buildGatewayResponse(ResponseCode code, Object... args) {
ObjectNode objectNode = JSONUtil.createObjectNode();
objectNode.put(JSONUtil.STATUS, code.getStatus().code());
objectNode.put(JSONUtil.CODE, code.getCode());
objectNode.put(JSONUtil.MESSAGE, code.getMessage());
GatewayResponse response = new GatewayResponse();
response.setHttpResponseStatus(code.getStatus());
response.putHeader(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.APPLICATION_JSON + ";charset=utf-8");
response.setContent(JSONUtil.toJSONString(objectNode));
return response;
}
/**
* 处理返回json对象,成功时调用
*
* @param data
* @return
*/
public static GatewayResponse buildGatewayResponse(Object data) {
ObjectNode objectNode = JSONUtil.createObjectNode();
objectNode.put(JSONUtil.STATUS, ResponseCode.SUCCESS.getStatus().code());
objectNode.put(JSONUtil.CODE, ResponseCode.SUCCESS.getCode());
objectNode.putPOJO(JSONUtil.DATA, data);
GatewayResponse response = new GatewayResponse();
response.setHttpResponseStatus(ResponseCode.SUCCESS.getStatus());
response.putHeader(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.APPLICATION_JSON + ";charset=utf-8");
response.setContent(JSONUtil.toJSONString(objectNode));
return response;
}
}
Rule:
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
/**
* @author yu
* @description 核心请求url的规则匹配类
*/
@Getter
@Setter
public class Rule implements Comparable<Rule>, Serializable {
/**
* 规则ID
*/
private String id;
/**
* 规则名称
*/
private String name;
/**
* 规则协议
*/
private String protocol;
/**
* 后端服务ID
*/
private String serviceId;
/**
* 请求前缀
*/
private String prefix;
/**
* 路径集合
*/
private List<String> paths;
/**
* 优先级
*/
Integer order;
/**
* 过滤器集合
*/
private Set<FilterConfig> filterConfigs = new HashSet<FilterConfig>();
/**
* 重试规则
*/
private RetryConfig retryConfig = new RetryConfig();
/**
* 限流规则
*/
private Set<FlowControlConfig> flowControlConfigs = new HashSet<>();
/**
* 熔断规则
*/
private Set<HystrixConfig> hystrixConfigs = new HashSet<>();
/**
* 规则过滤器
*/
public static class FilterConfig {
/**
* 过滤器唯一Id
*/
private String id;
/**
* 过滤器规则描述:
* 1.负载均衡过滤器:{"load_balance": "Random"}
* 2.限流过滤器: {}
* 3.认证鉴权过滤器:{"auth_path": "/backend-http-server/http-server/ping"}
*/
private String config;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getConfig() {
return config;
}
public void setConfig(String config) {
this.config = config;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
FilterConfig config = (FilterConfig) obj;
return id.equals(config.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
/**
* 重试次数
*/
@Data
public static class RetryConfig {
private int times;
}
/**
* 熔断规则
*/
@Data
public static class HystrixConfig {
// 熔断降级路径
private String path;
// 超时时间
private int timeoutInMilliseconds;
// 核心线程数量
private int coreThreadSize;
// 熔断降级响应
private String fallbackResponse;
@Override
public boolean equals(Object obj) {
if(this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
HystrixConfig config = (HystrixConfig) obj;
return (config.path.equals(this.path)) && (config.timeoutInMilliseconds == this.timeoutInMilliseconds) && (config.coreThreadSize == this.coreThreadSize);
}
@Override
public int hashCode() {
return Objects.hash(new String[]{path, String.valueOf(timeoutInMilliseconds), String.valueOf(coreThreadSize)});
}
}
/**
* 限流过滤器配置
*/
@Data
public static class FlowControlConfig {
// 限流类型
private String type;
// 限流对象值
private String value;
// 限流模式——单机/分布式
private String mode;
// 限流规则,json字符串存储
private String config;
}
public boolean addFilterConfig(FilterConfig config) {
return filterConfigs.add(config);
}
public FilterConfig getFilterConfigById(String id) {
for (FilterConfig config : filterConfigs) {
if (config.getId().equals(id)) {
return config;
}
}
return null;
}
public Rule() {
super();
}
public Rule(String id, String name, String protocol, int order, Set<FilterConfig> configFilters) {
super();
this.id = id;
this.name = name;
this.protocol = protocol;
this.filterConfigs = configFilters;
this.order = order;
}
@Override
public int compareTo(Rule o) {
int orderCompare = Integer.compare(o.order, getOrder());
if (orderCompare == 0) {
return getId().compareTo(o.getId());
}
return orderCompare;
}
@Override
public boolean equals(Object obj) {
if(this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
Rule rule = (Rule)obj;
return id.equals(rule.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
3、配置类
之后我们就需要开始编写对我们的网关进行配置加载一些类和方法了。
下面是配置类详情,简单的提供了一些配置类的信息。
import lombok.Data;
/**
* @author yu
* @description 核心配置类
*/
@Data
public class Config {
private int port = 8888;
/**
* 服务名
*/
private String applicationName = "api-gateway";
/**
* 注册中心地址
*/
private String registryAddress = "localhost:8848";
/**
* 多环境配置
*/
private String env = "dev";
// Netty相关配置
private int eventLoopGroupBossNum = 1;
private int eventLoopGroupWorkerNum = Runtime.getRuntime().availableProcessors();
private int maxContentLength = 64 * 1024 * 1024;
/**
* 异步模式(单异步/双异步)
*/
private boolean whenComplete = true;
// Http Async
private int httpConnectTimeout = 30 * 1000;
private int httpRequestTimeout = 30 * 1000;
private int httpMaxRetryTimes = 2;
private int httpMaxConnections = 10000;
private int httpMaxConnectionsPerHost = 8000;
/**
* 客户端空闲连接超时时间
*/
private int httpPooledConnectionIdleTimeout = 60 * 1000;
/**
* disruptor队列前缀
*/
private String bufferType = "parallel";
/**
* disruptor缓冲区大小
*/
private int bufferSize = 1024 * 16;
/**
* 工作线程数
*/
private int processThread = Runtime.getRuntime().availableProcessors();
/**
* 等待策略
*/
private String waitStrategy = "blocking";
}
而下面则提供了配置的加载方式,分别从配置文件,环境变量,JM参数,运行时参数进行配置信息的加载
import com.yu.gateway.common.utils.PropertiesUtils;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
import java.util.Properties;
/**
* @author yu
* @description 核心配置加载类
*/
@Slf4j
public class ConfigLoader {
private static final String CONFIG_FILE = "gateway.properties";
private static final String ENV_PREFIX = "GATEWAY_";
private static final String JVM_PREFIX = "gateway.";
private static ConfigLoader instance;
private Config config;
private ConfigLoader() {}
public static Config getConfig() {
return instance.config;
}
public static ConfigLoader getInstance() {
if (instance == null) {
synchronized (ConfigLoader.class) {
if (instance == null) {
instance = new ConfigLoader();
}
}
}
return instance;
}
/**
* 配置加载,指定优先级:运行时参数——>jvm参数——>环境变量——>配置文件——>配置对象默认值
* @param args
* @return
*/
public Config load(String[] args) {
// 配置对象默认值
config = new Config();
// 配置文件
loadConfigFile();
// 环境变量
loadEnv();
// jvm参数
loadJvmParams();
// 运行参数
loadRuntimeParams(args);
return config;
}
/**
* 配置文件加载方式
* @return
*/
public void loadConfigFile() {
InputStream in = ConfigLoader.class.getClassLoader().getResourceAsStream(CONFIG_FILE);
if (in != null) {
Properties prop = new Properties();
try {
prop.load(in);
PropertiesUtils.properties2Object(prop, config);
} catch (IOException e) {
log.warn("load config file {} error", CONFIG_FILE, e);
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
/**
* 环境变量加载方式
*/
public void loadEnv() {
Map<String, String> env = System.getenv();
Properties properties = new Properties();
properties.putAll(env);
PropertiesUtils.properties2Object(properties, config, ENV_PREFIX);
}
/**
* JVM加载方式
*/
public void loadJvmParams() {
Properties properties = System.getProperties();
PropertiesUtils.properties2Object(properties, config, JVM_PREFIX);
}
/**
* 运行时参数加载方式
*/
public void loadRuntimeParams(String[] args) {
if (args != null && args.length > 0) {
Properties properties = new Properties();
for (String arg : args) {
if (arg.startsWith("--") && arg.contains("=")) {
properties.put(arg.substring(2, arg.indexOf("=")), arg.substring(arg.indexOf("=")+1));
}
}
PropertiesUtils.properties2Object(properties, config);
}
}
}
生命周期这一块,就比较简单了,定义一个接口,提供初始化,启动,以及关闭等方法即可。
/**
* @author yu
* @description 组件生命周期接口
*/
public interface LifeCycle {
/**
* 初始化
*/
void init();
/**
* 启动
*/
void start();
/**
* 关闭
*/
void shutdown();
}