Spring Cloud:网关zuul实现动态路由

533 阅读4分钟

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

Zuul是Netflix提供的一个开源组件,提供动态路由、监控、弹性、安全等边缘服务的框架。也有很多公司使用它来作为网关的重要组成部分。Zuul包含了对请求的路由过滤两个最主要的功能,外加代理功能。路由功能负责将外部请求转发到具体的微服务实例上,是实现外部访问统一入口的基础,过滤器功能则负责对请求的处理过程进行干预,是实现请求校验,服务聚合等功能的基础。

动态路由需要达到可持久化配置,动态刷新的效果。不仅要能满足从spring的配置文件properties加载路由信息,还需要从数据库加载我们的配置。另外一点是,路由信息在容器启动时就已经加载进入了内存,我们希望配置完成后,实施发布,动态刷新内存中的路由信息,达到不停机维护路由信息的效果。下面将进行动态路由的实战。

zuul实现动态路由

环境说明

spring boot 2.2.11\spring cloud Hoxton.SR3

数据库配置

创建数据库的路由配置表,该表用来保存微服务的路由信息,表结构是根据zuul的ZuulRoute类进行定义的,zuul启动的时候除了从配置文件读取路由信息外,也从DB中读取路由路由信息,生成router对象。

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);
INSERT INTO gateway_api_route (id, path, service_id, retryable, strip_prefix, url, enabled) VALUES ('vipProducer', '/vipProducer/**', 'vip-producer',0,1, NULL, 1);
INSERT INTO gateway_api_route (id, path, service_id, retryable, strip_prefix, url, enabled) VALUES ('vipConsumer', '/vipConsumer/**', 'vip-consumer',0,1, NULL, 1);

# MYSQL数据库字段类型设置为tinyint(1),值为1=true,0=false sql语句where判断写成:
# where enabled = true 或者写成 enabled = 1都能查询到
select * from gateway_api_route where enabled = true;

引入依赖

<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

创建数据库实体类

package com.xinxin.vip.sso.model;

import java.io.Serializable;

/**
 * @ClassName GatewayApiRouteDO
 * @Description
 * 网关API动态路由配置表
 * 字段一定不要修改,参照ZuulRoute进行编写,动态路由的时候通过BeanUtils.copyProperties()方法进行复制即可
 * 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;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getPath() {
        return path;
    }

    public void setPath(String path) {
        this.path = path;
    }

    public String getServiceId() {
        return serviceId;
    }

    public void setServiceId(String serviceId) {
        this.serviceId = serviceId;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public boolean isStripPrefix() {
        return stripPrefix;
    }

    public void setStripPrefix(boolean stripPrefix) {
        this.stripPrefix = stripPrefix;
    }

    public Boolean getRetryable() {
        return retryable;
    }

    public void setRetryable(Boolean retryable) {
        this.retryable = retryable;
    }

    public Boolean getEnabled() {
        return enabled;
    }

    public void setEnabled(Boolean enabled) {
        this.enabled = enabled;
    }
}

修改配置文件

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

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

自定义RouteLocator类

加载配置文件和数据,生成ZuulRoute对象。

package com.xinxin.vip.sso.locator;

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配置类

将自定义的RouteLocator类DynamicRouteLocator加入到Spring容器中。

package com.xinxin.vip.sso.config;

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;
​
@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;
    }
​
}

创建定时器

使用Spring的定时任务,定时刷新数据库的路由配置。

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.debug("定时刷新路由表==>{}", routeLocator.getRoutes());
        RoutesRefreshedEvent routesRefreshedEvent = new RoutesRefreshedEvent(routeLocator);
        publisher.publishEvent(routeLocator);
    }
}