SpringCloud 之 Zuul 基础配置与进阶

  • 简介
  • 基础使用
  • 准备
  • 加依赖
  • 启动器加注释
  • 配置
  • 日志查看
  • 不加额外配置
  • 自定义服务访问以及服务忽略
  • 自定义路由名配置
  • 直接通过 URL 配置(有缺陷)
  • 直接通过 URL 配置(无缺陷)
  • 路由前缀
  • 进阶配置
  • 正则表达式指定Zuul的路由匹配规则
  • 自定义 Zuul 拦截器
  • 禁用自定义拦截器
  • 容错与回退


简介

springcloud有哪些注解_网关

基础使用

PS:zuul 基本需要配合 Eureka 使用,就不多介绍了:SpringCloud 之 Eureka 配置,Eureka 集群,Eureka 监听

准备

服务A
服务名:service-a
端口号:8080

服务B
服务名:service-b
端口号:8081

zuul 服务
服务名:zuul
端口号:8084

加依赖

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

启动器加注释

@EnableZuulProxy

配置

PS:Eureka 配置就不写了,和上面 Eureka 里面客户端配置一样

#配置端口号
server:
  port: 8084

spring:
  application:
    #配置服务名
    name: zuul

日志查看

# netflix ⽇志
logging:
  level:
    com.netflix: DEBUG

如无法掌握Zuul路由的规律,可将com.netflix包的日志级别设为DEBUG。这样,Zuul就会打印 转发的具体细节,从而帮助我们更好地理解Zuul的路由配置

springcloud有哪些注解_spring cloud zuul_02


意思:service-a返回服务器:172.0.0.1:8080 的请求是 /api/1

不加额外配置

在不加多余的配置文件配置时, zuul 就已经可以使用了,我们可以通过 IP + 服务名 的方式结合 Eureka 去访问,如图:

springcloud有哪些注解_拦截器_03

springcloud有哪些注解_springcloud有哪些注解_04

自定义服务访问以及服务忽略

虽然说不加配置就可以使用了,但是 Eureka 中所有的服务都能访问到这显然很不合理,也不安全
因此可以通过自定义服务与服务忽略结合,来提高可用性
ignored-services: '*' 表示忽略所有服务的前提下,只对配置的服务进行路由
ignored-services: service-a,service-b 当然也可以指定服务,逗号隔开
ignoredPatterns: /**/admin/** 如果想使用通配符进行忽略,那就要换 ignoredPatterns 才行
这个是忽略所有包含 /admin/ 的路径

# zuul 配置
zuul:
  # 使⽤'*'可忽略所有微服务
  # 通过此配置使得只有通过 routes 配置的路由才生效,更安全
  ignored-services: '*'
  routes:
    # 配置 service-a 服务的 访问路径为 /service-a/**
    service-a: /service-a/**

现在来访问下 service-a

springcloud有哪些注解_springcloud有哪些注解_05


再来访问下 service-b

springcloud有哪些注解_网关_06


可以看到,由于我们忽略了所有的服务后,只配置了service-a的配置,因此service-a能请求到,但是service-b就不行了

自定义路由名配置

这种配置效果和上面横写是一样的,访问路径同样是 http://127.0.0.1:8080/service-a/xxx

# zuul 配置
zuul:
  ignored-services: '*'
  routes:
    # 给路由⼀个名称,可以任意起名
    # 通过服务名配置路由
    service-a:
      service-id: service-a # 指定服务名
      path: /service-a/** # 服务名对应的路径

直接通过 URL 配置(有缺陷)

我们也可以直接通过 URL 进行配置,以 service-b 为例

# zuul 配置
zuul:
  ignored-services: '*'
  routes:
    # 通过 URL 配置路由
    service-b:
      url: http://127.0.0.1:8081 # 指定的url
      path: /service-b/** # url对应的路径

springcloud有哪些注解_springcloud有哪些注解_07


PS:使用这种方式配置的路由不会作为 HystrixCommand 执行,同时也不能使用 Ribbon 来负载均衡多个URL

直接通过 URL 配置(无缺陷)

# zuul 配置
zuul:
  ignored-services: '*'
  routes:
    # 改为通过服务名去匹配
    service-b:
      service-id: service-b # 指定的服务名
      path: /service-b/** # url对应的路径

# 禁⽤掉ribbon的eureka使⽤
ribbon:
  eureka:
    # 详⻅:http://cloud.spring.io/spring-cloud-static/Camden.SR4/#_example_disable_eureka_use_in_ribbon
    enabled: false

# 把 service-b 这个服务名与 IP 进行映射
service-b:
  ribbon:
    listOfServers: localhost:8081,localhost:8082

路由前缀

请求路径:http://127.0.0.1:8080/api/service-a/1 实际请求路径:http://127.0.0.1:8080/service-a/api/1

这种配置估计应用场景应该是不同版本的接口切换吧,切换版本的时候,前端直接在统一配置里把api换掉,所有请求接口就切到新的版本上

# Zuul 配置
zuul:
  ignored-services: '*'
  # 路由前缀,把 /api/service-a/1 映射到 /service-a/api/1
  prefix: /api
  strip-prefix: false
  routes:
  	service-a:
      service-id: service-a # 指定服务名
      path: /service-a/** # 服务名对应的路径

可以看到请求正常

springcloud有哪些注解_spring cloud zuul_08


再来看下日志输出

springcloud有哪些注解_zuul_09


可以看到实际路径是http://127.0.0.1:8080/service-a/api/1

进阶配置

正则表达式指定Zuul的路由匹配规则

借助PatternServiceRouteMapper,实现从微服务到映射路由的正则配置
servicePattern 指定微服务的正则
routePattern 指定路由正则
正则怎么写,不会。。。

// zuul
@EnableZuulProxy
// Eureka 客户端
@EnableDiscoveryClient
// 由于没数据库,排除配置
@SpringBootApplication(exclude={DataSourceAutoConfiguration.class})
public class ZuulApplication {
    public static void main(String[] args) {
        SpringApplication.run(ZuulApplication.class, args);
    }

    // 正则表达式指定Zuul的路由匹配规则
    @Bean
    public PatternServiceRouteMapper serviceRouteMapper() {
        // 调⽤构造函数PatternServiceRouteMapper(String servicePattern, String routePattern)

        // servicePattern指定微服务的正则
        // routePattern指定路由正则
        // 最后一个 "-" 后面以 v 打头的为 version,之前的都是 name
        return new PatternServiceRouteMapper("(?<name>^.+)-(?<version>v.+$)", "${version}/${name}");
    }

}

自定义 Zuul 拦截器

zuul 除了帮助我们进行路由转发的功能外,还给我们提供了一个过滤器,我们可以通过继承 ZuulFilter 来实现 zuul 过滤器
我们可以通过这个过滤器来实现授权拦截,限流,日志记录等功能
filterType 指定过滤器的调用时机,方便我们在需要的时间段调用过滤器
filterOrder 指定过滤器执行的先后顺便,比如有两个 pre 的过滤器,就可以通过这个返回值控制两个过滤器的先后顺序
shouldFilter 指定过滤器是否使用
run 具体的过滤逻辑
这里我以一个简单的授权过滤器为例子

/**
 * Zuul 过滤器
 * 授权验证示例
 * @author: linjinp
 * @create: 2019-09-12 09:42
 **/
@Component
public class AuthFilter extends ZuulFilter {

    private static final Logger LOGGER = LoggerFactory.getLogger(AuthFilter. class);

    /**
     * 是否开启验证
     * 正常项目里,这种属性应该放配置文件里
     */
    private static final Boolean AUTH = Boolean.TRUE;

    /**
     * 指定过滤器的调用时机
     * pre: 路由之前,如实现认证,记录调试信息等
     * routing: 路由时
     * post: 路由后,比如添加HTTP header
     * error: 发生错误时调用
     *
     * @return
     */
    @Override
    public String filterType() {
        return FilterConstants.PRE_TYPE;
    }

    /**
     * 过滤器顺序
     * 比如有两个 pre 的过滤器,可以通过设置数字大小,控制两个过滤器执行先后
     *
     * @return
     */
    @Override
    public int filterOrder() {
        return 1;
    }

    /**
     * 判断是否启用该过滤
     *
     * @return
     */
    @Override
    public boolean shouldFilter() {
        return true;
    }

    /**
     * 过滤的具体逻辑
     *
     * @return
     * @throws ZuulException
     */
    @Override
    public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();

        // 启动验证
        if (AUTH) {
            String token = request.getHeader("Authorization");
            if (token == null) {
                LOGGER.info("该访问未进行授权");
                // 路由失败
                ctx.setSendZuulResponse(false);
                // 返回错误码
                ctx.setResponseStatusCode(401);
            } else {
                LOGGER.info("访问已授权");
                // 验证成功
                ctx.setSendZuulResponse(true);
                ctx.setResponseStatusCode(200);
            }
        } else {
            LOGGER.info("访问成功");
            // 没启用就直接成功
            ctx.setSendZuulResponse(true);
            ctx.setResponseStatusCode(200);
        }
        return null;
    }
}

看看日志打印

springcloud有哪些注解_zuul_10

禁用自定义拦截器

除了在拦截器里直接关闭外,我们也可以通过在配置文件里配置关闭拦截器

zuul.<SimpleClassName>.<filterType>.disable=true

比如我要关闭上面的 AuthFilter 拦截器

zuul:
  # AuthFilter 拦截器开关
  AuthFilter:
    pre:
      disable: true

容错与回退

当我们使用 zuul 时,难免会出现接口因为各种原因请求不通的情况,比如服务宕机了之类的
为了保证系统的健壮性,我们可以通过实现 FallbackProvider 方法,来自定义返回内容

  • Edgware版本写法:实现 FallbackProvider
  • Edgware之前版本:实现 ZuulFallbackProvider
/**
 * 容错与回退
 * @author: linjinp
 * @create: 2019-09-12 11:27
 **/
@Component
public class CommonFallbackProvider implements FallbackProvider {

    private static final Logger LOGGER = LoggerFactory.getLogger(AuthFilter. class);

    /**
     * 需要回退的微服务,"*" 表示所有
     *
     * @return
     */
    @Override
    public String getRoute() {
        return "*";
    }

    /**
     * 回退逻辑
     * @param route
     * @param cause
     * @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);
        }
    }

    /**
     * 自定义返回值内容
     * @param status
     * @return
     */
    private ClientHttpResponse response(final HttpStatus status) {
        return new ClientHttpResponse() {
            @Override
            public HttpStatus getStatusCode() throws IOException {
                return status;
            }

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

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

            @Override
            public void close() {
            }

            @Override
            public InputStream getBody() throws IOException {
                LOGGER.info("服务不可⽤,请稍后再试");
                return new ByteArrayInputStream("服务不可⽤,请稍后再试。".getBytes());
            }

            @Override
            public HttpHeaders getHeaders() {
                // headers设定
                HttpHeaders headers = new HttpHeaders();
                MediaType mt = new MediaType("application", "json", Charset.forName(
                        "UTF-8"));
                headers.setContentType(mt);
                return headers;
            }
        };

    }
}

现在我们把 service-a 停掉,然后请求试试,已模拟服务宕机

springcloud有哪些注解_拦截器_11