天天看点

Spring Cloud学习之路由网关-Zuul

1、为什么需要Zuul

1.1、Zuul、Ribbon和Eureka相结合,可以实现智能路由和负载均衡的功能,Zuul可以将请求流量按照某种策略分发到集群的多个服务实例上;

1.2、gateway将所有API接口统一聚合,并统一对外暴露,外界系统统一调用gateway提供的接口,外界无需知道微服务直接接口相互的调用关系,为微服务的API接口提供了更高的安全性;

1.3、Gateway服务可以用作身份认证和权限控制,防止非法访问API接口;

1.4、Gateway可以实现监控、日志输出和对请求进行记录;

1.5、Gateway可以实现流量的监控,在高流量的情况下,可以对服务进行降级;

1.6、Gateway将所有接口都统一对外,方便测试。

2、Zuul的工作原理

Zuul是通过Servlet实现的,通过自定义的ZuulServlet对请求进行控制。Zuul的核心是一系列的过滤器,可以在Http请求的发起到相应返回期间执行一系列的过滤器,主要有一下四种过滤器:

a、Pre过滤器,请求路由之前执行的过滤器,主要用做安全验证;

b、Routing过滤器,用于具体路由到微服务的实例,默认使用Http Client进行网络请求;

c、Post过滤器,在请求被路由到微服务后执行的,用作收集统计信息,指标,已经将相应传输到客户端;

d、Error过滤器,在其他过滤器发生错误执行。

由于Zuul的过滤器之间是无法直接通信,所以只能通过RequestContext对象进行共享数据,每个请求都会创建一个RequestContext对象。每个过滤器都有如下特性:

a、Type(类型),过滤器的类型,定义以上4中过滤器的一种;

b、Execution Order(执行顺序),过滤的执行顺序,Order值越小,越优先执行;

c、Criteria(标准),过滤器执行所需的条件;

d、Action(动作),符合Criteria的,则执行具体的Action,具体的逻辑代码实现。

3、源码实战

3.1、建立Zuul服务

新建一个eureka-zuul-client的工程,添加相关依赖,Pom文件如下:

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.hzlitai</groupId>
        <artifactId>eureka-memo</artifactId>
        <version>1.1-SNAPSHOT</version>
    </parent>
    <groupId>com.hzlitai</groupId>
    <artifactId>eureka-zuul-client</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>eureka-zuul-client</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <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>
        <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>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
           

程序的启动类添加相关注解,代码如下:

package com.hzlitai.eurekazuulclient;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;

@EnableEurekaClient
@EnableZuulProxy
@SpringBootApplication
public class EurekaZuulClientApplication {

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

}
           

在application.yml配置中增加如下代码:

server:
  port: 5000

spring:
  application:
    name: service-zuul
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka

zuul:
  prefix: /v1
  routes:
    hiapi:
      path: /hiapi/**
      serviceId: eureka-client
    ribbonapi:
      path: /rebbonapi/**
      serviceId: eureka-ribbon-client
    feignapi:
      path: /feignapi/**
      serviceId: eureka-feign-client
           

服务端口为:5000,服务实例名:service-zuul,注册到服务中心:serviceUrl,统一版本:prefix,路由配置:routes。

依次启动各个服务后,访问:http://localhost:5000/ribbonapi/sayHi?name=zhuheliang,浏览器会交替出现访问不同端口的结果。可见Zuul在路由转发实现了负载均衡。

3.2、在Zuul上配置熔断器Hystrix

在Zuul实现熔断功能需要实现FallbackProvider接口,实现两个方法:getRoute()和fallbackResponse(),源码如下:

package com.hzlitai.eurekazuulclient.provider;

import com.netflix.hystrix.exception.HystrixTimeoutException;
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 ZuulFallbackProvider implements FallbackProvider {
    @Override
    public String getRoute() {
        //api服务id(eureka-client),如果需要所有调用都支持回退,则return "*"或 return null
        return "*";
    }

    @Override
    public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
        if (cause instanceof HystrixTimeoutException) {
            return response(HttpStatus.GATEWAY_TIMEOUT);
        } else {
            return response(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

    private ClientHttpResponse response(final HttpStatus status) {
        return new ClientHttpResponse() {
            @Override
            public HttpStatus getStatusCode() throws IOException {
                //return status;
                return HttpStatus.BAD_REQUEST;
            }

            @Override
            public int getRawStatusCode() throws IOException {
                //return status.value();
                return HttpStatus.BAD_REQUEST.value();
            }

            @Override
            public String getStatusText() throws IOException {
                //return status.getReasonPhrase();
                //return HttpStatus.BAD_REQUEST.name();
                return HttpStatus.BAD_REQUEST.getReasonPhrase();
            }

            @Override
            public void close() {
            }

            @Override
            public InputStream getBody() throws IOException {
                return new ByteArrayInputStream(("fallback:"+ZuulFallbackProvider.this.getRoute()).getBytes());
            }

            @Override
            public HttpHeaders getHeaders() {
                HttpHeaders headers = new HttpHeaders();
                headers.setContentType(MediaType.APPLICATION_JSON);
                return headers;
            }
        };
    }
}
           

3.3、在Zuul中自定义过滤器

Zuul自定义过滤器只需继承ZuulFilter类,实现ZuulFilter里的抽象方法,有filterType()和filterOrder(),还有IZuulFilter的shouldFilter()和Object run(),示例代码如下:

package com.hzlitai.eurekazuulclient.filter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;

import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_TYPE;

@Component
public class MyFilter extends ZuulFilter {
    @Override
    public String filterType() {
        return PRE_TYPE;
    }

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

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

    @Override
    public Object run() throws ZuulException {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request =  ctx.getRequest();
        Object accessToken = request.getParameter("accessToken");
        if(accessToken == null){
            ctx.setSendZuulResponse(false);
            ctx.setResponseStatusCode(401);
            try{
                ctx.getResponse().getWriter().write("token is empty");
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        return null;
    }
}