由于公司环境较多,为避免频繁更新网关路由表配置,同时方便测试时可以略过网关解密功能,需要根据固定规则的请求url自动生成路由表,并转发请求到后端服务。
要求:

  1. 支持注册中心服务名与固定ip转发请求。
  2. 兼容配置文件中已经配置的路由转发规则
  3. 只有配置文件中允许的模块才允许动态路由
  4. 不允许重复创建并刷新路由,防止请求过慢

实际测试来看,如果请求走动态创建路由规则后再转发请求到后端接口,则请求要慢15倍左右。

1. 创建过滤器拦截所有请求url并判断是否需要创建动态路由

@Component
public class DynamicRouteFilter implements Filter {
    private static final Logger logger = LoggerFactory.getLogger(DynamicRouteFilter.class);

    @Autowired
    private DynamicRouteLocator dynamicRouteLocator;

    //允许动态路由的模块名
    @Value("${dynamic-route-module}")
    private String moduleName;

    // 允许动态路由的模块名集合
    private static LinkedHashSet<String> moduleNames = new LinkedHashSet<>();

    // 健康检查路径
    private final static String HEALTH_URL = "/health";

    private final static String BACKSLASH  = "/";


    /**
     * 加载配置文件中允许动态路由的模块名
     *
     * @param filterConfig
     * @throws ServletException
     */
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        Collections.addAll(moduleNames, moduleName.split(","));
    }

    /**
     * 获取请求路径,判断是否需要创建路由
     *
     * @param servletRequest
     * @param servletResponse
     * @param filterChain
     * @throws IOException
     * @throws ServletException
     */
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        String requestURI = request.getRequestURI(); // /apaas/api/user-service/getalluser
        if (BACKSLASH.equals(requestURI)){
            logger.error("request url is blank");
            filterChain.doFilter(servletRequest, servletResponse);
            return;
        }
        String[] urls = requestURI.split(BACKSLASH);
        if (requestURI.endsWith(HEALTH_URL) ||
                (StringUtils.isNotBlank(urls[1]) && !moduleNames.contains(urls[1]))) {
            filterChain.doFilter(servletRequest, servletResponse);
            return;
        }
        StringBuffer result = new StringBuffer(64);
        boolean notBlank = true;
        for (int i = 1; i < 4; i++) {
            notBlank = notBlank && StringUtils.isNotBlank(urls[i]);
        }
        if (notBlank) {
            result.append("/").append(urls[1]).append("/").append(urls[2]).append("/")
                    .append(urls[3]).append("/**");
        }
        if (this.create(result.toString())) {
            filterChain.doFilter(servletRequest, servletResponse);
        }
    }

    @Override
    public void destroy() {
    }

    /**
     * 如果路由表中没有,则创建动态路由
     *
     * @param result
     * @return
     */
    private synchronized boolean create(String result) {
        Map<String, ZuulProperties.ZuulRoute> routeMap = dynamicRouteLocator.getRouteMap();
        if (!routeMap.containsKey(result)) {
            dynamicRouteLocator.getUrl(result);
        }
        return true;
    }
}

2. spring-cloud-zuul要实现自定义路由需要继承SimpleRouteLocator类并实现路由刷新接口RefreshableRouteLocator

public class DynamicRouteLocator extends SimpleRouteLocator implements RefreshableRouteLocator {
    private static final Logger logger = LoggerFactory.getLogger(DynamicRouteLocator.class);
    @Autowired
    private ZuulHandlerMapping zuulHandlerMapping;

    // 初始化路由规则map长度
    private final static int mapSize = 1024;

    // 初始化路由规则id长度
    private final static int routeIdSize = 64;

    // 路由规则map
    private static Map<String, ZuulProperties.ZuulRoute> routeMap;

    //路由转发路径
    private String url = null;

    /**
     * 加载配置文件中的路由配置并且创建新路由表
     *
     * @param servletPath
     * @param properties
     */
    public DynamicRouteLocator(String servletPath, ZuulProperties properties) {
        super(servletPath, properties);
        routeMap = new HashMap<>(mapSize);
        routeMap.putAll(super.locateRoutes());
        logger.info("load " + super.locateRoutes().size() + " Routes rule from yml");
    }

    /**
     * 获取请求url,刷新路由
     *
     * @param url
     */
    public void getUrl(String url) {
        this.url = url;
        this.refresh();
    }

    /**
     * 获取请求url,刷新路由
     *
     * @return
     */
    public Map<String, ZuulProperties.ZuulRoute> getRouteMap() {
        return locateRoutes();
    }

    /**
     * 拼接信息并创建路由刷新规则
     *
     * @return 路由规则表
     */
    @Override
    protected Map<String, ZuulProperties.ZuulRoute> locateRoutes() {
        try {
            String path = this.url;// /apaas/api/mp-b-configure-service/**
            if (StringUtils.isBlank(path)) {
                return routeMap;
            }
            String[] urls = path.split("/");
            if (urls.length < 5) {
                logger.error("urls length not match");
                return routeMap;
            }
            String target = "";
            ZuulProperties.ZuulRoute zuulRoute = null;
            StringBuilder id = new StringBuilder(routeIdSize);
            id.append(urls[1]).append("-").append(urls[2]).append("-").append(urls[3]);
            if (urls[3].startsWith("ip_")) {
                String[] ipAndPort = urls[3].split("_");
                target = "http://" + ipAndPort[1] + ":" + ipAndPort[2];
                // 生成转发到固定ip port的ZuulRoute对象
                zuulRoute = createZuulRouteWithUrl(id.toString(), path, target);
            } else {
                target = urls[3];
                // 生成转发到服务名的ZuulRoute对象
                zuulRoute = createZuulRouteWithServiceID(id.toString(), path, target);
            }
            routeMap.put(path, zuulRoute);
            this.url = null;
            // 刷新路由,添加到SpringMvc的HandlerMapping链中,只有选择了ZuulHandlerMapping的请求才能出发到Zuul的后续流程
            zuulHandlerMapping.setDirty(true);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return routeMap;
    }

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

    /**
     * 创建路由规则
     *
     * @param id        路由表唯一id
     * @param path      映射路径
     * @param serviceId 服务名
     */
    private ZuulProperties.ZuulRoute createZuulRouteWithServiceID(String id, String path, String serviceId) {
        ZuulProperties.ZuulRoute zuulRoute = new ZuulProperties.ZuulRoute();
        zuulRoute.setId(id);
        zuulRoute.setPath(path);
        zuulRoute.setServiceId(serviceId);
        zuulRoute.setStripPrefix(true);
        logger.info("创建了路由 " + zuulRoute.toString());
        return zuulRoute;
    }

    /**
     * 创建路由规则
     *
     * @param id   路由表唯一id
     * @param path 映射路径
     * @param url  固定ip:port
     */
    private ZuulProperties.ZuulRoute createZuulRouteWithUrl(String id, String path, String url) {
        ZuulProperties.ZuulRoute zuulRoute = new ZuulProperties.ZuulRoute();
        zuulRoute.setId(id);
        zuulRoute.setPath(path);
        zuulRoute.setStripPrefix(true);
        zuulRoute.setUrl(url);
        logger.info("创建了路由 " + zuulRoute.toString());
        return zuulRoute;
    }
}

3. 启动类加载自定义路由bean
ZuulProperties springboot已经提前加载,直接使用即可

@Bean
    public DynamicRouteLocator getRouteLocator(ZuulProperties zuulProperties){
        return new DynamicRouteLocator(zuulProperties.getPrefix(), zuulProperties);
    }

4. 网关路由规则

  1. Yml配置文件配置静态路由
  • 例如:
    sso:
    path: /api/sso/**
    service-id: mp-b-sso-service
  1. 动态路由
    服务yml中配置允许动态路由的模块名称,多个模块使用,分割,只有配置了允许动态路由的模块才能走动态路由,不配置不允许走动态路由。
    动态路由请求url规则:
  • 例如:
    http://localhost:9099/apaas/openapi/mp-b-user-service/getall
    /apaas:模块名,类似apaas,pay,等等
    /api:需要解密的请求
    /openapi:不需要解密的请求
    /mp-b-user-service:请求的后端服务在注册中心的名称
    /getall:后端服务接口
  • 例如:
    http://localhost:9099/apaas/openapi/ip_127.0.0.1_9090/getall
    按照规则的url可选择是否使用加密请求后端固定ip端口服务,无需再配置文件中添加路由
    url规则:http://ip:port/模块名/是否要解密/ip_服务ip_服务端口/后端服务接口
    /apaas:模块名,类似apaas,pay,等等
    /api:需要解密的请求
    /openapi:不需要解密的请求
    ip_127.0.0.1_9090:请求的后端服务的ip端口,注意是ip_为开始标志
    /getall:后端服务接口