SpringCloud踩坑记(七)Spring Cloud路由网关

前言

路由是微服务架构不可或缺的一部分。例如,/可能被映射到您的Web应用程序,
/api/users被映射到用户服务以及/api/shop被映射到商店服务。

生产环境中我们经常会使用Nginx进行来做路由转发,但是Spring Cloud本身已经有集成
zuul和getaway组件来提供动态路由。现在我们就来学习如何使用这俩个组件和了解其实现机制。

zuul

Zuul是一项网关服务,可提供动态路由,监视,弹性,安全性等。

使用

创建gateway工程并创建注册中心Server子模块、服务提供Producer子模块,
本文之前我们学习如何编写配置中心服务和客户端,这边就不重复讲解。
初步工程目录如下:

- gateway
 - Producer
 - Server

在服务提供Producer子模块增加测试接口MessageController,内容如下

@RestController
public class MessageController {

    @Value("${server.port}")
    String port;

    @GetMapping("/get")
    public String getMessage(@RequestParam("name")String name){
        return "Hi " + name + " ,I am from port:" + port;
    }
}

提供俩个接口,一个带版本号访问,一个不带版本号。

启动注册中心和服务提供Producer子模块,并且修改Producer子模块
使用的端口8001为8002,然后再次启动一个服务提供Producer子模块。

IDEA允许重复启动配置,界面选择Edit Configuration->
选择ProducerApplication->勾选Allow parallel run

创建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">
    <parent>
        <artifactId>gateway</artifactId>
        <groupId>com.smallstep</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>Zuul</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</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-zuul</artifactId>
        </dependency>
    </dependencies>
</project>

编写zuul子模块的启动类,内容如下:

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

增加@EnableZuulProxy注解进行启用zuul路由代理

编写application配置文件,配置内容如下:

server:
  port: 8600

spring:
  application:
    name: zuul

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:9000/eureka/

zuul:
  routes:
    producer:
      path: /proxy/**
      serviceId: producer

其中定义了一个路由匹配路径为/proxy/**(这边设置匹配多级目录。如果想匹配一级目录可修改为/proxy/*),
并且对应调用的服务为producer。

启动zuul子模块,访问http://127.0.0.1:8600/proxy/get?name=IT_LIGe,界面会出现循环出现
如下内容:

Hi IT_LIGe ,I am from port:8001
或
Hi IT_LIGe ,I am from port:8002

原因是zuul默认会去调用ribbon进行客户端负载均衡去消费服务。如果需要指定服务,可以
修改application的配置内的serviceId并添加path为参数,例如:

zuul:
  routes:
    producer:
      path: /proxy/**
      url: http://127.0.0.1:8001

这样会默认去调用8001端口的服务。如果要增加统一前缀可配置zuul.prefix参数。

熔断器

当Zuul中给定路线的电路跳闸时,可以通过创建type的bean提供后备响应FallbackProvider。
在此Bean中,您需要指定回退的路由ID,并提供一个ClientHttpResponse作为回退的返回。
以下示例显示了一个相对简单的FallbackProvider实现:

@Component
class MyFallbackProvider implements FallbackProvider {

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

    @Override
    public ClientHttpResponse fallbackResponse(String route, final 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;
            }

            @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 {
                return new ByteArrayInputStream("fallback".getBytes());
            }

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

关闭俩个提供服务子模块。再次访问http://127.0.0.1:8600/proxy/get?name=IT_LIGe,界面出现如下

fallback

说明使用了自定义熔断器。如果您想为所有路由提供默认的后备,则可以自定义FallbackProvider的getRoute()return *或 return null。

源码分析

启动类增加了@EnableZuulProxy即实现开启了路由代理,那我们看看EnableZuulProxy的实现。实现
内容如下:

@EnableCircuitBreaker
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Import({ZuulProxyMarkerConfiguration.class})
public @interface EnableZuulProxy {
}

可以发现了加了@EnableCircuitBreaker 启用了熔断器,并且Import了ZuulProxyMarkerConfiguration。
ZuulProxyMarkerConfiguration注入了Marker类实例。

我们再看看spring-cloud-netflix-zuul-2.1.4.RELEASE.jar内的
spring.factories,内容如下:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.netflix.zuul.ZuulServerAutoConfiguration,\
org.springframework.cloud.netflix.zuul.ZuulProxyAutoConfiguration

根据内容我们可以知道这个设置了俩个自动加载配置类。

我们观察ZuulServerAutoConfiguration和ZuulProxyAutoConfiguration类头部,可以看到@ConditionalOnBean({Marker.class}),因此
只有实例化了Marker类这俩个配置类才有效,通过上文我们知道,增加@EnableZuulProxy
会注入Marker实例,因此只有增加了@EnableZuulProxy这俩个配置类才能生效。

ZuulProxyAutoConfiguration类内主要配置了使用何种Client转发请求,这边不扩展讲。
我们主要查看ZuulServerAutoConfiguration类,可以看到默认注入了
ZuulHandlerMapping的Mapping映射,然后最后交给MVC容器,
具体ZuulHandlerMapping代码如下:

@Bean
    public ZuulHandlerMapping zuulHandlerMapping(RouteLocator routes) {
        ZuulHandlerMapping mapping = new ZuulHandlerMapping(routes, this.zuulController());
        mapping.setErrorController(this.errorController);
        mapping.setCorsConfigurations(this.getCorsConfigurations());
        return mapping;
    }

其中routes为我们在appliaction配置的相关配置路由,默认实现SimpleRouteLocator。
SimpleRouteLocator注入代码如下:

@Bean
    @ConditionalOnMissingBean({SimpleRouteLocator.class})
    public SimpleRouteLocator simpleRouteLocator() {
        return new SimpleRouteLocator(this.server.getServlet().getContextPath(), this.zuulProperties);
    }

this.zuulProperties为我们配置的内容。

再回过来看看this.zuulController的实现类为:

public class ZuulController extends ServletWrappingController {
    public ZuulController() {
        this.setServletClass(ZuulServlet.class);
        this.setServletName("zuul");
        this.setSupportedMethods((String[])null);
    }

    public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
        ModelAndView var3;
        try {
            var3 = super.handleRequestInternal(request, response);
        } finally {
            RequestContext.getCurrentContext().unset();
        }

        return var3;
    }
}

查看super.handleRequestInternal最终调用this.servletInstance.service(request, response);
因此最终调用的是ZuulServlet内的service方法。

我们再来看看ZuulServle实现内容:

public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        try {
            this.init((HttpServletRequest)servletRequest, (HttpServletResponse)servletResponse);
            RequestContext context = RequestContext.getCurrentContext();
            context.setZuulEngineRan();

            try {
                this.preRoute();
            } catch (ZuulException var12) {
                this.error(var12);
                this.postRoute();
                return;
            }

            try {
                this.route();
            } catch (ZuulException var13) {
                this.error(var13);
                this.postRoute();
                return;
            }

            try {
                this.postRoute();
            } catch (ZuulException var11) {
                this.error(var11);
            }
        } catch (Throwable var14) {
            this.error(new ZuulException(var14, 500, "UNHANDLED_EXCEPTION_" + var14.getClass().getName()));
        } finally {
            RequestContext.getCurrentContext().unset();
        }
    }

我们可以发现其先调用了preRoute()前置路由过滤如果异常调用错误过滤器error()路由postRoute()后置路由事件。
如果没有错误直接调用route()路由事件。

preRoute、error、route、postRoute最终是调用了不同的类型的ZuulFilter
类的实例。

zuul已经内置了不同了ZuulFilter实现类,如果我们想自定义Filter只要实现ZuulFilter
如何注入到Spring容器内即可。

看到这个我们可以知道zuul其实就最终ZuulServlet通过一系列的ZuulFilter链条调用实现。

处理过程大致如下图:

zuul-001

Spring Cloud Gateway

该项目提供了一个用于在Spring MVC之上构建API网关的库。Spring Cloud Gateway旨在提供一种简单而有效的方法来路由到API,并为它们提供跨领域的关注,例如:安全性,监视/指标和弹性。

工作原理

gateway-001

客户端向Spring Cloud Gateway发出请求。如果网关处理程序映射确定请求与路由匹配,
则将其发送到网关Web处理程序。
该处理程序通过特定于请求的过滤器链来运行请求。
筛选器由虚线分隔的原因是,筛选器可以在发送代理请求之前和之后运行逻辑。
所有前置过滤器逻辑均被执行。然后发出代理请求。发出代理请求后,将运行后置过滤器逻辑。

编写

创建Gateway子模块。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">
    <parent>
        <artifactId>gateway</artifactId>
        <groupId>com.smallstep</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>Gateway</artifactId>

    <dependencies>
        <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-gateway</artifactId>
        </dependency>
    </dependencies>
</project>

注意:此处不需要引入spring-boot-starter-web相关依赖,不然会冲突。

编写工程启动类GatewayApplication,代码如下:

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

配置application.yml文件,内容如下:

server:
  port: 8700

spring:
  application:
    name: gateway
  cloud:
    gateway:
      routes:
        - id: producer
          uri: lb://producer
          predicates:
            - Path=/proxy/**
          filters:
            - RewritePath=/proxy/(?<segment>.*), /${segment}


eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:9000/eureka/

其中配置说明如下:

  • routes:为设置的路由列表。它由ID,目标URI,断言集合和过滤器集合定义。如果断言工程返回为true,则匹配路由。

  • predicates:为断言集合,可以匹配HTTP请求中的所有内容,例如http请求头部或参数等。

  • filters: 为过滤器集合,spring cloud gateway本身提供了很多内置过滤器。通过这些过滤器可以在发送请求之前或之后修改请求和响应相关内容。

该配置内容是添加了一个匹配/proxy/**路径的路由,并且通过LoadBalancerClient从
注册中心获取最终请求的服务。并且通过filters重新改变请求地址去掉/proxy。

启动服务然后访问http://127.0.0.1:8700/proxy/get?name=IT_LiGe,界面显示如下

Hi IT_LiGe ,I am from port:8001

可以发现最终调用了注册中心中serviceId为producer的服务/get?name=IT_LiGe。

在高并发情况下,我们为了保证服务高可用,我们会增加熔断器和限流功能。
spring cloud gateway本身也提供了相关的处理方法。这边就介绍下熔断器使用。

pom.xml增加熔断器相关依赖:

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

然后修改application.yml增加Hystrix过滤器,application.yml内容如下:

server:
  port: 8700

spring:
  application:
    name: gateway
  cloud:
    gateway:
      routes:
        - id: producer
          uri: lb://producer
          predicates:
            - Path=/proxy/**
          filters:
            - RewritePath=/proxy/(?<segment>.*), /${segment}
            - name: Hystrix
              args:
                name: fallbackcmd
                fallbackUri: forward:/fallback


eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:9000/eureka/

其中如果失败跳转到/fallback接口,所以我们再编写FallbackController,内容
如下:

@RestController
public class FallbackController {
    @GetMapping("/fallback")
    public String fallback(@RequestParam String name){
        return name + ",fallback";
    }
}

重启gateway工程,然后关闭producer服务工程。再次访问
http://127.0.0.1:8700/proxy/get?name=IT_LiGe,界面返回内容如下:

IT_LiGe,fallback

说明熔断器已经生效。

以上内容是通过配置文件进行配置,也可以用过代码进行配置,等效代码如下:

//@Configuration
public class GatewayConfig {

    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
        return builder.routes()
                .route("producer", r -> r.path("/proxy/**")
                        .filters(f -> f.rewritePath("/proxy/(?<segment>.*)",
                                "/${segment}")
                                .hystrix(c -> c.setFallbackUri("forward:/fallback"))
                        )
                        .uri("lb://producer"))
                .build();
    }
}

如何选择?

Zuul是在servlet 2.5(与3.x一起工作)上使用阻塞api构建的。它不支持任何长时间的连接,比如websockets。
虽然Zuul已经发布了Zuul 2.x,基于Netty,也是非阻塞的,支持长连接,但Spring Cloud暂时还没有整合计划。

Gateway是使用非阻塞api在Spring Framework 5、Project Reactor和Spring Boot 2上构建的。
Websockets是受支持的,它是一个更好的开发体验,因为它与Spring紧密集成。

所以如果是小型项目并无需无长连接支持,俩者都行。如果中大型项目建议选择Gateway,因为
配置更简单方便,支持功能更加完善,并且毕竟是Spring Cloud 亲儿子,不会始乱终弃,停止维护。后续扩展维护
等方面更有保障

附录

源代码:https://gitee.com/LeeJunProject/spring_cloud_learning/tree/master/gateway

END

欢迎扫描下图关注公众号 IT李哥,公众号经常推送一些优质的技术文章

在这里插入图片描述

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值