最近因为工作原因, 一直没有空写文章, 所以都是边忙项目, 边利用空闲时间, 周末时间学习总结, 最终在下班回家后加班加点写完本篇文章, 若有不足之处, 还请谅解, 谢谢!
本文内容导航:
一, 网关的作用
二, 网关与 ESB 的区别
三, zuul 网关组件应用示例说明
2.1. 创建 zuul API gateway server 空项目
2.2. 配置通过 url 进行路由, 演示最简单模式
2.3. 集成加入到 Eureka 注册中心, 实现集群高可用
2.4. 配置通过 serviceid 进行路由
2.5. 自定义继承自 ZuulFilter 的 AuthFilter 过滤器, 进行鉴权
2.6. 自定义实现 FallbackProvider 接口的 RemoteServiceFallbackProvider 熔断降级提供者类, 以便当下游 API 不可用时可以进行熔断降级处理
2.7. 进阶用法: 通过自定义实现 RefreshableRouteLocator 的 CustomRouteLocator 动态路由定位器类, 以实现可灵活动态管理路由(路由存储在 DB 中)
2.8. 进阶用法: 通过重新注册 ZuulProperties 并指明从 config server(配置中心)来获得路由配置信息
2.9. 服务之间通过 zuul 网关调用
一, 网关的作用
网关就好比古代城门, 所有的出入口都从指定的大门进出, 大门有士兵把守, 禁止非法进入城内, 确保进出安全; 在设计模式中有点类似门面模式;
网关是把原来多个服务之间多对多的调用关系变为多对一的调用关系, 通常用于向客户端或者合作伙伴应用提供统一的服务接入方式;
网关提供统一的身份校验, 动态路由, 负载均衡, 安全管理, 统计, 监控, 流量管理, 灰度发布, 压力测试等功能
更多作用和说明可参见:
二, 网关与 ESB 的区别
ESB(企业服务总线): 可以提供比传统中间件产品更为廉价的解决方案, 同时它还可以消除不同应用之间的技术差异, 让不同的应用服务器协调运作, 实现了不同服务之间的通信与整合. 从功能上看, ESB 提供了事件驱动和文档导向的处理模式, 以及分布式的运行管理机制, 它支持基于内容的路由和过滤, 具备了复杂数据的传输能力, 并可以提供一系列的标准接口.(摘要百度百科)
ESB 简单讲就是可以进行: 系统集成, 协议转换, 路由转发, 过滤, 消费服务等, 相关服务可能会依赖耦合 ESB, 而 API 网关相比 ESB 比较轻量简单, 可能大部份功能 API 网关也具备, 但 API 网关通常使用 REST 风格来实现, 故服务提供方, 消费方可能不知道有 API 网关的存在. 具体可参考: 在微服务架构中, 我们还需要 ESB 吗?
三, zuul 网关组件应用示例说明
2.1. 创建 zuul API gateway server 空项目
首先通过 IDEA spring initializer[也有显示为: Spring Assistant] (或直接通过 https://start.spring.io/)创建一个 spring boot 项目(demo 项目命名: zuulapigateway), 创建过程中选择: zuul 依赖, 生成项目后的 POM xml 文件如下:
- <?xml version="1.0" encoding="UTF-8"?>
- <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- <modelVersion>4.0.0</modelVersion>
- <parent>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-parent</artifactId>
- <version>2.1.3.RELEASE</version>
- <relativePath/> <!-- lookup parent from repository -->
- </parent>
- <groupId>cn.zuowenjun.cloud</groupId>
- <artifactId>zuulapigateway</artifactId>
- <version>0.0.1-SNAPSHOT</version>
- <name>zuulapigateway</name>
- <description>Demo project for Spring Boot</description>
- <properties>
- <java.version>1.8</java.version>
- <spring-cloud.version>Greenwich.RELEASE</spring-cloud.version>
- </properties>
- <dependencies>
- </dependency>
- <dependency>
- <groupId>org.springframework.cloud</groupId>
- <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
- </dependency>
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-test</artifactId>
- <scope>test</scope>
- </dependency>
- </dependencies>
- <dependencyManagement>
- <dependencies>
- <dependency>
- <groupId>org.springframework.cloud</groupId>
- <artifactId>spring-cloud-dependencies</artifactId>
- <version>${spring-cloud.version}</version>
- <type>pom</type>
- <scope>import</scope>
- </dependency>
- </dependencies>
- </dependencyManagement>
- <build>
- <plugins>
- <plugin>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-maven-plugin</artifactId>
- </plugin>
- </plugins>
- </build>
- <repositories>
- <repository>
- <id>spring-milestones</id>
- <name>Spring Milestones</name>
- <url>https://repo.spring.io/milestone</url>
- </repository>
- </repositories>
- </project>
然后添加 Bootstrap.YAML(或 application.YAML)配置文件, 这里使用 Bootstrap.YAML, 是因为示例代码后面要使用 coud config client 实现动态获取路由配置, 这种模式下为了能够初始化配置数据, 必需放在 Bootstrap.YAML 文件中. 配置如下内容:
- server:
- port: 1008
- spring:
- application:
- name: zuulapigateway
最后在 spring boot 启动类上 (ZuulapigatewayApplication) 添加 @EnableZuulProxy 注解即可, 比较简单就不贴出代码了. 这样就可以启动运行了, 一个最简单最基本的 zuul 网关实例就跑起来了. 由于现在没有配置任何 route 转发路由, 故无法直接验证结果, 下面就分几种情况进行演示说明.
2.2. 配置通过 url 进行路由, 演示最简单模式
在 Bootstrap.YAML 配置文件中添加 zuul routes 配置, 这里我们直接配置最简单的方式(path->url: 即当访问 zuul 指定路径则直接转发到对应的 URL 上), 配置如下:
- # 配置 zuul 网关静态路由信息
- zuul:
- routes:
- zwj: #直接 path 到 URL 路由(注意: URL 模式不会触发网关的 Fallback, 参考: https://blog.csdn.net/qq_41293765/article/details/80911414)
- path: /**
- url: http://www.zuowenjun.cn/
- 配置后, 启动运行网关项目, 在浏览器中访问: http://localhost:1008/, 会发现显示的内容是 http://www.zuowenjun.cn/ 的首页内容. 这说明网关路由转发功能已生效.
- 2.3. 集成加入到 Eureka 注册中心, 实现集群高可用
- 虽然 2.2 中我们实现了简单的 path->url 的路由转发, 但实际生产中, 我们不可能只有一个 zuul 网关实例, 因为网关是所有服务消费者的统一入口, 如果网关挂掉了, 那们就无法请求后端的服务提供者, 故必需是集群高可用的, 而实现集群高可用, 最简单的方式就是部署多个 zuul 网关实例, 并注册到注册中心, 这样当某一个 zuul 网关实例出问题, 还会有其它 zuul 网关实例进行服务, 不影响系统正常运行. 集成加入到 Eureka 注册中心很简单, 如果不清楚可以查看我之前的文章《玩转 Spring Cloud 之服务注册发现 (eureka) 及负载均衡消费(ribbon,feign)》, 这里还是简要说明一下:
- 首先在 POM xml 中添加 eureka-client 依赖, maven 依赖如下:
- <dependency>
- <groupId>org.springframework.cloud</groupId>
- <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
- </dependency>
- 然后在 Bootstrap.YAML 配置文件中添加 eureka client 相关配置, 如下:
- # 配置连接到注册中心, 目的: 1. 网关本身的集群高可用; 2. 可以获得所有已注册服务信息, 可以通过 path->serviceId 进行路由
- eureka:
- client:
- serviceUrl:
- defaultZone: http://localhost:8800/eureka/
- 最后在 spring boot 启动类上 (ZuulapigatewayApplication) 添加 @EnableDiscoveryClient 注解即可. 我们可以把这个 zuul 网关项目的端口号改成不同的依次启动多个, 这样我们就可以在 eureka server 上看到多个 zuul 网关实例注册信息, 集群也就搭建好了, 这里就不贴图了.[注意: 启动 zuul 网关项目前, 请务必请正常开启 eureka server 项目(eurekaserver, 这里直接使用之前文章中示例的 eureka server 项目)]
- 2.4. 配置通过 serviceid 进行路由
- 在 2.2 中通过直接配置 path->url 实现路由转发, 无需注册中心, 虽然简单但因为写死了 URL 也就失去的灵活性, 故在微服务场景中我们更多的是通过服务 ID 进行识别与路由, 这样会相比 URL 灵活很多, 至少不用管 service URL, 由 zuul 通过注册中心动态获取 serviceId 对应的 url, 这样后续如果 url 更改了都不用改动 zuul 网关, 是不是比较爽.
- 只要我们集成了注册中心后, 就配置了默认的路由规则:/{serviceid}/**, 这样我们若需访问某个微服务(前提是访问的微服务项目必需也加入到 eureka 注册中心), 直接按照这个 path 格式来即可, 比如访问一个服务: http://localhost:1086/demo/message/zuowenjun.
- 为了便于演示本文服务提供者, 服务消费者, 服务网关, 故我重新编写了一个基于 IDEA 多模块 (多项目) 的父项目(demo-microservice-parent), 项目结构如下图示:
- 项目简要说明:
- demo-microservice-parent 是父 POM 项目, 仅提供 POM 依赖管理与继承, packaging 类型为 POM, 目的是: 所有子项目只需按需添加 maven 依赖即可, 且无需指定 version, 统一由父 POM 管理与配置.
- testservice-API 是 controller 接口定义项目, 之所以单独定义, 是因为考虑到服务提供者需要实现 API 接口以提供服务, 而服务消费者也需要继承及实现该 API 接口从而可以最终实现 FeignClient 代理接口及熔断降级回调实现类, 避免重复定义接口.
- testservice-provider 是服务提供者, 实现 testservice-API 接口
- testservice-consumer 是服务消费者, 继承及实现 testservice-API 接口, 以便可以远程调用 testservice-provider 的 API
- 至于如何创建 IDEA 多模块项目, 网上大把教程, 比如: https://www.cnblogs.com/tibit/p/6185704.html, 故我不再复述了.
- 这里我们先通过 zuul 网关请求访问 testservice-provider, 默认路由 (/{serviceid}/**) 如: http://localhost:1008/testservice/demo/message/zuowenjun, 就出现 testservice-provider 的接口响应的内容, 与下面指定 path->serviceId 相同(因为最终都是请求到同一个服务接口, 只是 zuul 网关的入口地址不同而矣), 配置 path->serviceId 如下:
- zuul:
- routes:
- testservice: #通过 path 到指定服务 ID 路由(服务发现)
- path: /test/**
- serviceId: testservice
- 当再次通过 zuul 网关请求访问 testservice-provider, 路由(/test/**), 如: http://localhost:1008/test/demo/message/zuowenjun, 最终响应结果如下:
- 2.5. 自定义继承自 ZuulFilter 的 AuthFilter 过滤器, 进行鉴权
- 网关的作用之一就是可以实现统一的身份校验(简称: 鉴权), 这里采取过滤器来实现当请求网关时, 从请求头上获得 token 并进行验证, 验证通过才能正常路由转发, 否则报 401 错误; 代码实现很简单如下:
- package cn.zuowenjun.cloud;
- import com.netflix.zuul.ZuulFilter;
- import com.netflix.zuul.context.RequestContext;
- import com.netflix.zuul.exception.ZuulException;
- import org.apache.commons.lang.StringUtils;
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
- import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
- import org.springframework.stereotype.Component;
- import javax.servlet.http.HttpServletRequest;
- /**
- * 自定义 token 验证过滤器, 实现请求验证
- */
- @Component
- public class AuthFilter extends ZuulFilter {
- private static final Logger logger= LoggerFactory.getLogger(AuthFilter.class);
- @Override
- public String filterType() {
- return FilterConstants.PRE_TYPE;// 路由执行前
- }
- @Override
- public int filterOrder() {
- return 0;// 过滤器优先顺序, 数字越小越先执行
- }
- @Override
- public boolean shouldFilter() {
- if(RequestContext.getCurrentContext().getRequest().getRequestURL().toString().contains("/testgit/")){
- return false;
- }
- return true;// 是否需要过滤
- }
- @Override
- public Object run() throws ZuulException {
- RequestContext ctx = RequestContext.getCurrentContext();
- HttpServletRequest request = ctx.getRequest();
- Object token = request.getHeader("token");
- // 校验 token
- Boolean isValid=false;
- if (StringUtils.equals(String.valueOf(token),"zuowenjun.cn.zuul.token.888888")){ // 模拟 TOKEN 验证, 验证通过
- isValid=true;
- }
- if (!isValid) {
- logger.error("token 验证不通过, 禁止访问!");
- ctx.setSendZuulResponse(false);//false 表示不发送路由响应给消费端, 即不会去路由请求后端服务
- ctx.getResponse().setContentType("text/html;charset=UTF-8");
- ctx.setResponseBody("token 验证不通过, 禁止访问!");
- ctx.setResponseStatusCode(401);
- return null;
- }
- logger.info(String.format("token is %s", token));
- return null;
- }
- }
因为 AuthFilter 类上添加了 @Component 注解, 这样在 Spring boot 启动时, 会自动注册到 Spring IoC 容器中并被 zuul 框架所使用, 关于 zuul 过滤器的知识, 可参见: https://www.jianshu.com/p/ff863d532767
当通过 zuul 网关访问接口时, 如: http://localhost:1008/test/demo/numbers/1/15, 因为有 AuthFilter 过滤器, 而且请求时并没有传入正确的 token, 结果被拦截并报 401 错误, 如下图示:
当在请求头上加入正确的 token 后, 再次重试访问 zuul 网关接口, 就能正常的返回结果了, 如下图示:
2.6. 自定义实现 FallbackProvider 接口的 RemoteServiceFallbackProvider 熔断降级提供者类, 以便当下游 API 不可用时可以进行熔断降级处理
当 zuul 网关路由转发请求下游服务时, 如果下游服务不可用 (报错) 或不可达(请求或响应超时等), 那么就会出现服务无法被正常消费, 这在分布式系统中是常见的, 因为网络是不可靠的, 无法保证 100% 高可用, 那么当网关路由转发请求下游服务失败时, 应该采取必要的降级措施, 以尽可能的提供替代方案保证服务可用. 这里采用自定义实现 FallbackProvider 接口的 RemoteServiceFallbackProvider 熔断降级提供者类, 这个与微服务中使用的 Hystrix 熔断降级是同样的原理, zuul 网关内部也默认集成了 Hystrix,Ribbon, 实现代码如下:
- package cn.zuowenjun.cloud;
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
- import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider;
- import org.springframework.http.HttpHeaders;
- import org.springframework.http.HttpStatus;
- import org.springframework.http.MediaType;
- import org.springframework.http.client.ClientHttpResponse;
- import org.springframework.stereotype.Component;
- import java.io.ByteArrayInputStream;
- import java.io.IOException;
- import java.io.InputStream;
- /**
- * 远程服务熔断降级回调提供者类
- */
- @Component
- public class RemoteServiceFallbackProvider implements FallbackProvider {
- private Logger logger = LoggerFactory.getLogger(RemoteServiceFallbackProvider.class);
- @Override
- public String getRoute() {
- return "*";// 指定熔断降级回调适用的服务名称,* 表示所有都适用, 否则请指定适用的 serviceId
- }
- @Override
- public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
- logger.warn(String.format("route:%s,exceptionType:%s,stackTrace:%s", route, cause.getClass().getName(), cause.getStackTrace()));
- return new ClientHttpResponse() {
- @Override
- public HttpStatus getStatusCode() throws IOException {
- return HttpStatus.OK;
- }
- @Override
- public int getRawStatusCode() throws IOException {
- return HttpStatus.OK.value();
- }
- @Override
- public String getStatusText() throws IOException {
- return HttpStatus.OK.getReasonPhrase();
- }
- @Override
- public void close() {
- }
- @Override
- public InputStream getBody() throws IOException {
- return new ByteArrayInputStream(("服务不可用, 原因:" + cause.getMessage()).getBytes());
- }
- @Override
- public HttpHeaders getHeaders() {
- HttpHeaders headers = new HttpHeaders();
- headers.setContentType(MediaType.APPLICATION_JSON);
- return headers;
- }
- };
- }
- }
与 zuul 过滤器原理类似, 在 RemoteServiceFallbackProvider 上添加了 @Component 注解, 这样在 Spring boot 启动时, 会自动注册到 Spring IoC 容器中并被 zuul 框架所使用, 当出现路由转发请求下游服务失败时就会返回降级处理的内容, 如下图所示:
2.7. 进阶用法: 通过自定义实现 RefreshableRouteLocator 的 CustomRouteLocator 动态路由定位器类, 以实现可灵活动态管理路由(路由存储在 DB 中)
前面介绍了在 zuul 网关项目的配置文件 Bootstrap.YAML 中配置路由转发规则, 比如: path->url,path->serviceId, 显然 path->serviceId 会灵活一些, 而且只有这样才会用上负载均衡及熔断降级, 但如果随着微服务项目越来越多, 每次都得改 zuul 网关的配置文件而且还得重启项目, 这样简值是要命的, 故这里分享采取自定义实现 RefreshableRouteLocator 的 CustomRouteLocator 动态路由定位器类, 以实现可灵活动态管理路由(路由存储在 DB 中), 并配合 RefreshRouteService 类(发布刷新事件通知, 当 DB 中的配置改变后, 应该调用 RefreshRouteService.refreshRoute 方法即可完成自动刷新路由配置信息, 无需重启项目), 实现原理可参考: https://github.com/lexburner/zuul-gateway-demo
- package cn.zuowenjun.cloud;
- import org.apache.commons.lang.StringUtils;
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
- import org.springframework.beans.BeanUtils;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.boot.autoconfigure.Web.ServerProperties;
- import org.springframework.cache.annotation.Cacheable;
- import org.springframework.cloud.netflix.zuul.filters.RefreshableRouteLocator;
- import org.springframework.cloud.netflix.zuul.filters.SimpleRouteLocator;
- import org.springframework.cloud.netflix.zuul.filters.ZuulProperties;
- import org.springframework.jdbc.core.BeanPropertyRowMapper;
- import org.springframework.jdbc.core.JdbcTemplate;
- import org.springframework.stereotype.Component;
- import java.util.LinkedHashMap;
- import java.util.List;
- import java.util.Map;
- /**
- * 自定义动态路由定位器
- * Refer https://github.com/lexburner/zuul-gateway-demo
- */
- @Component
- public class CustomRouteLocator extends SimpleRouteLocator implements RefreshableRouteLocator {
- public final static Logger logger = LoggerFactory.getLogger(CustomRouteLocator.class);
- private JdbcTemplate jdbcTemplate;
- private ZuulProperties properties;
- @Autowired
- public CustomRouteLocator(ServerProperties server, ZuulProperties properties, JdbcTemplate jdbcTemplate) {
- super(server.getServlet().getContextPath(), properties);
- this.properties = properties;
- this.jdbcTemplate = jdbcTemplate;
- logger.info("servletPath:{}",server.getServlet().getContextPath());
- }
- @Override
- public void refresh() {
- super.doRefresh();
- }
- @Override
- protected Map<String, ZuulProperties.ZuulRoute> locateRoutes() {
- LinkedHashMap<String, ZuulProperties.ZuulRoute> routesMap = new LinkedHashMap<>();
- // 先后顺序很重要, 这里优先采用 DB 中配置的路由映射信息, 然后才使用本地文件路由配置
- routesMap.putAll(locateRoutesFromDB());
- routesMap.putAll(super.locateRoutes());
- LinkedHashMap<String, ZuulProperties.ZuulRoute> values = new LinkedHashMap<>();
- for (Map.Entry<String, ZuulProperties.ZuulRoute> entry : routesMap.entrySet()) {
- String path = entry.getKey();
- if (!path.startsWith("/")) {
- path = "/" + path;
- }
- if (StringUtils.isNotBlank(this.properties.getPrefix())) {
- path = this.properties.getPrefix() + path;
- if (!path.startsWith("/")) {
- path = "/" + path;
- }
- }
- values.put(path, entry.getValue());
- }
- return values;
- }
- @Cacheable(value = "locateRoutes",key = "RoutesFromDB",condition ="true")
- public Map<String, ZuulProperties.ZuulRoute> locateRoutesFromDB(){
- Map<String, ZuulProperties.ZuulRoute> routes = new LinkedHashMap<>();
- List<CustomZuulRoute> results = jdbcTemplate.query("select * from zuul_gateway_routes where enabled =1",new BeanPropertyRowMapper<>(CustomZuulRoute.class));
- for (CustomZuulRoute result : results) {
- if(StringUtils.isBlank(result.getPath())
- || (StringUtils.isBlank(result.serviceId) && StringUtils.isBlank(result.getUrl()))){
- continue;
- }
- ZuulProperties.ZuulRoute zuulRoute = new ZuulProperties.ZuulRoute();
- try {
- BeanUtils.copyProperties(result,zuulRoute);
- } catch (Exception e) {
- logger.error("load zuul route info from db has error",e);
- }
- routes.put(zuulRoute.getPath(),zuulRoute);
- }
- return routes;
- }
- public static class CustomZuulRoute {
- private String id;
- private String path;
- private String serviceId;
- private String url;
- private boolean stripPrefix = true;
- private Boolean retryable;
- public String getId() {
- return id;
- }
- public void setId(String id) {
- this.id = id;
- }
- public String getPath() {
- return path;
- }
- public void setPath(String path) {
- this.path = path;
- }
- public String getServiceId() {
- return serviceId;
- }
- public void setServiceId(String serviceId) {
- this.serviceId = serviceId;
- }
- public String getUrl() {
- return url;
- }
- public void setUrl(String url) {
- this.url = url;
- }
- public boolean isStripPrefix() {
- return stripPrefix;
- }
- public void setStripPrefix(boolean stripPrefix) {
- this.stripPrefix = stripPrefix;
- }
- public Boolean getRetryable() {
- return retryable;
- }
- public void setRetryable(Boolean retryable) {
- this.retryable = retryable;
- }
- }
- }
上述代码重点关注: locateRoutesFromDB 方法, 这个方法主要就是完成从 zuul_gateway_routes 表中查询配置信息, 并加入到 routesMap 中, 这样本地路由配置与 DB 路由配置结合在一起, 相互补. 表结构 (表字段与 ZuulProperties.ZuulRoute 属性名保持相同) 如下图示:
另外代码中有使用到 spring cache 注解, 以免每次都查询 DB, 需要在 POM xml 中添加 spring-boot-starter-cache maven 依赖(并在 spring boot 启动类添加 @EnableCaching), 同时既然用到了 DB 查询路由配置, 肯定也需要添加 jdbc+mssql 相关 maven 依赖项, 具体配置如下:
- <!-- 添加 CACHE 依赖, 以便可以实现注解 CACHE-->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-cache</artifactId>
- </dependency>
- <!-- 添加 cloud config client 依赖, 实现从 config server 取 zuul 配置 -->
- <dependency>
- <groupId>org.springframework.cloud</groupId>
- <artifactId>spring-cloud-starter-config</artifactId>
- </dependency>
- <dependency>
- <groupId>com.microsoft.sqlserver</groupId>
- <artifactId>mssql-jdbc</artifactId>
- </dependency>
RefreshRouteService 类代码如下:
- package cn.zuowenjun.cloud;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.cloud.netflix.zuul.RoutesRefreshedEvent;
- import org.springframework.cloud.netflix.zuul.filters.RouteLocator;
- import org.springframework.context.ApplicationEventPublisher;
- /**
- * 刷新路由服务(当 DB 路由有变更时, 应调用 refreshRoute 方法)
- */
- public class RefreshRouteService {
- @Autowired
- ApplicationEventPublisher publisher;
- @Autowired
- RouteLocator routeLocator;
- public void refreshRoute() {
- RoutesRefreshedEvent routesRefreshedEvent = new RoutesRefreshedEvent(routeLocator);
- publisher.publishEvent(routesRefreshedEvent);
- }
- }
我们通过 zuul 网关请求访问服务 testservice-provider, 采用 DB 中的路由配置(如:/testsrv/msg/*), 如: http://localhost:1008/testsrv/msg/zuowenjun, 响应结果如下图示:
采用 DB 中的另一个路由配置 (如:/testx/**) 访问另一个服务接口, 如: http://localhost:1008/testx/demo/numbers/1/10, 响应结果如下图示:
2.8. 进阶用法: 通过重新注册 ZuulProperties 并指明从 config server(配置中心)来获得路由配置信息
除了 2.7 中使用 DB 作为存储路由配置的介质, 我们其实还可以采用 config server(配置中心)来实现, 这里使用 spring cloud config(使用 Git 作为配置存储介质, 当然使用其它介质也可以, 如: SVN,server 端本地配置文件等形式, 具体可参见该系列上一篇文章), 我们先在 GitHub 指定目录创建创建一个配置文件(目录位置: https://github.com/zuowj/learning-demos/master/config/zuulapigateway-dev.yml), 路由配置内容如下:
- zuul:
- routes:
- test-fromgit:
- path: /testgit/**
- serviceId: testservice
- 然后启动该系列上一篇文章中所用到的 spring cloud config server 示例项目(demo-configserver), 保证 config server 正常启动;
- 最后在 zuul 网关项目 POM xml 添加 spring confg client 依赖项:(其实看上篇文章就可以, 这里算再次说明以便巩固吧)
- <!-- 添加 cloud config client 依赖, 实现从 config server 取 zuul 配置 -->
- <dependency>
- <groupId>org.springframework.cloud</groupId>
- <artifactId>spring-cloud-starter-config</artifactId>
- </dependency>
- 在 spring boot 启动类添加重新注册 ZuulProperties 的方法 zuulProperties(单独使用 config 文件也是可以的, 这里只是图简单), 注意由于 ZuulProperties 默认就被注册了, 故这里必需显式加上:@Primary, 以表示优先使用该方法注册 bean, 代码如下:
- package cn.zuowenjun.cloud;
- import org.springframework.boot.SpringApplication;
- import org.springframework.boot.autoconfigure.SpringBootApplication;
- import org.springframework.boot.context.properties.ConfigurationProperties;
- import org.springframework.cache.annotation.EnableCaching;
- import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
- import org.springframework.cloud.context.config.annotation.RefreshScope;
- import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
- import org.springframework.cloud.netflix.zuul.filters.ZuulProperties;
- import org.springframework.cloud.netflix.zuul.filters.discovery.PatternServiceRouteMapper;
- import org.springframework.context.annotation.Bean;
- import org.springframework.context.annotation.Primary;
- @EnableZuulProxy
- @EnableDiscoveryClient
- @EnableCaching
- @SpringBootApplication
- public class ZuulapigatewayApplication {
- public static void main(String[] args) {
- SpringApplication.run(ZuulapigatewayApplication.class, args);
- }
- /**
- * 实现从 config server 获取配置数据并映射到 ZuulProperties 中
- * @return
- */
- @Bean
- @Primary
- @RefreshScope
- @ConfigurationProperties("zuul")
- public ZuulProperties zuulProperties(){
- return new ZuulProperties();
- }
- /**
- * 实现自定义 serviceId 到 route 的映射(如正则映射转换成 route)
- * @return
- */
- @Bean
- public PatternServiceRouteMapper serviceRouteMapper() {
- // 实现当 serviceId 符合: 服务名 - v 版本号, 则转换成: 版本号 / 服务名, 如: testservice-v1-->1/testservice
- return new PatternServiceRouteMapper("(?<name>^.+)-(?<version>v.+$)", "${version}/${name}");
- }
- }
如上代码 zuulProperties 方法上还添加了 @RefreshScope 注解, 表示可以实现配置变更后自动刷新, 当然这里只是仅仅添加了这个注解而矣, 并没有实现自动刷新, 实现自动刷新配置相对较复杂, 大家可以查看我上一篇讲 spring cloud config 文章, 里面有介绍方法及参考文章, 当然如果想要更好的配置中心中间件, 个人认为携程的 Apollo config 还是不错的, 大家可以自行上网查询相关资料. 另外还有个 serviceRouteMapper 方法, 这个是可以实现自定义 serviceId 到 route 的映射(如正则映射转换成 route)
我们通过 zuul 网关请求访问服务 testservice-provider, 采用 config server 中的路由配置(如:/testgit/**), 如: http://localhost:1008/testgit/demo/numerbs/10/20, 响应结果如下图示:
特别说明: 三种路由配置均可同时并存, 相互补充.
2.9. 服务之间通过 zuul 网关调用
上面都是演示直接通过 zuul 网关对应的路由请求服务接口, 而实际情况下, 可能是两个微服务项目之间调用, 虽说也可以直接使用 httpClient 来直接请求 zuul 网关消费服务, 但在 spring cloud 中一般都是使用 FeignClient 作为远程服务代理接口来实现的, 以前是 FeignClient 注解上指定 servierName 即可, 那么如果要连接 zuul 网关该如何处理呢? 目前有二种方法:
第一种: FeignClient 的 name 仍然指向要消费者的服务名, 然后 url 指定 zuul 网关路由 url, 类似如下:(优点是: 原来的接口定义都不用变, 只需增加 url, 缺点是: 写死了网关的 url, 生产中不建议使用)
@FeignClient(name = "testservice",url="http://localhost:1008/testservice",fallback =DemoRemoteService.DemoRemoteServiceFallback.class )
第二种: FeignClient 的 name 指向网关的名字(即把网关当成统一的服务入口), 无需再指定 url, 然后接口中的 RequestMapping 的 value 应加上远程调用服务的名字, 再正常加后面的 url(优点是: 直接依赖 zuul 网关, 没有写死 Url, 缺点是: 破环了 API 接口 url 的请求地址, 不利于框架整合, 就目前 demo-microservice-parent 项目中 API 为独立接口项目, 这种情况则不太适合, 只适合单独定义远程服务调用接口 )
- @FeignClient(name = "zuulapigateway",fallback =DemoRemoteService.DemoRemoteServiceFallback.class )
- public interface DemoRemoteService extends DemoService {
- @RequestMapping(value = "/testservice/demo/message/{name}")
- String getMessage(@PathVariable("name") String name);
- }
可能还有其它方式, 但由于时间精力有限, 可能暂时无法研究那么深, 如果大家有发现其它方式可以下方评论交流, 谢谢! 这样改造后, 其它代码都不用变就实现了服务之间通过 zuul 网关路由转发请求服务 API.
最后补充说明关于 zuul 重试实现方法:
1. 在 zuul 网关项目添加 spring-retry 依赖项, 如下:
- <!-- 添加重试依赖, 使 zuul 支持重试 -->
- <dependency>
- <groupId>org.springframework.retry</groupId>
- <artifactId>spring-retry</artifactId>
- </dependency>
2. 在 zuul 网关项目 Bootstrap.YAML 配置添加重试相关的参数:
- zuul:
- retryable: true
- ribbon:
- # ribbon 重试超时时间
- ConnectTimeout: 250
- # 建立连接后的超时时间
- ReadTimeout: 1000
- # 对所有操作请求都进行重试
- OkToRetryOnAllOperations: true
- # 切换实例的重试次数
- MaxAutoRetriesNextServer: 2
- # 对当前实例的重试次数
- MaxAutoRetries: 1
如上配置后, 当我们通过 zuul 网关请求某个服务时, 若请求服务失败时, 则会触发重试请求, 直到达到配置重试参数的上限后, 才会触发熔断降级处理的结果. 本示例中我把 testservice-provider 的 getMessage 方法额外增加 sleep, 确保请求超时, 以模拟服务异常, 同时在请求时打印请求信息, 通过调试可以看到, 当 zuul 网关路由转发请求该服务 API 时, 由于响应超时, 导致重试两次, 最终返回熔断降级处理的结果. API 重试两次记录如下图示:
- demo-eurekaserver(eurea config server): https://github.com/zuowj/learning-demos/tree/master/java/demo-eurekaserver
- demo-configserver(cloud config server):
来源: https://www.cnblogs.com/zuowj/p/10645189.html