Zuul 实现动态路由

需求:SpringCloud 整合 zuul路由,实现在数据库中修改serviceId 字段的值即可切换调用服务。譬如:假设在本地环境,zuul的运行端口是 8081。现在有ServiceA, ServiceB在正常运行,在zuul里配置的路由地址分别是 /service-a/** 和 /service-b/** 。现在访问A,访问地址可以写成 http://localhost:8081/service-a/xxx。现在我要实现只在数据库的路由表中改一下 serviceId,使得原来访问服务A的地址去访问到B服务上。

实现步骤如下:

1. 搭建zuul 项目

   1.1 导入依赖

    <!--eureka-client-->
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>

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

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

1.2 核心代码

自定义路由处理器是实现动态路由的核心,需要继承 

org.springframework.cloud.netflix.zuul.filters.SimpleRouteLocator 类

和实现 org.springframework.cloud.netflix.zuul.filters.RefreshableRouteLocator 接口

package com.atguigu.springcloud.filter;

import com.atguigu.springcloud.entity.ZuulRouteEntity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.dao.DataAccessException;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.util.StringUtils;

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

/**
 * Created by caoshi at 21:21 2022-05-04
 */
public class CustomRouteLocator extends SimpleRouteLocator implements RefreshableRouteLocator {

    private static Logger logger = LoggerFactory.getLogger(CustomRouteLocator.class);

    private JdbcTemplate jdbcTemplate;

    private ZuulProperties properties;


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

    public CustomRouteLocator(String servletPath, ZuulProperties properties) {
        super(servletPath, properties);
        this.properties = properties;
        logger.info("servletPath:{}", servletPath);
    }

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

    @Override
    protected Map<String, ZuulProperties.ZuulRoute> locateRoutes() {
        LinkedHashMap<String, ZuulProperties.ZuulRoute> routesMap = new LinkedHashMap<>();
        routesMap.putAll(super.locateRoutes());
        routesMap.putAll(locateRoutesFromDB());
        Map<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;
    }

    private Map<String, ZuulProperties.ZuulRoute> locateRoutesFromDB() {
        logger.info("============= 开始加载db路由表配置 ==============");
        Map<String, ZuulProperties.ZuulRoute> routes = new LinkedHashMap<>();
        try {
            List<ZuulRouteEntity> results =
                    this.jdbcTemplate.query("select * from x_gw_route where enabled = 1",
                            new BeanPropertyRowMapper<>(ZuulRouteEntity.class));

            for (ZuulRouteEntity result : results) {
                if (StringUtils.isEmpty(result.getPath())) {
                    continue;
                }
                ZuulProperties.ZuulRoute zuulRoute = new ZuulProperties.ZuulRoute();
                copy2ZuulRoute(result, zuulRoute);

                routes.put(zuulRoute.getPath(), zuulRoute);
            }
        } catch (DataAccessException e) {
            logger.error("=============加载db中路由表配置出错==============", e);
        }

        return routes;
    }

    /**
     *
     * @param zuulRouteEntity
     * @param zuulRoute
     */
    private void copy2ZuulRoute(ZuulRouteEntity zuulRouteEntity, ZuulProperties.ZuulRoute zuulRoute) {
        if (zuulRouteEntity == null || zuulRoute == null) {
            return;
        }

        zuulRoute.setId(zuulRouteEntity.getId());
        zuulRoute.setServiceId(zuulRouteEntity.getService_id());
        zuulRoute.setPath(zuulRouteEntity.getPath());
        zuulRoute.setStripPrefix(zuulRouteEntity.getStrip_prefix() == 1);
        zuulRoute.setUrl(zuulRouteEntity.getUrl());
        zuulRoute.setRetryable(zuulRouteEntity.getRetryable() == 1);
    }

}

路由表映射实体类:

package com.atguigu.springcloud.entity;

/**
 * Created by caoshi at 20:53 2022-06-16
 */
public class ZuulRouteEntity {

    private String id;          // 主键id
    private String path;        // 访问路径 /xxx/** 
    private String service_id;  // 服务id 一般配成 spring.application.name
    private String url;         // 
    private int strip_prefix;   // 是否去除前缀,zuul自带前缀 /zuul,所以设置成1就是去掉前缀
    private int retryable;      // 是否失败后重新请求

    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 getService_id() {
        return service_id;
    }

    public void setService_id(String service_id) {
        this.service_id = service_id;
    }

    public String getUrl() {
        return url;
    }

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

    public int getStrip_prefix() {
        return strip_prefix;
    }

    public void setStrip_prefix(int strip_prefix) {
        this.strip_prefix = strip_prefix;
    }

    public int getRetryable() {
        return retryable;
    }

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

}

配置类,配置 CustomRouteLocator 交由Spring容器管理。

package com.atguigu.springcloud.config;

import com.atguigu.springcloud.filter.CustomRouteLocator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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;

/**
 * Created by caoshi at 20:51 2022-06-16
 */
@Configuration
public class CustomZuulConfig {


    private static Logger logger = LoggerFactory.getLogger(CustomZuulConfig.class);

    private ZuulProperties zuulProperties = new ZuulProperties();

    @Autowired
    private ServerProperties server;

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Bean
    public CustomRouteLocator routeLocator() {
        String contextPath = server.getServlet().getContextPath();

        logger.info("=========== servlet contextPath {}", contextPath);
        CustomRouteLocator routeLocator = new CustomRouteLocator(contextPath, zuulProperties);
        routeLocator.setJdbcTemplate(jdbcTemplate);
        return routeLocator;
    }


}

数据库路由表结构

CREATE TABLE `x_gw_route` (
  `id` varchar(50) NOT NULL COMMENT '主键',
  `path` varchar(255) NOT NULL COMMENT '访问路径',
  `service_id` varchar(50) DEFAULT NULL COMMENT '服务id',
  `url` varchar(255) DEFAULT NULL,
  `retryable` tinyint(1) DEFAULT NULL COMMENT '是否失败后重新请求1是0否',
  `enabled` tinyint(1) NOT NULL COMMENT '是否使用1正常0不使用',
  `strip_prefix` int(11) DEFAULT NULL COMMENT '是否去除前缀1是0否',
  `api_name` varchar(255) DEFAULT NULL COMMENT '服务名称',
  `c_desc` varchar(255) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '描述',
  `c_createuserid` varchar(60) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '创建用户id',
  `c_createtime` datetime DEFAULT NULL,
  `c_updateuserid` varchar(60) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '最后修改者id',
  `c_updatetime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;

至此动态路由的功能,zuul的配置完成。

如果还要实现请求过滤,比如校验token信息等。可以自定义过滤器

package com.atguigu.springcloud.filter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;

import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_TYPE;
/**
 * Created by caoshi at 22:43 2022-06-04
 */
@Component
public class AuthHeaderFilter extends ZuulFilter {

    private static Logger logger = LoggerFactory.getLogger(AuthHeaderFilter.class);


    @Override
    public String filterType() {
        return PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return 1;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() throws ZuulException {
        RequestContext requestContext = RequestContext.getCurrentContext();

        Object serviceIdKey = requestContext.get(FilterConstants.SERVICE_ID_KEY);
        Object request_uri_key = requestContext.get(FilterConstants.REQUEST_URI_KEY);

        System.out.println(serviceIdKey + "  " + request_uri_key);

        String servletPath = requestContext.getRequest().getServletPath();
        System.out.println("servletPath = " + servletPath);
        logger.info("网关接收到路由请求 {}  ", requestContext.getRequest().getRequestURI());


        return null;
    }
}

可以重写 run() 方法,可以取到 RequestContext 对象对请求进行过滤处理。

踩坑记录:

默认在路由表里 strip_prefix 字段的值是0,导致zull不会去除前缀,而zuul本身默认就包含一个前缀 /zuul

 

 导致在真实的请求前面加上这个前缀 /zuul。而我在服务提供者方的控制器方法上设置的 匹配路径中是不包含 前缀/zuul的,所以导致请求失败,报 404 错误。

最后的解决办法是 将路由表里 strip_prefix 设置为 1,表示去除前缀。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值