Spring-Cloud-Zuul网关实现

有些人可能学了Spring-Cloud后想要将Spring-Cloud整合进自己的项目,但是自己的项目是一个旧项目。那么要想升级就会很复杂,但是这一节讲的网关至少是可以轻松与旧项目整合起来的。

背景介绍

Spring-Cloud体系中后端的服务将会有无法估量的数量,可能只有几个也可能有上百个,并且同一个服务可能都会部署好几个,那么让前端直接调用,我想前端心态要炸了。所以为了简化前端的调用,就有了zuul这样的api gateway。同时它也可以提供负载均衡、反向代理、权限认证的作用。

那么我就来看看zuul怎么使用吧!

案例实践

本案例使用Spring-Cloud Hoxton.SR5 版本

简单应用

引入依赖

<dependency>
    <groupId>org.springframework.cloudgroupId>
    <artifactId>spring-cloud-starter-netflix-zuulartifactId>
dependency>

配置文件

spring:
  application:
    name: spring-cloud-zuul
server:
  port: 8888
zuul:
  routes:
    baidu:
      path: /it/** #访问/it/**都会重定向到百度首页
      url: http://www.ityouknow.com/
    hello:
      path: /hello/**
      url: http://localhost:9000/

启动类中添加@EnableZuulProxy注解启用zuul代理

@SpringBootApplication
@EnableZuulProxy
public class SpringCloudZuulApplication {
     

    public static void main(String[] args) {
     
        SpringApplication.run(SpringCloudZuulApplication.class, args);
    }
}

测试代理

将服务启动,访问http://localhost:8888/it/spring-cloud就会看到纯洁的微笑Spring Cloud系列博客。
我们再将之前的服务提供者启动,然后访问http://localhost:8888/hello/hello?name=bennett就可以正常得到hello接口的返回了。

利用这一点我们就可以将旧项目和Spring-Cloud项目整合在一起。

服务化

上面的提供者服务只有一个,如果有很多个我们就要配置很多个这样的路由,很显然这样做会很累的。那么这时候我们就需要依赖Eureka这样的注册发现服务来帮助我们完成这个工作了,zuul默认会代理所有在Eureka里注册的服务,这样我们就不必每个服务都手动设置代理了。

引入依赖

<dependency>
    <groupId>org.springframework.cloudgroupId>
    <artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>

配置文件

我们先来尝试手动的代理注册到Eureka的服务。

zuul:
  routes:
    api-a:
      path: /producer/**
      serviceId: spring-cloud-producer
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8000/eureka/

然后重启zuul,并将Eureka启动。

访问http://localhost:8888/producer/hello?name=bennett,可以正常得到我们的hello接口的返回信息。

现在我们把刚才配置的路由去掉,重启zuul。

再次访问http://localhost:8888/spring-cloud-producer/hello?name=bennett,我们可以得到跟刚才一样的返回信息,如果觉得是缓存也可以把name的值换成别的。

访问zuul代理的服务的规则:http://ZUUL_HOST:ZUUL_PORT/微服务在Eureka上的serviceId/**

路由重试

如果因为网络等原因导致路由代理失败,那么服务就会暂时不可用,如果我们希望可以连接重试的话zuul也为我们实现了这个功能,它需要与spring-retry结合使用。

引入依赖

<dependency>
    <groupId>org.springframework.retrygroupId>
    <artifactId>spring-retryartifactId>
dependency>

配置文件

zuul:
  retryable: true #开启重试
ribbon:
  MaxAutoRetries: 2 # 当前服务最大重试次数
  MaxAutoRetriesNextServer: 0 # 切换相同Server的次数

测试重试

将zuul服务重启,为了模拟超时重试,我们可以用线程睡眠实现。

修改服务提供者的hello接口,让这个接口的线程睡眠60000毫秒,并且在执行睡眠前添加日志看是否有重试。

@RestController
public class HelloController {
     

    private static final Logger log = LoggerFactory.getLogger(HelloController.class);

    @RequestMapping("/hello")
    public String hello(String name, HttpServletRequest request) {
     
        int port = request.getServerPort();
        log.info("request from {}, name is {}", port, name);
        // 模拟超时异常
        try {
     
            Thread.sleep(1000000);
        } catch (Exception e) {
     
            log.error(" hello {} error", port, e);
        }
        return "hello " + name + ", this is " + port + " first message";
    }
}

修改完后我们将服务重启,访问http://localhost:8888/spring-cloud-producer/hello?name=bennett,我们通过日志可以看到,总共输出了三次request from 9000, name is bennett。这说明重试确实在起作用。

路由熔断

上面我们测试时,连接超时后重试了两次之后还是超时,最后服务就给我们返回了超时的错误。但是那么我们有没有办法像Hystrix一样提供一个熔断降级呢?答案时肯定的,zuul为我们提供一个FallbackProvider接口类,我们可以通过实现这个类来自定义我们的熔断降级。

package org.springframework.cloud.netflix.zuul.filters.route;

import org.springframework.http.client.ClientHttpResponse;

/**
 * Provides fallback when a failure occurs on a route.
 *
 * @author Ryan Baxter
 * @author Dominik Mostek
 */
public interface FallbackProvider {
     

	/**
	 * The route this fallback will be used for.
	 * @return The route the fallback will be used for.
	 */
	String getRoute();

	/**
	 * Provides a fallback response based on the cause of the failed execution.
	 * @param route The route the fallback is for
	 * @param cause cause of the main method failure, may be null
	 * @return the fallback response
	 */
	ClientHttpResponse fallbackResponse(String route, Throwable cause);

}
  • getRoute()方法是我们要熔断降级的路由
  • fallbackResponse()方法可以处理熔断降级操作

接下来我们自定义一个实现类

@Component
public class ProducerFallback implements FallbackProvider {
     

    private static final Logger log = LoggerFactory.getLogger(ProducerFallback.class);

    @Override
    public String getRoute() {
     
        return "spring-cloud-producer";
    }

    @Override
    public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
     
        if (cause != null && cause.getCause() != null) {
     
            log.info("Exception {}", cause.getCause().getMessage());
        }
        return new ClientHttpResponse() {
     
            /**
             * Return the headers of this message.
             *
             * @return a corresponding HttpHeaders object (never {@code null})
             */
            @Override
            public HttpHeaders getHeaders() {
     
                HttpHeaders headers = new HttpHeaders();
                headers.setContentType(MediaType.APPLICATION_JSON);
                return headers;
            }

            /**
             * Return the body of the message as an input stream.
             *
             * @return the input stream body (never {@code null})
             * @throws IOException in case of I/O errors
             */
            @Override
            public InputStream getBody() throws IOException {
     
                return new ByteArrayInputStream("The service is unavailable.".getBytes());
            }

            /**
             * Get the HTTP status code as an {@link HttpStatus} enum value.
             * 

For status codes not supported by {@code HttpStatus}, use * {@link #getRawStatusCode()} instead. * * @return the HTTP status as an HttpStatus enum value (never {@code null}) * @throws IOException in case of I/O errors * @throws IllegalArgumentException in case of an unknown HTTP status code * @see HttpStatus#valueOf(int) * @since #getRawStatusCode() */ @Override public HttpStatus getStatusCode() throws IOException { return HttpStatus.OK; } /** * Get the HTTP status code (potentially non-standard and not * resolvable through the {@link HttpStatus} enum) as an integer. * * @return the HTTP status as an integer value * @throws IOException in case of I/O errors * @see #getStatusCode() * @see HttpStatus#resolve(int) * @since 3.1.1 */ @Override public int getRawStatusCode() throws IOException { return HttpStatus.OK.value(); } /** * Get the HTTP status text of the response. * * @return the HTTP status text * @throws IOException in case of I/O errors */ @Override public String getStatusText() throws IOException { return HttpStatus.OK.getReasonPhrase(); } /** * Close this response, freeing any resources created. */ @Override public void close() { } }; } }

经过这样处理,再次访问之前的超时接口就不会再发生直接响应服务超时的状态了,返回的将是我们给的The service is unavailable.,同样的我们也可以在这个地方添加我们自己的业务处理。

Zuul的过滤器

Zuul大部分功能都是通过过滤器来实现的,这些过滤器类型对应于请求的典型生命周期。

  • PRE: 这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
  • ROUTING:这种过滤器将请求路由到微服务。这种过滤器用于构建发送给微服务的请求,并使用Apache HttpClient或Netfilx Ribbon请求微服务。
  • POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。
  • ERROR:在其他阶段发生错误时执行该过滤器。 除了默认的过滤器类型,Zuul还允许我们创建自定义的过滤器类型。例如,我们可以定制一种STATIC类型的过滤器,直接在Zuul中生成响应,而不将请求转发到后端的微服务。

Zuul中默认实现的Filter

类型 顺序 过滤器 功能
pre -3 ServletDetectionFilter 标记处理Servlet的类型
pre -2 Servlet30WrapperFilter 包装HttpServletRequest请求
pre -1 FormBodyWrapperFilter 包装请求体
route 1 DebugFilter 标记调试标志
route 5 PreDecorationFilter 处理请求上下文供后续使用
route 10 RibbonRoutingFilter serviceId请求转发
route 100 SimpleHostRoutingFilter url请求转发
route 500 SendForwardFilter forward请求转发
post 0 SendErrorFilter 处理有错误的请求响应
post 1000 SendResponseFilter 处理正常的请求响应

禁用指定的Filter

可以在application.yml中配置需要禁用的filter,格式:

zuul:
  FormBodyWrapperFilter:
    pre:
      disable: true

自定义Filter

实现自定义Filter,需要继承ZuulFilter的类,并覆盖其中的4个方法。
下面自定义了一个检查是否携带token的的过滤器,没有token的不对其路由。

@Component
public class TokenFilter extends ZuulFilter {
     

    private static final Logger log = LoggerFactory.getLogger(TokenFilter.class);

    @Override
    public String filterType() {
     
        // 可以在路由前调用,总共有四种pre,post,error,route
        return "pre";
    }

    @Override
    public int filterOrder() {
     
        // filter调用优先级,数字越小优先级越高
        return 0;
    }

    @Override
    public boolean shouldFilter() {
     
        // 是否需要执行filter,默认为false不执行
        return true;
    }

    @Override
    public Object run() throws ZuulException {
     
        RequestContext rc = RequestContext.getCurrentContext();
        HttpServletRequest request = rc.getRequest();
        log.info("--->> TokenFilter {}, {}", request.getMethod(), request.getRequestURL());
        String token = request.getParameter("token");
        if (StringUtils.isBlank(token)) {
     
            // 不对其路由
            rc.setSendZuulResponse(false);
            rc.setResponseStatusCode(400);
            rc.setResponseBody("token is empty");
            rc.set("isSuccess", false);
        } else {
     
            // 进行路由
            rc.setSendZuulResponse(true);
            rc.setResponseStatusCode(200);
            rc.set("isSuccess", true);
        }
        return null;
    }
}

测试自定义Filter

将服务重启,访问http://localhost:8888/spring-cloud-producer/hello?name=bennett,我们得到了400的状态码以及token is empty的响应信息,并没有给我们路由到9000端口。

我将token参数加上,再次访问http://localhost:8888/spring-cloud-producer/hello?name=bennett&token=token我们得到了正确的响应结果(如果没有注释掉之前的线程睡眠,则返回的是服务不可用)。

关于zuul网关的就学习到这里了。

文章参考

纯洁的微笑:服务网关zuul初级篇、服务网关Zuul高级篇
SpringCloud中文网:路由和过滤器:Zuul

案例源码

码云:spring-cloud-demo

你可能感兴趣的:(Spring,Cloud,spring-cloud,zuul,zuul,retry)