微服务专题12-Spring Cloud Gateway

前言

前面的章节我们讲了Spring Cloud 服务调用

本节,继续微服务专题的内容分享,共计16小节,分别是:

本节内容重点为:

  • 核心概念:介绍服务网关使用场景、服务能力、依赖关系、架构以及类型
  • Ribbon 整合
  • Hystrix 整合

Spring Cloud Gateway 简介

SpringCloud Gateway 是 Spring Cloud 的一个全新项目,该项目是基于 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。

Spring Cloud Gateway 类似于 Spring WebFlux。WebFlux的目的在于去 Servlet 化(Java EE Web 技术中心),WebFlux其技术核心为:Reactor + Netty + Lambda。

实际上,目前函数式编程、网络编程、Reactive这三点技术栈已成为主流:

  • 函数式编程:Java Lambda、Koltin、Scala、Groovy。

  • 网络编程:Old Java BIO、Java 1.4 NIO( Reactor 模式)、Java 1.7 NIO2 和 AIO、Netty。

  • Reactive:编程模型(非阻塞 + 异步) + 对象设计模式(观察者模式)。

透过这三个技术点我们也能看到典型的技术代表:

  • 单机版(函数式、并发编程)
    • Reactor
    • RxJava
    • Java 9 Flow API
  • 网络版(函数式、并发编程、网络编程)
    • Netty + Reactor :WebFlux、Spring Cloud Gateway
    • Vert.x (Netty)

Spring Cloud Gateway 的特性

取代 Zuul 1.x(基于 Servlet)

SpringCloud Gateway 作为 Spring Cloud 生态系统中的网关,目标是替代 Zuul,在Spring Cloud 2.0以上版本中,没有对新版本的Zuul 2.0以上最新高性能版本进行集成,仍然还是使用的Zuul 2.0之前的非Reactor模式的老版本。而为了提升网关的性能,SpringCloud Gateway是基于WebFlux框架实现的,而WebFlux框架底层则使用了高性能的Reactor模式通信框架Netty。

既然 Spring Cloud Gateway 的出现是为了取代Zuul 1.x,而Zuul 1.x则是基于Servlet实现,在Servlet之前则是 Resin Servlet 容器,据说可以与 Nginx 匹敌,继 Resin之后,则是 Tomcat Servlet 容器的时代,Tomcat里有一个核心概念是连接器,通常分为三种:

  • Java Blocking Connector
  • Java Non Blocking Connector
  • APR/native Connector

实际上Servlet容器还有很多,比如

  • JBoss
  • Weblogic
  • Netflix Zuul :缺点是实现 API 不是非常友好

服务发现

Spring Cloud Gateway 的目标,不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控/指标,和限流。

Spring Cloud Zuul

Zuul是Netflix开源的微服务网关,可以和Eureka、Ribbon、Hystrix等组件配合使用,Spring Cloud对Zuul进行了整合与增强,Zuul默认使用的HTTP客户端是Apache HTTPClient,也可以使用RestClient或okhttp3.OkHttpClient。

Spring Cloud Zuul 简单流程

1、 @Enable 模块装配

  • @EnableZuulProxy
    在这里插入图片描述

  • 配合注解:@Import
    在这里插入图片描述

2、 依赖服务发现

  • 我是谁
  • 目的服务在哪里

3、 依赖服务路由

  • URI 映射到目的服务

4、 依赖服务熔断(可选)

举例说明

假设路由的 URI : /gateway/spring-cloud-server-application/say,其中 Servlet Path是/gateway,而spring-cloud-server-application 是服务的应用名称,/sayspring-cloud-server-application 的服务 URI。怎么实现网关呢?

新建spring-cloud-servlet-gateway项目。

1、配置依赖

这里忽略,详情见本文末的完整演示代码github链接

2、配置文件

# 当前应用名称
spring.application.name = spring-cloud-servlet-gateway
# Servlet Gateway 服务端口
server.port = 20000

3、编写服务网关的路由规则测试类:

/**
 * 服务网关的路由规则
 * /{service-name}/{service-uri}
 * /gateway/rest-api/hello-world-> http://127.0.0.1:8080/hello-world
 */
@WebServlet(name = "gateway", urlPatterns = "/gateway/*")
public class GatewayServlet extends HttpServlet {

    @Autowired
    private DiscoveryClient discoveryClient;

    private ServiceInstance randomChoose(String serviceName) {
        // 获取服务实例列表(服务IP、端口、是否为HTTPS)
        List<ServiceInstance> serviceInstances = discoveryClient.getInstances(serviceName);
        // 获得服务实例总数
        int size = serviceInstances.size();
        // 随机获取数组下标
        int index = new Random().nextInt(size);
        return serviceInstances.get(index);
    }

    @Override
    public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // ${service-name}/${service-uri}
        String pathInfo = request.getPathInfo();
        String[] parts = StringUtils.split(pathInfo.substring(1), "/");
        // 获取服务名称
        String serviceName = parts[0];
        // 获取服务 URI
        String serviceURI = "/" + parts[1];
        // 随机选择一台服务实例
        ServiceInstance serviceInstance = randomChoose(serviceName);
        // 构建目标服务 URL -> scheme://ip:port/serviceURI
        String targetURL = buildTargetURL(serviceInstance, serviceURI, request);

        // 创建转发客户端
        RestTemplate restTemplate = new RestTemplate();

        // 构造 Request 实体
        RequestEntity<byte[]> requestEntity = null;
        try {
            requestEntity = createRequestEntity(request, targetURL);
            ResponseEntity<byte[]> responseEntity = restTemplate.exchange(requestEntity, byte[].class);
            writeHeaders(responseEntity, response);
            writeBody(responseEntity, response);
        } catch (URISyntaxException e) {
            e.printStackTrace();
        }
    }

    private String buildTargetURL(ServiceInstance serviceInstance, String serviceURI, HttpServletRequest request) {
        StringBuilder urlBuilder = new StringBuilder();
        urlBuilder.append(serviceInstance.isSecure() ? "https://" : "http://")
                .append(serviceInstance.getHost()).append(":").append(serviceInstance.getPort())
                .append(serviceURI);
        String queryString = request.getQueryString();
        if (StringUtils.hasText(queryString)) {
            urlBuilder.append("?").append(queryString);
        }
        return urlBuilder.toString();
    }

    private RequestEntity<byte[]> createRequestEntity(HttpServletRequest request, String url) throws URISyntaxException, IOException {
        // 获取当前请求方法
        String method = request.getMethod();
        // 装换 HttpMethod
        HttpMethod httpMethod = HttpMethod.resolve(method);
        byte[] body = createRequestBody(request);
        MultiValueMap<String, String> headers = createRequestHeaders(request);
        RequestEntity<byte[]> requestEntity = new RequestEntity<byte[]>(body, headers, httpMethod, new URI(url));
        return requestEntity;
    }

    private MultiValueMap<String, String> createRequestHeaders(HttpServletRequest request) {
        HttpHeaders headers = new HttpHeaders();
        List<String> headerNames = Collections.list(request.getHeaderNames());
        for (String headerName : headerNames) {
            List<String> headerValues = Collections.list(request.getHeaders(headerName));
            for (String headerValue : headerValues) {
                headers.add(headerName, headerValue);
            }
        }
        return headers;
    }

    private byte[] createRequestBody(HttpServletRequest request) throws IOException {
        InputStream inputStream = request.getInputStream();
        return StreamUtils.copyToByteArray(inputStream);
    }


    /**
     * 输出 Body 部分
     *
     * @param responseEntity
     * @param response
     * @throws IOException
     */
    private void writeBody(ResponseEntity<byte[]> responseEntity, HttpServletResponse response) throws IOException {
        if (responseEntity.hasBody()) {
            byte[] body = responseEntity.getBody();
            // 输出二进值
            ServletOutputStream outputStream = response.getOutputStream();
            // 输出 ServletOutputStream
            outputStream.write(body);
            outputStream.flush();
        }
    }

    private void writeHeaders(ResponseEntity<byte[]> responseEntity, HttpServletResponse response) {
        // 获取相应头
        HttpHeaders httpHeaders = responseEntity.getHeaders();
        // 输出转发 Response 头
        for (Map.Entry<String, List<String>> entry : httpHeaders.entrySet()) {
            String headerName = entry.getKey();
            List<String> headerValues = entry.getValue();
            for (String headerValue : headerValues) {
                response.addHeader(headerName, headerValue);
            }
        }
    }

}

4、启动zk服务

我的zk服务地址,localhost:2181

在这里插入图片描述
5、分别启动客户端与服务端和gateway服务
访问地址:
http://localhost:20000/gateway/spring-cloud-server-application/say?message=world

6、结果输出

在这里插入图片描述

整合负载均衡(Ribbon)

实现 ILoadBalancer
实现 IRule

1、通过继承BaseLoadBalancer实现负载均衡:

public class ZookeeperLoadBalancer extends BaseLoadBalancer {

    @Value("${spring.application.name}")
    private String currentApplicationName = "spring-cloud-servlet-gateway";

    private final DiscoveryClient discoveryClient;

    private Map<Stri ng, BaseLoadBalancer> loadBalancerMap = new ConcurrentHashMap<>();

    public ZookeeperLoadBalancer(DiscoveryClient discoveryClient) {
        this.discoveryClient = discoveryClient;
        //
        updateServers();
    }

    @Override
    public Server chooseServer(Object key) {
        if (key instanceof String) {
            String serviceName = String.valueOf(key);
            BaseLoadBalancer baseLoadBalancer = loadBalancerMap.get(serviceName);
            return baseLoadBalancer.chooseServer(serviceName);
        }
        return super.chooseServer(key);
    }

    /**
     * 更新所有服务器
     */
    @Scheduled(fixedRate = 5000)
    public void updateServers() {
        discoveryClient.getServices().stream().forEach(serviceName -> {

            BaseLoadBalancer loadBalancer = new BaseLoadBalancer();

            loadBalancerMap.put(serviceName, loadBalancer);
            List<ServiceInstance> serviceInstances = discoveryClient.getInstances(serviceName);
            serviceInstances.forEach(serviceInstance -> {
                Server server = new Server(serviceInstance.isSecure() ? "https://" : "http://",
                        serviceInstance.getHost(), serviceInstance.getPort());
                loadBalancer.addServer(server);
            });
        });
    }
}

2、基于负载均衡策略的网关实现:

/**
 * 服务网关的路由规则
 * /{service-name}/{service-uri}
 * /gateway/rest-api/hello-world-> http://127.0.0.1:8080/hello-world
 */
@WebServlet(name = "ribbonGateway", urlPatterns = "/ribbon/gateway/*")
public class RibbonGatewayServlet extends HttpServlet {

    @Autowired
    private ZookeeperLoadBalancer zookeeperLoadBalancer;

    private Server chooseServer(String serviceName) {
        Server server = zookeeperLoadBalancer.chooseServer(serviceName);
        return server;
    }

    @Override
    public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // ${service-name}/${service-uri}
        String pathInfo = request.getPathInfo();
        String[] parts = StringUtils.split(pathInfo.substring(1), "/");
        // 获取服务名称
        String serviceName = parts[0];
        // 获取服务 URI
        String serviceURI = "/" + parts[1];
        // 随机选择一台服务实例
        Server server = chooseServer(serviceName);
        // 构建目标服务 URL -> scheme://ip:port/serviceURI
        String targetURL = buildTargetURL(server, serviceURI, request);

        // 创建转发客户端
        RestTemplate restTemplate = new RestTemplate();

        // 构造 Request 实体
        RequestEntity<byte[]> requestEntity = null;
        try {
            requestEntity = createRequestEntity(request, targetURL);
            ResponseEntity<byte[]> responseEntity = restTemplate.exchange(requestEntity, byte[].class);
            writeHeaders(responseEntity, response);
            writeBody(responseEntity, response);
        } catch (URISyntaxException e) {
            e.printStackTrace();
        }
    }

    private String buildTargetURL(Server server, String serviceURI, HttpServletRequest request) {
        StringBuilder urlBuilder = new StringBuilder();
        urlBuilder.append(server.getScheme())
                .append(server.getHost()).append(":").append(server.getPort())
                .append(serviceURI);
        String queryString = request.getQueryString();
        if (StringUtils.hasText(queryString)) {
            urlBuilder.append("?").append(queryString);
        }
        return urlBuilder.toString();
    }

    private RequestEntity<byte[]> createRequestEntity(HttpServletRequest request, String url) throws URISyntaxException, IOException {
        // 获取当前请求方法
        String method = request.getMethod();
        // 装换 HttpMethod
        HttpMethod httpMethod = HttpMethod.resolve(method);
        byte[] body = createRequestBody(request);
        MultiValueMap<String, String> headers = createRequestHeaders(request);
        RequestEntity<byte[]> requestEntity = new RequestEntity<byte[]>(body, headers, httpMethod, new URI(url));
        return requestEntity;
    }

    private MultiValueMap<String, String> createRequestHeaders(HttpServletRequest request) {
        HttpHeaders headers = new HttpHeaders();
        List<String> headerNames = Collections.list(request.getHeaderNames());
        for (String headerName : headerNames) {
            List<String> headerValues = Collections.list(request.getHeaders(headerName));
            for (String headerValue : headerValues) {
                headers.add(headerName, headerValue);
            }
        }
        return headers;
    }

    private byte[] createRequestBody(HttpServletRequest request) throws IOException {
        InputStream inputStream = request.getInputStream();
        return StreamUtils.copyToByteArray(inputStream);
    }


    /**
     * 输出 Body 部分
     *
     * @param responseEntity
     * @param response
     * @throws IOException
     */
    private void writeBody(ResponseEntity<byte[]> responseEntity, HttpServletResponse response) throws IOException {
        if (responseEntity.hasBody()) {
            byte[] body = responseEntity.getBody();
            // 输出二进值
            ServletOutputStream outputStream = response.getOutputStream();
            // 输出 ServletOutputStream
            outputStream.write(body);
            outputStream.flush();
        }
    }

    private void writeHeaders(ResponseEntity<byte[]> responseEntity, HttpServletResponse response) {
        // 获取相应头
        HttpHeaders httpHeaders = responseEntity.getHeaders();
        // 输出转发 Response 头
        for (Map.Entry<String, List<String>> entry : httpHeaders.entrySet()) {
            String headerName = entry.getKey();
            List<String> headerValues = entry.getValue();
            for (String headerValue : headerValues) {
                response.addHeader(headerName, headerValue);
            }
        }
    }

}

3、访问地址:
http://localhost:20000/ribbon/gateway/spring-cloud-client-application/rest/say?message=world

4、结果输出

在这里插入图片描述

后记

本节代码地址:Spring Cloud Gateway

更多架构知识,欢迎关注本套Java系列文章Java架构师成长之路

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值