玩转Spring Cloud之API网关(zuul)

最近因为工作原因,一直没有空写文章,所以都是边忙项目,边利用空闲时间,周末时间学习总结,最终在下班回家后加班加点写完本篇文章,若有不足之处,还请谅解,谢谢!

本文内容导航:

一、网关的作用

二、网关与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网关调用

一、网关的作用

  网关就好比古代城门,所有的出入口都从指定的大门进出,大门有士兵把守,禁止非法进入城内,确保进出安全;在设计模式中有点类似门面模式;

  网关是把原来多个服务之间多对多的调用关系变为多对一的调用关系,通常用于向客户端或者合作伙伴应用提供统一的服务接入方式;

  网关提供统一的身份校验、动态路由、负载均衡、安全管理、统计、监控、流量管理、灰度发布、压力测试等功能

  更多作用和说明可参见:http://www.ityouknow.com/springcloud/2017/06/01/gateway-service-zuul.html

二、网关与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.yml(或application.yml)配置文件,这里使用bootstrap.yml,是因为示例代码后面要使用coud config client实现动态获取路由配置,这种模式下为了能够初始化配置数据,必需放在bootstrap.yml文件中。配置如下内容:

server:
  port: 1008

spring:
  application:
    name: zuulapigateway

  最后在spring boot启动类上(ZuulapigatewayApplication)添加@EnableZuulProxy注解即可,比较简单就不贴出代码了。这样就可以启动运行了,一个最简单最基本的zuul网关实例就跑起来了。由于现在没有配置任何route转发路由,故无法直接验证结果,下面就分几种情况进行演示说明。

  2.2.配置通过url进行路由,演示最简单模式

  在bootstrap.yml配置文件中添加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.yml配置文件中添加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.yml中配置路由转发规则,比如: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.yml配置添加重试相关的参数:

zuul:
   retryable: true

   ribbon:
    #  ribbon重试超时时间
    ConnectTimeout: 250
    #  建立连接后的超时时间
    ReadTimeout: 1000
    #  对所有操作请求都进行重试
    OkToRetryOnAllOperations: true
    #  切换实例的重试次数
    MaxAutoRetriesNextServer: 2
    #  对当前实例的重试次数
    MaxAutoRetries: 1

如上配置后,当我们通过zuul网关请求某个服务时,若请求服务失败时,则会触发重试请求,直到达到配置重试参数的上限后,才会触发熔断降级处理的结果。本示例中我把testservice-provider的getMessage方法额外增加sleep,确保请求超时,以模拟服务异常,同时在请求时打印请求信息,通过调试可以看到,当zuul网关路由转发请求该服务API时,由于响应超时,导致重试两次,最终返回熔断降级处理的结果。API重试两次记录如下图示:

 

zuul其它相关知识要点还未涉及到的,可以参考如下相关文章:

Zuul 实现限流:https://www.cnblogs.com/tiancai/p/9623063.html

Zuul 超时、重试、并发参数设置:https://blog.csdn.net/xx326664162/article/details/83625104

 


 

本文示例相关项目代码已上传到GITHUB,具体如下:

demo-zuulapigateway(zuul网关项目):  https://github.com/zuowj/learning-demos/tree/master/java/demo-zuulapigateway   

demo-microservice-parent(多模块(服务提供者、服务消费者)项目): https://github.com/zuowj/learning-demos/tree/master/java/demo-microservice-parent

demo-eurekaserver(eurea config server):  https://github.com/zuowj/learning-demos/tree/master/java/demo-eurekaserver

demo-configserver(cloud config server): https://github.com/zuowj/learning-demos/tree/master/java/demo-configserver

posted @ 2019-04-18 10:53  梦在旅途  阅读(2487)  评论(1编辑  收藏  举报