踩坑日记 -- spring cloud zuul 路径匹配

1,822 阅读3分钟

spring cloud zuul 路径匹配

openapi gateway zuul 配置

  • account-biz-front服务 子母账户划转
zuul.routes.sub-transfer2.path = /v1/accounts/sub-transfer
zuul.routes.sub-transfer2.serviceId = account-biz-front
  • account-front 服务 查询账户
zuul.routes.accounts-single.path = /v1/accounts/*
zuul.routes.accounts-single.serviceId = account-front

zuul 匹配源码分析

在springboot项目中使用zuul需要使用 @EnableZuulServer或@EnableZuulProxy中的至少一个注解。

  • @EnableZuulServer对应ZuulServerAutoConfiguration
  • @EnableZuulProxy对应ZuulProxyAutoConfiguration
  1. 在ZuulProxyAutoConfiguration类中,注入了PreDecorationFilter进行拦截
@Configuration
@ConditionalOnBean(ZuulProxyMarkerConfiguration.Marker.class)
public class ZuulProxyAutoConfiguration extends ZuulServerAutoConfiguration {
    ...
	// pre filters
	// 注入PreDecorationFilter前置拦截
	@Bean
	@ConditionalOnMissingBean(PreDecorationFilter.class)
	public PreDecorationFilter preDecorationFilter(RouteLocator routeLocator, ProxyRequestHelper proxyRequestHelper) {
		return new PreDecorationFilter(routeLocator, this.server.getServlet().getServletPrefix(), this.zuulProperties,
				proxyRequestHelper);
	}
}
  1. 进入PreDecorationFilter.class 中分析运行代码
public class PreDecorationFilter extends ZuulFilter {
    ...
    @Override
	public Object run() {
	        //1.根据请求的url找到对应的路由Route
		RequestContext ctx = RequestContext.getCurrentContext();
		final String requestURI = this.urlPathHelper.getPathWithinApplication(ctx.getRequest());
		// 就是这里啦~~~  【KEY】
		Route route = this.routeLocator.getMatchingRoute(requestURI);
		....
	}
	...
}	

其中Route route = this.routeLocator.getMatchingRoute(requestURI); 是根据url路径获取路由的核心逻辑。

继续跟踪,routeLocator是一个接口,对于getMatchingRoute方法有两个实现类SimpleRouteLocator和CompositeRouteLocator,其中CompositeRouteLocator是遍历routeLocator.getMatchingRoute()方法,因此我们进入SimpleRouteLocator类的getMatchingRoute方法

public class SimpleRouteLocator implements RouteLocator, Ordered {

    @Override
	public Route getMatchingRoute(final String path) {

		return getSimpleMatchingRoute(path);

	}

	protected Map<String, ZuulRoute> getRoutesMap() {
		if (this.routes.get() == null) {
			this.routes.set(locateRoutes());
		}
		return this.routes.get();
	}

	protected Route getSimpleMatchingRoute(final String path) {
            if (log.isDebugEnabled()) {
            	log.debug("Finding route for path: " + path);
            }
            
            // This is called for the initialization done in getRoutesMap()
            //1.获取ZuulRoute的映射对
            getRoutesMap();
            
            if (log.isDebugEnabled()) {
            	log.debug("servletPath=" + this.dispatcherServletPath);
            	log.debug("zuulServletPath=" + this.zuulServletPath);
            	log.debug("RequestUtils.isDispatcherServletRequest()="
            			+ RequestUtils.isDispatcherServletRequest());
            	log.debug("RequestUtils.isZuulServletRequest()="
            			+ RequestUtils.isZuulServletRequest());
            }
            //2.对url路径预处理
            String adjustedPath = adjustPath(path);
            //3.根据路径获取匹配的ZuulRoute
            ZuulRoute route = getZuulRoute(adjustedPath);
            
            return getRoute(route, adjustedPath);
	}
	...
	protected ZuulRoute getZuulRoute(String adjustedPath) {
	        // 判断是否路由是否匹配
		if (!matchesIgnoredPatterns(adjustedPath)) {
			for (Entry<String, ZuulRoute> entry : getRoutesMap().entrySet()) {
				String pattern = entry.getKey();
				log.debug("Matching pattern:" + pattern);
				if (this.pathMatcher.match(pattern, adjustedPath)) {
					return entry.getValue();
				}
			}
		}
		return null;
	}
	
    ...
    // 路由规则通过LinkedHashMap存储
	protected Map<String, ZuulRoute> locateRoutes() {
		LinkedHashMap<String, ZuulRoute> routesMap = new LinkedHashMap<>();
		for (ZuulRoute route : this.properties.getRoutes().values()) {
			routesMap.put(route.getPath(), route);
		}
		return routesMap;
	}
	...
}
  1. 上面所示代码是基础的路由规则加载算法,我们可以看到这些路由规则是通过LinkedHashMap保存的,也就是说路由规则的保存是有序的。
protected boolean matchesIgnoredPatterns(String path) {
    //路由匹配算法    
    for (String pattern : this.properties.getIgnoredPatterns()) {
    	log.debug("Matching ignored pattern:" + pattern);
    	if (this.pathMatcher.match(pattern, path)) {
    		log.debug("Path " + path + " matches ignored pattern " + pattern);
    		return true;
    	}
    }
    return false;
}
  1. 从上面的路由匹配算法中,我们可以看到它在使用路由规则匹配请求路径的时候是通过线性遍历的方式,在请求路径获取到第一个匹配的路由规则之后就会返回并结束匹配过程。

**所以当存在多个匹配的路由规则时,匹配结果完全取决于路由规则的保存顺序 **

解决方案

  1. properties的配置内容无法保证有序
  • 为了保证路由的优先顺序,需要使用YAML文件来配置,以实现有序的路由规则,比如使用下面的定义:
zuul:
  routes:
    sub-transfer2:
      path: /v1/accounts/sub-transfer
      serviceId: account-biz-front
    accounts-single:
      path: /v1/accounts/*
      serviceId: account-front
  1. 变更uri,v1升级为v2
  • account-biz-front服务 子母账户划转
zuul.routes.sub-transfer2.path = /v2/accounts/sub-transfer
zuul.routes.sub-transfer2.serviceId = account-biz-front

小结

通配符规则

通配符 说明
? 匹配任意的单个字符
* 匹配任意数量的字符
** 匹配任意数量的字符,支持多级目录

EXAMPLE

URL路径 说明
/v1/accounts/? 匹配/v1/accounts/之后拼接一个任务字符的路径,比如:/v1/accounts/a、/v1/accounts/b、/v1/accounts/c
/v1/accounts/* 匹配/v1/accounts/之后拼接任意字符的路径,比如:/v1/accounts/a、/v1/accounts/aaa、/v1/accounts/bbb。
/v1/accounts/** 匹配/v1/accounts/*包含的内容之外,如/v1/accounts/a/b的多级目录路径