当前位置:网站首页>Zuul 實現動態路由

Zuul 實現動態路由

2022-06-26 04:52:00 Hejjon

需求: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,錶示去除前綴。

原网站

版权声明
本文为[Hejjon]所创,转载请带上原文链接,感谢
https://yzsam.com/2022/06/202206260452137778.html