Spring Cloud Netflix 之zuul实现动态路由

590 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第22天,点击查看活动详情

前言

ZuulSpring Cloud Netflix开源的应用层网关。Zuul网关的核心是一系列的过滤器,这些过滤器可以对请求或者响应结果做一系列过滤,Zuul提供了一个框架可以支持动态加载,编译,运行这些过滤器,这些过滤器是使用责任链方式顺序对请求或者响应结果进行处理的,这些过滤器不会直接进行通信,但是通过责任链传递的RequestContext参数可以共享一些东西。

在zuul中过滤器分为四种:

  • PRE Filters(前置过滤器) :当请求会路由转发到具体后端服务器前执行的过滤器,比如鉴权过滤器,日志过滤器,还有路由选择过滤器

  • ROUTING Filters (路由过滤器):该过滤器作用是把请求具体转发到后端服务器上,一般是通过Apache HttpClient 或者 Netflix Ribbon把请求发送到具体的后端服务器上

  • POST Filters(后置过滤器):当把请求路由到具体后端服务器后执行的过滤器;场景有添加标准http 响应头,收集一些统计数据(比如请求耗时等),写入请求结果到请求方等。

  • ERROR Filters(错误过滤器):当上面任何一个类型过滤器执行出错时候执行该过滤器

zuul的作用服务的灰度发布服务跨域服务限流负载均衡和降级动态路由

请求流程如下图:

image.png

下面将介绍如何在项目中通过数据库配置实现动态路由。

zuul动态路由

Java是一门面向对象的编程语言,一切皆对象,zuul网关存储的注册中心中的服务实例也是一种对象,这个对象叫rote。rote可以通过数据库+自定义RouteLocator,也可以通过zuul的配置文件+config(apollo\zk)bus配置中心+bus动态刷新配置文件完成。

原理就是通过将zuul的rote对象映射到实际的数据库表当中,在zuul服务中添加定时器,定时读取数据库路由表信息,生成路由对象放在应用内存的路由表中。

动态路由原理如下图:

image.png

创建数据库

创建数据库的路由配置表。

CREATE TABLE `gateway_api_route` (
	`id` VARCHAR ( 50 ) NOT NULL,
	`path` VARCHAR ( 255 ) NOT NULL,
	`service_id` VARCHAR ( 50 ) DEFAULT NULL,
	`url` VARCHAR ( 255 ) DEFAULT NULL,
	`retryable` TINYINT ( 1 ) DEFAULT NULL,
	`enabled` TINYINT ( 1 ) NOT NULL,
	`strip_prefix` INT ( 11 ) DEFAULT NULL,
	`api_name` VARCHAR ( 255 ) DEFAULT NULL,
	PRIMARY KEY ( `id` ) 
) ENGINE = INNODB DEFAULT CHARSET = utf8;

INSERT INTO gateway_api_route ( id, path, service_id, retryable, strip_prefix, url, enabled ) VALUES ( 'order-service', '/order/**', 'order-service', 0, 1, NULL, 1 );

引入依赖

<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-netflix-zuul</artifactId>
        <exclusions>
            <exclusion>
                <artifactId>guava</artifactId>
                <groupId>com.google.guava</groupId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <artifactId>guava</artifactId>
        <groupId>com.google.guava</groupId>
        <version>16.0.1</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
    <!-- druid连接池 -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.1.10</version>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.75</version>
    </dependency>
    <dependency>
        <groupId>commons-httpclient</groupId>
        <artifactId>commons-httpclient</artifactId>
        <version>3.1</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</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>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Hoxton.SR3</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

配置数据库链接

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://192.168.56.101:3306/vip?useUnicode=true&characterEncoding=UTF-8
    username: root
    password: root

创建数据库实体类

import java.io.Serializable;

/**
 * @ClassName GatewayApiRouteDO
 * @Description
 * 网关API动态路由配置表
 * 
 * org.springframework.cloud.netflix.zuul.filters.ZuulProperties.ZuulRoute
 * @Author lantianbaiyun
 * @Date 2021/4/6
 * @Version 1.0
 */
public class GatewayApiRouteDO implements Serializable {

    private String id;
    private String path;
    private String serviceId;
    private String url;
    private boolean stripPrefix = true;
    private Boolean retryable;
    // MYSQL数据库字段类型设置为tinyint(1),值为1=true,0=false sql语句where判断写成:
    // where enabled = true 或者写成 enabled = 1都能查询到
    private Boolean enabled;
}
```

字段一定不要修改,参照ZuulRoute进行编写,动态路由的时候通过BeanUtils.copyProperties()方法进行复制即可。

修改配置文件

删除配置文件application.yml中的配置,也可以保留,保留后也可以在数据库配置,但是数据库配置就不生效了,但是不会因为配置文件和数据库都有而重复。

#zuul:
#  routes:
#    vipProducer:
#      path: /vipProducer/**
#      serviceId: vip-producer
#    vipConsumer:
#      path: /vipConsumer/**
#      serviceId: vip-consumer

自定义RouteLocator类

import com.xinxin.vip.sso.model.GatewayApiRouteDO;
import org.springframework.beans.BeanUtils;
import org.springframework.cloud.netflix.zuul.filters.RefreshableRouteLocator;
import org.springframework.cloud.netflix.zuul.filters.SimpleRouteLocator;
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.util.StringUtils;

import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * @ClassName DynamicRouteLocator
 * @Description 自定义路由管理器
 * @Author lantianbaiyun
 * @Date 2021/4/6
 * @Version 1.0
 */
public class DynamicRouteLocator extends SimpleRouteLocator implements RefreshableRouteLocator {
    private JdbcTemplate jdbcTemplate;
    private ZuulProperties properties;

    public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    public DynamicRouteLocator(String servletPath, ZuulProperties properties) {
        super(servletPath, properties);
        this.properties = properties;
    }

    @Override
    public void refresh() {
        doRefresh();
    }

    @Override
    protected Map<String, ZuulProperties.ZuulRoute> locateRoutes() {
        LinkedHashMap<String, ZuulProperties.ZuulRoute> routesMap = new LinkedHashMap<>();
        // 加载配置文件application.yml中的路由表
        routesMap.putAll(super.locateRoutes());
        // 加载db中的路由表,也可以配置在redis\zk等任意可以读取的地方
        routesMap.putAll(locateRoutesFromDB());

        // 统一处理一下路由path格式
        LinkedHashMap<String, ZuulProperties.ZuulRoute> values = new LinkedHashMap<>();
        for (Map.Entry<String, ZuulProperties.ZuulRoute> entry : routesMap.entrySet()) {
            String path = entry.getKey();
            if (!path.startsWith("/")) {
                path = "/" + path;
            }
            if (StringUtils.hasText(this.properties.getPrefix())) {
                path = this.properties.getPrefix() + path;
                if (!path.startsWith("/")) {
                    path = "/" + path;
                }
            }
            values.put(path, entry.getValue());
        }
        return values;
    }

    /**
     * 从数据库中读取配置的路由表
     *
     * @return
     */
    private Map<String, ? extends ZuulProperties.ZuulRoute> locateRoutesFromDB() {
        Map<String, ZuulProperties.ZuulRoute> routes = new HashMap<>();

        List<GatewayApiRouteDO> results = jdbcTemplate.query(
                // MYSQL数据库字段类型设置为tinyint(1),值为1=true,0=false sql语句where判断写成:
                // where enabled = true 或者写成 enabled = 1都能查询到
                "select * from gateway_api_route where enabled = true",
                new BeanPropertyRowMapper<>(GatewayApiRouteDO.class));
        for (GatewayApiRouteDO result : results) {
            if (StringUtils.isEmpty(result.getPath())) {
                continue;
            }

            if (StringUtils.isEmpty(result.getServiceId())
                    && StringUtils.isEmpty(result.getUrl())) {
                continue;
            }

            ZuulProperties.ZuulRoute zuulRoute = new ZuulProperties.ZuulRoute();
            BeanUtils.copyProperties(result, zuulRoute);

            routes.put(zuulRoute.getPath(), zuulRoute);
        }

        return routes;
    }
}

RouteLocator配置类

import com.xinxin.vip.sso.locator.DynamicRouteLocator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
/**
 * @ClassName DynamicRouteConfiguration
 * @Description 自定义路由 Spring 配置类
 * @Author lantianbaiyun
 * @Date 2021/4/6
 * @Version 1.0
 */
@Configuration
public class DynamicRouteConfiguration {

    @Autowired
    private ZuulProperties zuulProperties;
    @Autowired
    private ServerProperties server;
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Bean
    public DynamicRouteLocator routeLocator() {
        // spring boot 1.5.x spring cloud Edgware.SR3写法
//        DynamicRouteLocator routeLocator = new DynamicRouteLocator(
//                this.server.getServletPrefix(), this.zuulProperties);
        // spring boot 2.2.x spring cloud Hoxton.SR3写法
        DynamicRouteLocator routeLocator = new DynamicRouteLocator(
                this.server.getServlet().getContextPath(), this.zuulProperties);
        routeLocator.setJdbcTemplate(jdbcTemplate);
        return routeLocator;
    }

}

创建定时器

package com.xinxin.vip.sso.task;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.netflix.zuul.RoutesRefreshedEvent;
import org.springframework.cloud.netflix.zuul.filters.RouteLocator;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

/**
 * @ClassName RefreshRouteTask
 * @Description 定时从数据库刷新最新的路由信息
 * @Author lantianbaiyun
 * @Date 2021/4/6
 * @Version 1.0
 */
@Component
@Configuration
@EnableScheduling
public class RefreshRouteTask {
    private static final Logger LOGGER = LoggerFactory.getLogger(RefreshRouteTask.class);

    @Autowired
    private ApplicationEventPublisher publisher;
    @Autowired
    private RouteLocator routeLocator;

    @Scheduled(fixedRate = 5000)
    private void refreshRoute() {
        LOGGER.info("定时刷新路由表==>{}", routeLocator.getRoutes());
        RoutesRefreshedEvent routesRefreshedEvent = new RoutesRefreshedEvent(routeLocator);
        publisher.publishEvent(routeLocator);
    }
}