diff --git a/README.md b/README.md index d7ee3d9..3f45b6a 100644 --- a/README.md +++ b/README.md @@ -12,21 +12,13 @@ 希望本项目对你有所帮助,请给个**STAR吧,谢谢!!!** ## 功能 -#### 基础篇 * [服务注册](https://github.com/DerekYRC/mini-spring-cloud/blob/main/changelog.md#服务注册) * [服务发现](https://github.com/DerekYRC/mini-spring-cloud/blob/main/changelog.md#服务发现) * [负载均衡](https://github.com/DerekYRC/mini-spring-cloud/blob/main/changelog.md#集成ribbon实现客户端负载均衡) * [集成Feign简化调用方式](https://github.com/DerekYRC/mini-spring-cloud/blob/main/changelog.md#集成Feign简化调用方式) +* [API网关](https://github.com/DerekYRC/mini-spring-cloud/blob/main/changelog.md#API网关) * [流量控制]() * [熔断降级]() -* [API 网关]() - -#### 扩展篇 -> TODO 基础篇遗留的功能点 - -* [自定义负载均衡规则]() - - ## 使用方法 阅读 [changelog.md](https://github.com/DerekYRC/mini-spring-cloud/blob/main/changelog.md) diff --git a/assets/zuul-filter.png b/assets/zuul-filter.png new file mode 100644 index 0000000..fd363a6 Binary files /dev/null and b/assets/zuul-filter.png differ diff --git a/assets/zuul-framework.png b/assets/zuul-framework.png new file mode 100644 index 0000000..6456890 Binary files /dev/null and b/assets/zuul-framework.png differ diff --git a/assets/zuul-servlet.png b/assets/zuul-servlet.png new file mode 100644 index 0000000..34d290d Binary files /dev/null and b/assets/zuul-servlet.png differ diff --git a/changelog.md b/changelog.md index 26e1171..5cb0fca 100644 --- a/changelog.md +++ b/changelog.md @@ -1313,3 +1313,372 @@ public interface EchoService { 访问```http://localhost:8080/bar``` +# [API网关](#API网关) +> 代码分支: api-gateway-netflix-zuul + +## 关于Netflix Zuul + +Netflix Zuul是一个提供动态路由、监控、弹性容量、安全等功能的基于第七层网络协议的应用程序网关。 + +#### Zuul核心框架和执行流程 + +![](./assets/zuul-framework.png) + +ZuulServlet负责拦截http请求,然后将http请求交给由ZuulFilter组成的过滤器链处理,ZuulFilter加载模块负责加载ZuulFilter。 + +可见ZuulFilter过滤器是zuul框架中的核心,API网关的鉴权、限流、权限、熔断、协议转换、错误码统一、缓存、日志、监控、告警等等功能可以实现ZuulFilter过滤器来实现。 + +#### ZuulFilter过滤器类型及执行顺序 + +ZuulFilter过滤器分为四种类型: + +- pre类型:调用远程服务之前执行 +- route:路由、发起远程调用 +- post:向客户端输出响应报文 +- error:处理过滤器链执行过程中出现的错误 + +ZuulServlet.service方法: + +![](./assets/zuul-servlet.png) + +从ZuulServlet.service方法中能看出四种类型的过滤器的执行顺序如下图所示: + +![](./assets/zuul-filter.png) + +## 功能实现 + +EnableZuulProxy注解启用API网关功能 + +```java +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Import(ZuulServerAutoConfiguration.class) +public @interface EnableZuulProxy { +} +``` + +EnableZuulProxy注解引入配置类ZuulServerAutoConfiguration,该配置类配置了ZuulServlet、过滤器加载模块的FilterRegistry、实现的三个ZuulFilter以及PreDecorationFilter过滤器需要使用的路由定位器RouteLocator。 + +```java + +@Configuration +@EnableConfigurationProperties({ZuulProperties.class}) +public class ZuulServerAutoConfiguration { + + @Autowired + protected ZuulProperties zuulProperties; + + /** + * 注册ZuulServlet,用于拦截处理http请求 + */ + @Bean + public ServletRegistrationBean zuulServlet() { + return new ServletRegistrationBean<>(new ZuulServlet(), zuulProperties.getServletPath()); + } + + /** + * 路由定位器 + */ + @Bean + public RouteLocator simpleRouteLocator() { + return new SimpleRouteLocator(zuulProperties); + } + + /** + * pre类型过滤器,根据RouteLocator来进行路由规则的匹配 + */ + @Bean + public ZuulFilter preDecorationFilter(RouteLocator routeLocator) { + return new PreDecorationFilter(routeLocator); + } + + /** + * route类型过滤器,使用ribbon负载均衡器进行http请求 + */ + @Bean + ZuulFilter ribbonRoutingFilter(LoadBalancerClient loadBalancerClient) { + return new RibbonRoutingFilter(loadBalancerClient); + } + + /** + * post类型过滤器,向客户端输出响应报文 + */ + @Bean + ZuulFilter sendResponseFilter() { + return new SendResponseFilter(); + } + + /** + * 注册过滤器 + */ + @Bean + public FilterRegistry filterRegistry(Map filterMap) { + FilterRegistry filterRegistry = FilterRegistry.instance(); + filterMap.forEach((name, filter) -> { + filterRegistry.put(name, filter); + }); + return filterRegistry; + } +} +``` + +只针对正常流程实现了以下三个过滤器,想了解更多过滤器可以参考这篇文章: [**Spring Cloud 源码分析(四)Zuul:核心过滤器**](https://blog.didispace.com/spring-cloud-source-zuul/) + +- pre类型过滤器PreDecorationFilter,使用路由定位器RouteLocator根据请求路径匹配路由,将路由信息放进请求上下文RequestContext中 + +```java +/** + * pre类型过滤器,根据RouteLocator来进行路由规则的匹配 + */ +public class PreDecorationFilter extends ZuulFilter { + private static Logger logger = LoggerFactory.getLogger(PreDecorationFilter.class); + + private RouteLocator routeLocator; + + public PreDecorationFilter(RouteLocator routeLocator) { + this.routeLocator = routeLocator; + } + + @Override + public String filterType() { + return PRE_TYPE; + } + + @Override + public int filterOrder() { + return 5; + } + + @Override + public boolean shouldFilter() { + return true; + } + + @Override + public Object run() throws ZuulException { + RequestContext requestContext = RequestContext.getCurrentContext(); + String requestURI = requestContext.getRequest().getRequestURI(); + //获取匹配的路由 + Route route = routeLocator.getMatchingRoute(requestURI); + if (route != null) { + requestContext.put(REQUEST_URI_KEY, route.getPath()); + requestContext.set(SERVICE_ID_KEY, route.getLocation()); + } else { + logger.error("获取不到匹配的路由, requestURI: {}", requestContext); + } + + return null; + } +} +``` + +路由定位器: + +```java +/** + * 路由定位器 + */ +public interface RouteLocator { + + /** + * 获取匹配的路由 + * + * @param path + * @return + */ + Route getMatchingRoute(String path); +} +``` + +```java +/** + * 路由定位器实现类 + */ +public class SimpleRouteLocator implements RouteLocator { + + private ZuulProperties zuulProperties; + + private PathMatcher pathMatcher = new AntPathMatcher(); + + public SimpleRouteLocator(ZuulProperties zuulProperties) { + this.zuulProperties = zuulProperties; + } + + @Override + public Route getMatchingRoute(String path) { + for (Map.Entry entry : zuulProperties.getRoutes().entrySet()) { + ZuulProperties.ZuulRoute zuulRoute = entry.getValue(); + String pattern = zuulRoute.getPath(); + if (pathMatcher.match(pattern, path)) { + String targetPath = path.substring(pattern.indexOf("*") - 1); + return new Route(targetPath, zuulRoute.getServiceId()); + } + } + + return null; + } +} +``` + +- route类型过滤器RibbonRoutingFilter,根据PreDecorationFilter过滤器匹配的路由信息发起远程调用,将调用结果放进请求上下文RequestContext + +```java +/** + * route类型过滤器,使用ribbon负载均衡器进行http请求 + */ +public class RibbonRoutingFilter extends ZuulFilter { + private static Logger logger = LoggerFactory.getLogger(RibbonRoutingFilter.class); + + private LoadBalancerClient loadBalancerClient; + + public RibbonRoutingFilter(LoadBalancerClient loadBalancerClient) { + this.loadBalancerClient = loadBalancerClient; + } + + @Override + public String filterType() { + return ROUTE_TYPE; + } + + @Override + public int filterOrder() { + return 10; + } + + @Override + public boolean shouldFilter() { + RequestContext requestContext = RequestContext.getCurrentContext(); + return requestContext.get(SERVICE_ID_KEY) != null; + } + + @Override + public Object run() throws ZuulException { + try { + RequestContext requestContext = RequestContext.getCurrentContext(); + //使用ribbon的负载均衡能力发起远程调用 + //TODO 简单实现,熔断降级章节再完善 + String serviceId = (String) requestContext.get(SERVICE_ID_KEY); + ServiceInstance serviceInstance = loadBalancerClient.choose(serviceId); + if (serviceInstance == null) { + logger.error("根据serviceId查询不到服务示例,serviceId: {}", serviceId); + return null; + } + + String requestURI = (String) requestContext.get(REQUEST_URI_KEY); + String url = serviceInstance.getUri().toString() + requestURI; + HttpRequest httpRequest = HttpUtil.createRequest(Method.POST, url); + HttpResponse httpResponse = httpRequest.execute(); + + //将响应报文的状态码和内容写进请求上下文中 + requestContext.setResponseStatusCode(httpResponse.getStatus()); + requestContext.setResponseDataStream(httpResponse.bodyStream()); + + return httpResponse; + } catch (Exception e) { + rethrowRuntimeException(e); + } + return null; + } +} +``` + +- post类型过滤器SendResponseFilter,将RibbonRoutingFilter过滤器发起远程调用的结果作为响应报文输出给客户端 + +```java +/** + * post类型过滤器,向客户端输出响应报文 + */ +public class SendResponseFilter extends ZuulFilter { + private static Logger logger = LoggerFactory.getLogger(SendResponseFilter.class); + + @Override + public String filterType() { + return POST_TYPE; + } + + @Override + public int filterOrder() { + return 1000; + } + + @Override + public boolean shouldFilter() { + return RequestContext.getCurrentContext() + .getResponseDataStream() != null; + } + + @Override + public Object run() throws ZuulException { + //向客户端输出响应报文 + RequestContext requestContext = RequestContext.getCurrentContext(); + InputStream inputStream = requestContext.getResponseDataStream(); + try { + HttpServletResponse servletResponse = requestContext.getResponse(); + servletResponse.setCharacterEncoding("UTF-8"); + + OutputStream outStream = servletResponse.getOutputStream(); + StreamUtils.copy(inputStream, outStream); + } catch (Exception e) { + rethrowRuntimeException(e); + } finally { + //关闭输入输出流 + if (inputStream != null) { + try { + inputStream.close(); + } catch (Exception e) { + logger.error("关闭输入流失败", e); + } + } + + //Servlet容器会自动关闭输出流 + } + return null; + } +} +``` + +## 测试: + +启动API网关ApiGatewayApplication + +API网关代码: + +```java + +@EnableZuulProxy +@SpringBootApplication +public class ApiGatewayApplication { + + public static void main(String[] args) { + SpringApplication.run(ApiGatewayApplication.class, args); + } +} +``` + +配置application.yml: +```yaml +spring: + application: + name: api-gateway-application + cloud: + tutu: + discovery: + server-addr: localhost:6688 + service: ${spring.application.name} + +server: + port: 8888 + +zuul: + servlet-path: /* + routes: + route_provider_application: + path: /provider-application/** + service-id: provider-application +``` + +访问```http://localhost:8888/provider-application/echo``` + +# 流量控制和熔断降级 + +TODO 待研究Sentinel完再写本章节,估计得隔一段时间~~~ \ No newline at end of file