SpringCloud 06 —— Zuul 网关

Zuul 介绍

Zuul 也是Netflix公司做出的系统,主要是Netflix API流量的数量和多样性有时会导致生产问题迅速出现而没有警告。因此Netflix团队做了这么一个统一的入口——网关,可以将所有的API组织起来。
Zuul是所有从设备和web站点到Netflix流媒体应用程序后端请求的前门。作为一个边缘服务应用程序,Zuul被构建来支持动态路由、监视、弹性和安全性。它还可以根据需要将请求路由到多个Amazon自动伸缩组。

  • Zuul 是一个基于JVM路由和服务端的负载均衡器,Spring Cloud 对Zuul 进行了一系列的封装和时间,并且可以和Eureka、Ribbon、Hystrix、Feign等组件配合使用,对于Spring Cloud 微服务来说不必暴露过多的接口,提升了整个分布式集群的安全性。

Zuul使用了一系列不同类型的过滤器,使我们能够快速灵活地将功能应用到服务中。这些过滤器帮助我们执行以下功能:

  • 身份验证和安全性: 识别每个资源的身份验证需求,并拒绝不满足它们的请求
  • 监控: 在边缘跟踪有意义的数据和统计数据,以便给我们一个准确的生产视图
  • 动态路由: 动态路由请求到不同的后端集群
  • 压力测试: 逐渐增加集群的流量,以评估性能
  • 限流: 为每种请求类型分配容量,并丢弃超过限制的请求
  • 静态响应处理: 直接在边缘构建一些响应,而不是将它们转发到内部集群

从较高的角度来看,Zuul 2.0是一台Netty服务器,它运行前置过滤器(入站过滤器),然后使用Netty客户端代理请求,然后在运行后置过滤器(出站过滤器)后返回响应。How It Works 2.0

过滤器是Zuul业务逻辑的核心所在。它们具有执行大量动作的能力,并且可以在请求-响应生命周期的不同部分运行,如上图所示。

  • 入站筛选器在路由到源之前执行,并且可以用于身份验证,路由和修饰请求。
  • 端点过滤器可用于返回静态响应,否则内置ProxyEndpoint过滤器会将请求路由到源。
  • 出站筛选器在从源获取响应后执行,可用于度量标准,修饰用户的响应或添加自定义标头。
  • 过滤器也有两种类型:同步和异步。由于我们在事件循环上运行,因此永远不要阻塞过滤器至关重要。如果要阻塞,请在单独的线程池上的异步过滤器中进行阻塞,否则可以使用同步过滤器。

和Zuul 1.*对比
前端用Netty Server代替Servlet,目的是支持前端异步。后端用Netty Client代替Http Client,目的是支持后端异步。
过滤器换了一下名字,用Inbound Filters代替Pre-routing Filters,用Endpoint Filter代替Routing Filter,用Outbound Filters代替Post-routing Filters。
性能提升约20%

这里的Spring Cloud Hoxton.RELEASE支持的Zuul 版本仍然是1.* 系列,因为目前大部分应用用的1.3.1,因为对于Zuul 的使用和介绍仍然以1.3.1 版本为主。(2.0在SpringCloud的后续支持下不会有太大使用上的差异)

Spring Cloud 还提供了一个网关Spring Cloud GateWay,基于NIO的非阻塞协议,我们先实现一个微服务的基本实现,将后面进行各个组件替换。

SpringCloud中的zuul:

<dependency>
  <groupId>com.netflix.zuul</groupId>
  <artifactId>zuul-core</artifactId>
  <version>1.3.1</version>
  <scope>compile</scope>
  <exclusions>
    <exclusion>
      <artifactId>groovy-all</artifactId>
      <groupId>org.codehaus.groovy</groupId>
    </exclusion>
    <exclusion>
      <artifactId>mockito-all</artifactId>
      <groupId>org.mockito</groupId>
    </exclusion>
  </exclusions>
</dependency>

Zuul 简单应用

新建项目模块cloud-gateways-zuul-simple,引入依赖:

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>

创建启动类
加上@EnableZuulProxy注解:

@EnableZuulProxy
@SpringBootApplication
public class ZuulSimpleApplication {
    public static void main(String[] args) {
        SpringApplication.run(ZuulSimpleApplication.class, args);
    }
}

添加应用配置

我们给zuul添加转发配置,当输入ip:port/simple 会转发到http://127.0.0.1:8762

server:
  port: 8678
spring:
  application:
    name: gateways-zuul-simple
zuul:
  routes:
    simple:
      url: http://127.0.0.1:8762 #转发的地址

启动项目
这里需要cloud-client-eureka服务的支持(8762端口),然后启动当前项目
在Postman 中输入127.0.0.1:8678/simple/hi实际上调用的是127.0.0.1:8762/hi

运行原理浅析

因为我们开启了@EnableZuulProxy注解,在Spring容器初始化的时候,会将Zuul的相关配置也初始化,Spring Boot提供了ServletRegistrationBean用于注册Servlet,Zuul中有一个类ZuulServlet在Servlet的service方法中执行各种ZuulFilter。下图是ZuulServlet的生命周期。

zuul把过滤器分为四个阶段,分别是

  • pre:主要是在请求路由之前调用,很多验证可以在这里做
  • route:在路由请求时候被调用,主要用来转发请求
  • post:主要用来处理响应请求
  • error:当错误发生时,会经由这个类型的过滤器处理

ZuulServlet的service方法在接收到请求后,会先执行pre阶段的过滤器,再执行routing阶段的过滤器,最后执行post阶段的过滤器,其中routing阶段的过滤器会将请求转发到“源服务”,源服务一般都是第三方服务,也可以是当前集群的其它服务,在实行pre、routing、post阶段发生异常时,会执行error过滤器,整个HTTP请求、响应等数据会被封装到RequestContext对象中。

Zuul路由配置

Zuul 简单路由

上面的配置方式对运维人员很不友好,因此Spring Cloud 团队实现Spring Cloud Zuul与Spring Cloud Eureka的整合然后进行路由操作了,并且默认的转发规则就是"Zuul网关地址+访问的服务名称+API URL",所以给定服务名称的时候尽量简短一点,服务名称直接可以从Eureka中获取。

比如:

网关地址: 127.0.0.1:8678
服务名称: auth
接口名称:/hi
通过Zuul网关访问的默认地址就是localhost:8678/auth/hi

server:
  port: 8678
spring:
  application:
    name: gateways-zuul-simple
zuul:
  routes:
    simple:
      url: http://127.0.0.1:8762 #转发的地址

上文的简单实用里面,我们说的配置方式合计就是简单路由。url 的配置会匹配http://或https://,如果直接配置127.0.0.1:8762将转发不成功。
这种配置方式是一种简单路由,由过滤器SimpleHostRoutingFilter使用HttpClient进行转发,该过滤器会将HttpServletRequest的请求数据转化为HttpClient的请求实例HttpRequest,然后把使用CloseableHttpClient转发。
当然Zuul也支持修改HttpClient的配置属性,

zuul:
  host:
    max-total-connections: 300 #设置目标主机的最大连接数,默认200
    max-per-route-connections: 30 #设置每个主机的初始化连接数,默认20

同样我们可以浏览器或者Postman 输入127.0.0.1:8678/simple/hi测试访问。

Zuul 路由前缀

我们可以通过zuul的prefix配置项配置前缀。

比如我们想给将通过网关的接口统一设置一个url前缀,可以使用如下配置

zuul:
  prefix: /api

那之前的接口在想请求simple 的数据,就必须带上/api,127.0.0.1:8678/simple/hi要变成127.0.0.1:8678/api/simple/hi

浏览器或者Postman 输入127.0.0.1:8678/api/simple/hi测试访问。

指定路由

新建项目模块cloud-gateways-zuul,引入依赖.

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
    </dependencies>

创建启动类

@EnableZuulProxy
@EnableDiscoveryClient
@SpringBootApplication
public class ZuulApplication {
    public static void main(String[] args) {
        SpringApplication.run(ZuulApplication.class, args);
    }
}

应用配置:

server:
  port: 8679
spring:
  application:
    name: gateways-zuul
eureka:
  client:
    service-url:
      defaultZone: http://xunfei:12345@127.0.0.1:8761/eureka/
zuul:
  routes:
    ribbon-server:
      path: /ribbon/** #设置前缀

另外上面的配置,还可以简写为:

zuul:
  routes:
    ribbon-server: 
      path: /ribbon/**

启动ribbon-server再启动当前项目。

在浏览器或Postman 输入127.0.01:8679/ribbon/user/1测试

Zuul 路由跳转

在简单路由里已经介绍了跳转到具体第三方地址的配置,下面我们来演示一下本地跳转的功能,Zuul 里面的本地跳转只要通过forward就可以了。

修改Zuul 部分的配置,添加一个forward的API前缀

server:
  port: 8679
spring:
  application:
    name: gateways-zuul
eureka:
  client:
    service-url:
      defaultZone: http://xunfei:12345@127.0.0.1:8761/eureka/
zuul:
  routes:
    ribbon-server:
      path: /ribbon/** #设置前缀
      url: forward:/local #添加一个forward的API前缀

添加控制层:

在gateways-zuul添加/local控制层

@RestController
public class LocalController {

    @GetMapping("/local/user/{id:\\d+}")
    public String getId(@PathVariable Long id){
        return id.toString()+",我是forward转发来的";
    }
}

重启当前项目,访问http://127.0.0.1:8679/ribbon/user/1进行测试.

Zuul 过滤器

Zuul作为网关来说,会做很多转发前的校验,而这些校验我们都可以基于Zuul过滤器来实现,比如Token校验、IP黑名单等。

新建过滤器

通过模拟用户必须携带token,来拦截用户的请求。前面我们说过Zuul 的生命周期,我们这里模拟在Header里面需要携带token,并且token 的值必须为123456,否则就会执行失败。

@Slf4j
public class TokenFilter extends ZuulFilter {

    @Override
    public String filterType() {
        return "pre"; // 在请求被路由之前调用
    }

    @Override
    public int filterOrder() {
        return 0; // filter执行顺序,通过数字指定 ,优先级为0,数字越大,优先级越低
    }

    @Override
    public boolean shouldFilter() {
        return true; // 是否执行该过滤器,此处为true,说明需要过滤
    }

    @Override
    public Object run() {
        log.info("我是TokenFilter");
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();

        String token = request.getHeader("token");// 获取请求的参数

        // 如果有token参数并且token值为123456,才进行路由
        if (StringUtils.isNotBlank(token) && token.equals("123456")) {
            ctx.setSendZuulResponse(true); //对请求进行路由
            ctx.setResponseStatusCode(200);
            ctx.set("code", 1);
        } else {
            ctx.setSendZuulResponse(false); //不对其进行路由
            ctx.setResponseStatusCode(401);
            HttpServletResponse response = ctx.getResponse();
            response.setHeader("content-type", "text/html;charset=utf8");
            ctx.setResponseBody("认证失败");
            ctx.set("code", 0);
        }
        return null;
    }
}

自定义过滤器需要继承ZuulFilter , 并且需要实现下面几个方法:

  • shouldFilter : 是否执行该过滤器, true 为执行, false 为不执行,可以结合配置中心及结合业务逻辑设置
  • filterType :过滤器类型,pre 、route 、post 、error 。
  • filterOrder :过滤器的执行顺序,数值越小,优先级越高。
  • run :执行自己的业务逻辑。设置的ctx.setSendZuulResponse(false); 代表不对其进行路由,ctx.setSendZuulResponse(true);表示对请求进行路由。

新建配置类,配置过滤器:

@Configuration
public class ZuulConfig {

    /**
     * 过滤器创建后生效,同理禁用注释掉即可
     * @return
     */
    @Bean
    public TokenFilter tokenFilter(){
        return new TokenFilter();
    }
    
}

关闭之前配置的本地跳转配置:

server:
  port: 8679
spring:
  application:
    name: gateways-zuul
eureka:
  client:
    service-url:
      defaultZone: http://xunfei:12345@127.0.0.1:8761/eureka/
zuul:
  routes:
    ribbon-server:
      path: /ribbon/** #设置前缀
      #url: forward:/local #添加一个forward的API前缀

重启应用浏览器或Postman 输入127.0.0.1:8679/ribbon/user/1访问 :

我们在请求中设置上token再次访问:

Zuul 过滤器拦截顺序

我们再建一个新的过滤器,这样才能有对比的演示顺序

新建一个过滤器,设置执行顺序为5(在前面的TokenFilter之后),这个过滤器是验证header中是否有name这个key,并且key的值要是zuul才能通过,否则失败。

@Slf4j
public class ZuulFilter extends com.netflix.zuul.ZuulFilter {
    @Override
    public String filterType() {
        return "pre"; // 在请求被路由之前调用
    }

    @Override
    public int filterOrder() {
        return 5; // filter执行顺序,通过数字指定 ,优先级为0,数字越大,优先级越低
    }

    @Override
    public boolean shouldFilter() {
        return true; // 是否执行该过滤器,此处为true,说明需要过滤
    }

    @Override
    public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        log.info("我是ZuulFilter");
        String token = request.getHeader("name");// 获取请求的参数

        // 如果有name参数并且token值为zuul,才进行路由
        if (StringUtils.isNotBlank(token) && token.equals("zuul")) {
            ctx.setSendZuulResponse(true); //对请求进行路由
            ctx.setResponseStatusCode(200);
            ctx.set("code", 1);
        } else {
            ctx.setSendZuulResponse(false); //不对其进行路由
            ctx.setResponseStatusCode(401);
            HttpServletResponse response = ctx.getResponse();
            response.setHeader("content-type", "text/html;charset=utf8");
            ctx.setResponseBody("请携带网关必须参数");
            ctx.set("code", 0);
        }
        return null;
    }
}

同样的我们加上这个配置:

    @Bean
    public ZuulFilter zuulFilter(){
        return new ZuulFilter();
    }

重启项目
浏览器或Postman输入127.0.0.1:8679/ribbon/user/1

首先全部设置正确的header

和前面一样一切正常.并且看到控制台打印出了执行顺序:

但是当我们去掉token请求时,虽然第一TokenFilter失败了,但是后面ZuulFilter还是执行了.

按理说应该不会执行第二个过滤器,所以这是为什么呢?

Zuul中Filter的执行过程如下:

在ZuulServlet的service方法中执行对应的Filter,比如preRoute()。preRoute()中会通过zuulRunner来执行

过程源码如下:

首先请求经过ZuulServlet执行

@Override
public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {
    try {
        init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);

        // Marks this request as having passed through the "Zuul engine", as opposed to servlets
        // explicitly bound in web.xml, for which requests will not have the same data attached
        RequestContext context = RequestContext.getCurrentContext();
        context.setZuulEngineRan();

        try {
            preRoute();
        } catch (ZuulException e) {
            error(e);
            postRoute();
            return;
        }
        try {
            route();
        } catch (ZuulException e) {
            error(e);
            postRoute();
            return;
        }
        try {
            postRoute();
        } catch (ZuulException e) {
            error(e);
            return;
        }

    } catch (Throwable e) {
        error(new ZuulException(e, 500, "UNHANDLED_EXCEPTION_" + e.getClass().getName()));
    } finally {
        RequestContext.getCurrentContext().unset();
    }
}

执行pre类型的过滤器

/**
 * executes "pre" filters
 *
 * @throws ZuulException
 */
void preRoute() throws ZuulException {
    zuulRunner.preRoute();
}

调用FilterProcessor的preRoute()

public void preRoute() throws ZuulException {
    FilterProcessor.getInstance().preRoute();
}

然后preRoute()调用runFilters()获取所有过滤器并执行

public void preRoute() throws ZuulException {
    try {
        this.runFilters("pre");
    } catch (ZuulException var2) {
        throw var2;
    } catch (Throwable var3) {
        throw new ZuulException(var3, 500, "UNCAUGHT_EXCEPTION_IN_PRE_FILTER_" + var3.getClass().getName());
    }
}

public Object runFilters(String sType) throws Throwable {
    if (RequestContext.getCurrentContext().debugRouting()) {
        Debug.addRoutingDebug("Invoking {" + sType + "} type filters");
    }

    boolean bResult = false;
    List<ZuulFilter> list = FilterLoader.getInstance().getFiltersByType(sType);
    if (list != null) {
        for(int i = 0; i < list.size(); ++i) {
            ZuulFilter zuulFilter = (ZuulFilter)list.get(i);
            Object result = this.processZuulFilter(zuulFilter);
            if (result != null && result instanceof Boolean) {
                bResult |= (Boolean)result;
            }
        }
    }

    return bResult;
}

由此,可以知道为什么第一个报错了,为什么第二个过滤器也执行。

要解决这个问题需要用到数据传递👇

Zuul 数据传递

通过分析Zuul 的这段源码,可以看出,先判断这个过滤器是不是已经禁用了,禁用的话不执行,然后判断shouldFilter(),我们可以通过设置shouldFilter()的true、false 来控制过滤器的执行。

public ZuulFilterResult runFilter() {
    ZuulFilterResult zr = new ZuulFilterResult();
    if (!this.isFilterDisabled()) {
        if (this.shouldFilter()) {
            Tracer t = TracerFactory.instance().startMicroTracer("ZUUL::" + this.getClass().getSimpleName());

            try {
                Object res = this.run();
                zr = new ZuulFilterResult(res, ExecutionStatus.SUCCESS);
            } catch (Throwable var7) {
                t.setName("ZUUL::" + this.getClass().getSimpleName() + " failed");
                zr = new ZuulFilterResult(ExecutionStatus.FAILED);
                zr.setException(var7);
            } finally {
                t.stopAndLog();
            }
        } else {
            zr = new ZuulFilterResult(ExecutionStatus.SKIPPED);
        }
    }

    return zr;
}

因此我们可以在第一个失败的时候,通过RequestContext传递变量,为什么用RequestContext,主要还是因为RequestContext的实现原理是基于ThreadLocal实现的,源码如下

private static RequestContext testContext = null;
protected static final ThreadLocal<? extends RequestContext> threadLocal = new ThreadLocal<RequestContext>() {
    protected RequestContext initialValue() {
        try {
            return (RequestContext)RequestContext.contextClass.newInstance();
        } catch (Throwable var2) {
            throw new RuntimeException(var2);
        }
    }
};

在TokenFilter认证失败处添加如下代码,设置isShould为false。

//失败之后通知后续不应该执行了
ctx.set("isShould",false);

将ZuulFilter 中的shouldFilter()修改成如下。

    @Override
    public boolean shouldFilter() {
        RequestContext requestContext = RequestContext.getCurrentContext();
        Boolean isShould = (Boolean) requestContext.get("isShould");
        return null == isShould ? true : isShould; // 是否执行该过滤器,此处为true,说明需要过滤
    }

重启项目后再次测试,这一次发现第一个认证失败后后面不在继续执行。

Zuul 禁用过滤器

Zuul 禁用过滤器有两种方法,第一种就是我们前面说的通过代码把对应的配置bean注释掉即可.如:我们需要禁用TokenFilter:

//    @Bean
//    public TokenFilter tokenFilter(){
//        return new TokenFilter();
//    }

另一种办法是通过修改配置的形式:

zuul:
    TokenFilter: #要禁用的过滤器名
      route:
        disabled: true

重启验证,发现TokenFilter已经失效。

Zuul 异常处理

对于过滤器的各阶段在执行时,异常主要发生在run()方法中,Zuul 为我们提供了异常处理的过滤器,方法执行时抛出的异常会被捕获到并且调用RequestContext.setThrowable 方法设置异常,error 阶段的SendErrorFilter会判断RequestContext中是否存在异常,如果存在会执行SendErrorFilter过滤器。
SendErrorFilter过滤器在执行的时候,会将异常信息写到HttpServletRequest中,再调用RequestDispatcher的forward方法,默认会跳转到/error页面,我们这里会实现一个/error 的接口,让错误信息可以被我们自定义成JSON输出。

新建异常过滤器

@Slf4j
public class ErrorFilter extends ZuulFilter {
    @Override
    public String filterType() {
        return "error";
    }

    @Override
    public int filterOrder() {
        return 10;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
        Throwable throwable = ctx.getThrowable();
        ctx.setSendZuulResponse(false); //不对其进行路由
        ctx.setResponseStatusCode(401);
        HttpServletResponse response = ctx.getResponse();
        response.setHeader("content-type", "text/html;charset=utf8");
        ctx.setResponseBody("认证失败" + throwable.getCause().getMessage());
        ctx.set("code", 500);
        log.error("异常信息,{}", throwable.getCause().getMessage());
        return null;
    }
}

添加配置

@Bean
public ErrorFilter errorFilter(){
    return new ErrorFilter();
}

在TokenFilter添加一个异常(如果前面禁用了TokenFilter这里需要取消掉或者添加到ZuulFilter中):

    @Override
    public Object run() {
        log.info("我是TokenFilter");
        //故意抛出异常
        int i = 123 / 0;
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();

        String token = request.getHeader("token");// 获取请求的参数

        // 如果有token参数并且token值为123456,才进行路由
        if (StringUtils.isNotBlank(token) && token.equals("123456")) {
            ctx.setSendZuulResponse(true); //对请求进行路由
            ctx.setResponseStatusCode(200);
            ctx.set("code", 1);
        } else {
            ctx.setSendZuulResponse(false); //不对其进行路由
            ctx.setResponseStatusCode(401);
            HttpServletResponse response = ctx.getResponse();
            response.setHeader("content-type", "text/html;charset=utf8");
            ctx.setResponseBody("认证失败");
            ctx.set("code", 0);
            ctx.set("isShould",false);
        }
        return null;
    }

这里提前创建一个信息的统一处理类(封装的返回类):

@Data
public class ApiResult<T> implements Serializable {

    private static final long serialVersionUID = 1L;

    private static final int SUCCESS = 200;

    private static String MSG_SUCCESS = "操作成功";

    private static int FAIL = 500;

    private static String MSG_FAIL = "操作失败";

    private int code;

    private String msg;

    private T data;

    //分页信息
    private Object meta;

    public static <T> ApiResult<T> ok() {
        return restResult(null, SUCCESS, MSG_SUCCESS, null);
    }

    public static <T> ApiResult<T> okMsg(String msg) {
        return restResult(null, SUCCESS, msg, null);
    }

    public static <T> ApiResult<T> ok(T data) {
        return restResult(data, SUCCESS, MSG_SUCCESS, null);
    }

    public static <T> ApiResult<T> ok(T data, String msg) {
        return restResult(data, SUCCESS, msg, null);
    }

    public static <T> ApiResult<T> okMeta(T data) {
        return restResult(data, SUCCESS, null, data);
    }

    public static <T> ApiResult<T> ok(T data, String msg, Object meta) {
        return restResult(data, SUCCESS, msg, meta);
    }

    public static <T> ApiResult<T> failed() {
        return restResult(null, FAIL, MSG_FAIL, null);
    }

    public static <T> ApiResult<T> failedMsg(String msg) {
        return restResult(null, FAIL, msg, null);
    }

    public static <T> ApiResult<T> failedCodeMsg(int code, String msg) {
        return restResult(null, code, msg, null);
    }

    public static <T> ApiResult<T> failed(T data) {
        return restResult(data, FAIL, MSG_FAIL, null);
    }

    public static <T> ApiResult<T> failedCodeData(int code, T data) {
        return restResult(data, code, MSG_FAIL, null);
    }

    public static <T> ApiResult<T> failed(T data, String msg) {
        return restResult(data, FAIL, msg, null);
    }

    private static <T> ApiResult<T> restResult(T data, int code, String msg, Object meta) {
        ApiResult apiResult = new ApiResult();
        apiResult.setCode(code);
        apiResult.setData(data);
        apiResult.setMsg(msg);
        apiResult.setMeta(meta);
        return apiResult;
    }

}

新建异常处理处理类

@RestController
public class ErrorHandlerController implements ErrorController {

    @Autowired
    private ErrorAttributes errorAttributes;

    @Override
    public String getErrorPath() {
        return "/error";
    }

    @RequestMapping("/error")
    public ApiResult error(HttpServletRequest request) {
        Map<String, Object> errorAttributes = this.errorAttributes.getErrorAttributes(new ServletWebRequest(request), true);
        String message = (String) errorAttributes.get("message");
        String trace = (String) errorAttributes.get("trace");
        if (StringUtils.isNotBlank(trace)) {
            message = message + ",trace is " + trace;
        }
        return ApiResult.failed(message);
    }
}

浏览器或Postman 输入127.0.0.1:8679/ribbon/user/1 进行测试

重试机制

之前我们在介绍Feign,也讲到的重试机制,现在我们来说一下Zuul的重试机制,希望Zuul 在转发服务的时候,如果失败可以重试几次,可能第一次是网络抖动,或者转发到相同服务名是的其它地址上面。
spring-retry是spring提供的一个基于spring的重试框架,我们直接使用这个框架.

首先我们注释掉前面为了测试错误故意抛出的异常代码。

maven 配置添加

<!--重试包-->
<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
</dependency>

配置中开启重试(默认不开启)

zuul:
  retryable: true #开启重试

开启后的配置和Ribbon 配置一样,如下

#重试
ribbon:
  #配置首台服务器重试1次
  MaxAutoRetries: 1
  #配置其他服务器重试两次
  MaxAutoRetriesNextServer: 2
  #链接超时时间
  ConnectTimeout: 500
  #请求处理时间
  ReadTimeout: 500
  #每个操作都开启重试机制
  OkToRetryOnAllOperations: true

这里可以直接使用Ribbon 的配置,是因为zuul 里面已经集成了Ribbon的包

修改后完整的配置如下:

server:
  port: 8679
spring:
  application:
    name: gateways-zuul
eureka:
  client:
    service-url:
      defaultZone: http://xunfei:12345@127.0.0.1:8761/eureka/
zuul:
  routes:
    ribbon-server:
      path: /ribbon/** #设置前缀
      #url: forward:/local #添加一个forward的API前缀
  retryable: true #开启重试

ribbon:
  #配置首台服务器重试1次
  MaxAutoRetries: 1
  #配置其他服务器重试两次
  MaxAutoRetriesNextServer: 2
  #链接超时时间
  ConnectTimeout: 500
  #请求处理时间
  ReadTimeout: 500
  #每个操作都开启重试机制
  OkToRetryOnAllOperations: true

然后我们在Ribbon Server添加随机延时
超过500秒就会重试

int millis = new Random().nextInt(3000);
System.out.println("client线程休眠时间:"+millis);
Thread.sleep(millis);

浏览器或Postman 输入127.0.0.1:8679/ribbon/user/1

我们看到控制台的请求次数(休眠)

Zuul FallBack

虽然前面我们配置了重试机制,但是重试次数结束了没成功任然会失败,比如如下错误,因为Spring Cloud Zuul 里面已经集成了Hystrix,我们可以自己定义回退机制。

Caused by: java.net.SocketTimeoutException: Read timed out

新建FallBack类:

实现回退需要实现FallbackProvider接口

@Slf4j
@Component
public class ZuulFallBack implements FallbackProvider {

    /**
     * 方法中返回*表示对所有服务进行回退操作,
     * 如果只想对某个服务进行回退,那么就返回需要回退的服务名称,
     * 这个名称是注册到Eureka中的名称
     *
     * @return
     */
    @Override
    public String getRoute() {
        return "*";
    }

    /**
     * 构造回退的内容
     *
     * @param route
     * @param cause
     * @return
     */
    @Override
    public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
        return new ClientHttpResponse() {
            /**
             * 返回响应的状态码
             * @return
             * @throws IOException
             */
            @Override
            public HttpStatus getStatusCode() throws IOException {
                return HttpStatus.OK;
            }

            @Override
            public int getRawStatusCode() throws IOException {
                return this.getStatusCode().value();
            }

            /**
             * 返回响应状态码对应的文本
             * @return
             * @throws IOException
             */
            @Override
            public String getStatusText() throws IOException {
                return this.getStatusCode().getReasonPhrase();
            }

            @Override
            public void close() {

            }

            /**
             * 返回回退的内容
             * @return
             * @throws IOException
             */
            @Override
            public InputStream getBody() throws IOException {
                RequestContext ctx = RequestContext.getCurrentContext();
                Throwable throwable = ctx.getThrowable();
                if (null != throwable) {
                    log.error("Zuul发生错误,{}", throwable.getCause().getMessage());
                    ApiResult<String> byteMsg = ApiResult.failed(throwable.getCause().getMessage(), "网络或服务异常");
                    return new ByteArrayInputStream(JSONUtil.toJsonStr(byteMsg).getBytes());
                }
                ApiResult<String> byteMsg = ApiResult.failedMsg("网络或服务异常");
                return new ByteArrayInputStream(JSONUtil.toJsonStr(byteMsg).getBytes());
            }

            /**
             * 返回响应的请求头信息
             * @return
             */
            @Override
            public HttpHeaders getHeaders() {
                HttpHeaders headers = new HttpHeaders();
                MediaType mediaType = new MediaType("application", "json", StandardCharsets.UTF_8);
                headers.setContentType(mediaType);
                return headers;
            }
        };
    }
}
  • getRoute 方法中返回*表示对所有服务进行回退操作,如果只想对某个服务进行回退,那么就返回需要回退的服务名称,这个名称是注册到Eureka中的名称
  • ClientHttpResponse 构造回退的内容
  • getStatusCode 返回响应的状态码
  • getStatusText 返回响应状态码对应的文本
  • getBody 返回回退的内容
  • getHeaders 返回响应的请求头信息

设置hystrix 超时时间配置

#回退超时时间
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 1000 #一秒

重启服务
浏览器或Postman 输入地址127.0.0.1:8679/ribbon/user/1,如果想快速看到,可以直接把ribbon-server关掉肯定失败

更新时间:2020-04-23 18:05:35

本文由 寻非 创作,如果您觉得本文不错,请随意赞赏
采用 知识共享署名4.0 国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
原文链接:https://www.zhouning.group/archives/springcloud06zuul网关
最后更新:2020-04-23 18:05:35

评论

Your browser is out of date!

Update your browser to view this website correctly. Update my browser now

×