首先萄喳,引入spring-cloud-starter-zuul
之后會(huì)間接引入:
hystrix依賴已經(jīng)引入且警,那么何種情況下使用hystrix呢句旱?
在Zuul的自動(dòng)配置類ZuulServerAutoConfiguration
和ZuulProxyAutoConfiguration
中總共會(huì)向Spring容器注入3個(gè)Zuul的RouteFilter呻待,分別是
-
SimpleHostRoutingFilter
簡(jiǎn)單路由薇组,通過(guò)HttpClient向預(yù)定的URL發(fā)送請(qǐng)求
生效條件:
RequestContext.getCurrentContext().getRouteHost() != null
? && RequestContext.getCurrentContext().sendZuulResponse()1、RequestContext中的routeHost不為空铸敏,routeHost就是URL缚忧,即使用URL直連
2、RequestContext中的sendZuulResponse為true杈笔,即是否將response發(fā)送給客戶端闪水,默認(rèn)為true
-
RibbonRoutingFilter
使用Ribbon、Hystrix和可插入的http客戶端發(fā)送請(qǐng)求
生效條件:
(RequestContext.getRouteHost() == null && RequestContext.get(SERVICE_ID_KEY) != null
? && RequestContext.sendZuulResponse())1蒙具、RequestContext中的routeHost為空球榆,即URL為空
2、RequestContext中的serviceId不為空
3禁筏、RequestContext中的sendZuulResponse為true持钉,即是否將response發(fā)送給客戶端,默認(rèn)為true
-
SendForwardFilter
forward到本地URL
生效條件:
RequestContext.containsKey(FORWARD_TO_KEY)
? && !RequestContext.getBoolean(SEND_FORWARD_FILTER_RAN, false)1篱昔、RequestContext中包含F(xiàn)ORWARD_TO_KEY每强,即URL使用 forward: 映射
2、RequestContext中SEND_FORWARD_FILTER_RAN為false州刽,SEND_FORWARD_FILTER_RAN意為“send forward是否運(yùn)行過(guò)了”空执,在SendForwardFilter#run()時(shí)會(huì)
ctx.set(SEND_FORWARD_FILTER_RAN, true)
綜上所述,在使用serviceId映射的方法路由轉(zhuǎn)發(fā)的時(shí)候穗椅,會(huì)使用Ribbon+Hystrix
而哪種路由配置方式是“URL映射”辨绊,哪種配置方式又是“serviceId映射”呢?
Zuul有一個(gè)前置過(guò)濾器PreDecorationFilter
用于通過(guò)RouteLocator路由定位器
決定在何時(shí)以何種方式路由轉(zhuǎn)發(fā)
RouteLocator是用于通過(guò)請(qǐng)求地址匹配到Route路由的匹表,之后PreDecorationFilter
再通過(guò)Route信息設(shè)置RequestContext上下文门坷,決定后續(xù)使用哪個(gè)RouteFilter做路由轉(zhuǎn)發(fā)
所以就引出以下問(wèn)題:
- 什么是Route
- RouteLocator路由定位器如何根據(jù)請(qǐng)求路徑匹配路由
- 匹配到路由后,PreDecorationFilter如何設(shè)置RequestContext請(qǐng)求上下文
什么是Route
我總共見(jiàn)到兩個(gè)和Route相關(guān)的類
ZuulProperties.ZuulRoute
袍镀,用于和zuul配置文件關(guān)聯(lián)默蚌,保存相關(guān)信息
org.springframework.cloud.netflix.zuul.filters.Route
, RouteLocator找到的路由信息就是這個(gè)類苇羡,用于路由轉(zhuǎn)發(fā)
public static class ZuulRoute {
private String id; //ZuulRoute的id
private String path; //路由的pattern敏簿,如 /foo/**
private String serviceId; //要映射到此路由的服務(wù)id
private String url; //要映射到路由的完整物理URL
private boolean stripPrefix = true; //用于確定在轉(zhuǎn)發(fā)之前是否應(yīng)剝離此路由前綴的標(biāo)志位
private Boolean retryable; //此路由是否可以重試,通常重試需要serviceId和ribbon
private Set<String> sensitiveHeaders = new LinkedHashSet(); //不會(huì)傳遞給下游請(qǐng)求的敏感標(biāo)頭列表
private boolean customSensitiveHeaders = false; //是否自定義了敏感頭列表
}
public class Route {
private String id;
private String fullPath;
private String path;
private String location; //可能是 url 或 serviceId
private String prefix;
private Boolean retryable;
private Set<String> sensitiveHeaders = new LinkedHashSet<>();
private boolean customSensitiveHeaders;
}
可以看到org.springframework.cloud.netflix.zuul.filters.Route
和ZuulProperties.ZuulRoute
基本一致宣虾,只是Route用于路由轉(zhuǎn)發(fā)定位的屬性location根據(jù)不同的情況,可能是一個(gè)具體的URL温数,可能是一個(gè)serviceId
RouteLocator路由定位器如何根據(jù)請(qǐng)求路徑匹配路由
Zuul在自動(dòng)配置加載時(shí)注入了2個(gè)RouteLocator
-
CompositeRouteLocator: 組合的RouteLocator绣硝,在
getMatchingRoute()
時(shí)會(huì)依次調(diào)用其它的RouteLocator,先找到先返回撑刺;CompositeRouteLocator的routeLocators集合中只有DiscoveryClientRouteLocator -
DiscoveryClientRouteLocator: 可以將靜態(tài)的鹉胖、已配置的路由與來(lái)自DiscoveryClient服務(wù)發(fā)現(xiàn)的路由組合在一起,來(lái)自DiscoveryClient的路由優(yōu)先;SimpleRouteLocator的子類(SimpleRouteLocator 基于加載到
ZuulProperties
中的配置定位Route路由信息)
其中CompositeRouteLocator是 @Primary 的甫菠,它是組合多個(gè)RouteLocator的Locator挠铲,其getMatchingRoute()
方法會(huì)分別調(diào)用其它所有RouteLocator的getMatchingRoute()方法,通過(guò)請(qǐng)求路徑匹配路由信息寂诱,只要匹配到了就馬上返回
默認(rèn)CompositeRouteLocator混合路由定位器的routeLocators只有一個(gè)DiscoveryClientRouteLocator拂苹,故只需分析DiscoveryClientRouteLocator#getMatchingRoute(path)
//----------DiscoveryClientRouteLocator是SimpleRouteLocator子類,其實(shí)是調(diào)用的SimpleRouteLocator##getMatchingRoute(path)
@Override
public Route getMatchingRoute(final String path) {
return getSimpleMatchingRoute(path);
}
protected Route getSimpleMatchingRoute(final String path) {
if (log.isDebugEnabled()) {
log.debug("Finding route for path: " + path);
}
// routes是保存路由信息的map痰洒,如果此時(shí)還未加載瓢棒,調(diào)用locateRoutes()
if (this.routes.get() == null) {
this.routes.set(locateRoutes());
}
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());
}
/**
* 下面的方法主要是先對(duì)path做微調(diào)
* 再根據(jù)path到routes中匹配到ZuulRoute
* 最后根據(jù) ZuulRoute 和 adjustedPath 生成 Route
*/
String adjustedPath = adjustPath(path);
ZuulRoute route = getZuulRoute(adjustedPath);
return getRoute(route, adjustedPath);
}
下面我們來(lái)看看locateRoutes()
是如何加載靜態(tài)的、已配置的路由與來(lái)自DiscoveryClient服務(wù)發(fā)現(xiàn)的路由的
//----------DiscoveryClientRouteLocator#locateRoutes() 服務(wù)發(fā)現(xiàn)路由定位器的locateRoutes()
@Override
protected LinkedHashMap<String, ZuulRoute> locateRoutes() {
//保存ZuulRoute的LinkedHashMap
LinkedHashMap<String, ZuulRoute> routesMap = new LinkedHashMap<String, ZuulRoute>();
//調(diào)用父類SimpleRouteLocator#locateRoutes()
//加載ZuulProperties中的所有配置文件中的路由信息
routesMap.putAll(super.locateRoutes());
//如果服務(wù)發(fā)現(xiàn)客戶端discovery存在
if (this.discovery != null) {
//將routesMap已經(jīng)存在的配置文件中的ZuulRoute放入staticServices<serviceId, ZuulRoute>
Map<String, ZuulRoute> staticServices = new LinkedHashMap<String, ZuulRoute>();
for (ZuulRoute route : routesMap.values()) {
String serviceId = route.getServiceId();
//如果serviceId為null丘喻,以id作為serviceId脯宿,此情況適合 zuul.routes.xxxx=/xxxx/** 的情況
if (serviceId == null) {
serviceId = route.getId();
}
if (serviceId != null) {
staticServices.put(serviceId, route);
}
}
// Add routes for discovery services by default
List<String> services = this.discovery.getServices(); //到注冊(cè)中心找到所有service
String[] ignored = this.properties.getIgnoredServices()
.toArray(new String[0]);
//遍歷services
for (String serviceId : services) {
// Ignore specifically ignored services and those that were manually
// configured
String key = "/" + mapRouteToService(serviceId) + "/**";
//如果注冊(cè)中心的serviceId在staticServices集合中,并且此路由沒(méi)有配置URL
//那么泉粉,更新路由的location為serviceId
if (staticServices.containsKey(serviceId)
&& staticServices.get(serviceId).getUrl() == null) {
// Explicitly configured with no URL, cannot be ignored
// all static routes are already in routesMap
// Update location using serviceId if location is null
ZuulRoute staticRoute = staticServices.get(serviceId);
if (!StringUtils.hasText(staticRoute.getLocation())) {
staticRoute.setLocation(serviceId);
}
}
//如果注冊(cè)中心的serviceId不在忽略范圍內(nèi)连霉,且routesMap中還沒(méi)有包含,添加到routesMap
if (!PatternMatchUtils.simpleMatch(ignored, serviceId)
&& !routesMap.containsKey(key)) {
// Not ignored
routesMap.put(key, new ZuulRoute(key, serviceId));
}
}
}
// 如果routesMap中有 /** 的默認(rèn)路由配置
if (routesMap.get(DEFAULT_ROUTE) != null) {
ZuulRoute defaultRoute = routesMap.get(DEFAULT_ROUTE);
// Move the defaultServiceId to the end
routesMap.remove(DEFAULT_ROUTE);
routesMap.put(DEFAULT_ROUTE, defaultRoute);
}
//將routesMap中的數(shù)據(jù)微調(diào)后嗡靡,放到values<String, ZuulRoute>跺撼,返回
LinkedHashMap<String, ZuulRoute> values = new LinkedHashMap<>();
for (Entry<String, ZuulRoute> entry : routesMap.entrySet()) {
String path = entry.getKey();
// Prepend with slash if not already present.
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;
}
此方法運(yùn)行后就已經(jīng)加載了配置文件中所有路由信息,以及注冊(cè)中心中的服務(wù)路由信息叽躯,有的通過(guò)URL路由财边,有的通過(guò)serviceId路由
只需根據(jù)本次請(qǐng)求的requestURI與 路由的pattern匹配找到對(duì)應(yīng)的路由
匹配到路由后,PreDecorationFilter如何設(shè)置RequestContext請(qǐng)求上下文
//----------PreDecorationFilter前置過(guò)濾器
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
final String requestURI = this.urlPathHelper.getPathWithinApplication(ctx.getRequest());
Route route = this.routeLocator.getMatchingRoute(requestURI); //找到匹配的路由
//----------------到上面為止是已經(jīng)分析過(guò)的点骑,根據(jù)requestURI找到匹配的Route信息
// ==== 匹配到路由信息
if (route != null) {
String location = route.getLocation();
if (location != null) {
ctx.put(REQUEST_URI_KEY, route.getPath());//RequestContext設(shè)置 requestURI:路由的pattern路徑
ctx.put(PROXY_KEY, route.getId());//RequestContext設(shè)置 proxy:路由id
//設(shè)置需要忽略的敏感頭信息酣难,要么用全局默認(rèn)的,要么用路由自定義的
if (!route.isCustomSensitiveHeaders()) {
this.proxyRequestHelper
.addIgnoredHeaders(this.properties.getSensitiveHeaders().toArray(new String[0]));
}
else {
this.proxyRequestHelper.addIgnoredHeaders(route.getSensitiveHeaders().toArray(new String[0]));
}
//設(shè)置重試信息
if (route.getRetryable() != null) {
ctx.put(RETRYABLE_KEY, route.getRetryable());
}
//如果location是 http/https開(kāi)頭的黑滴,RequestContext設(shè)置 routeHost:URL
//如果location是 forward:開(kāi)頭的憨募,RequestContext設(shè)置 forward信息、routeHost:null
//其它 RequestContext設(shè)置 serviceId袁辈、routeHost:null菜谣、X-Zuul-ServiceId
if (location.startsWith(HTTP_SCHEME+":") || location.startsWith(HTTPS_SCHEME+":")) {
ctx.setRouteHost(getUrl(location));
ctx.addOriginResponseHeader(SERVICE_HEADER, location);
}
else if (location.startsWith(FORWARD_LOCATION_PREFIX)) {
ctx.set(FORWARD_TO_KEY,
StringUtils.cleanPath(location.substring(FORWARD_LOCATION_PREFIX.length()) + route.getPath()));
ctx.setRouteHost(null);
return null;
}
else {
// set serviceId for use in filters.route.RibbonRequest
ctx.set(SERVICE_ID_KEY, location);
ctx.setRouteHost(null);
ctx.addOriginResponseHeader(SERVICE_ID_HEADER, location);
}
//是否添加代理頭信息 X-Forwarded-For
if (this.properties.isAddProxyHeaders()) {
addProxyHeaders(ctx, route);
String xforwardedfor = ctx.getRequest().getHeader(X_FORWARDED_FOR_HEADER);
String remoteAddr = ctx.getRequest().getRemoteAddr();
if (xforwardedfor == null) {
xforwardedfor = remoteAddr;
}
else if (!xforwardedfor.contains(remoteAddr)) { // Prevent duplicates
xforwardedfor += ", " + remoteAddr;
}
ctx.addZuulRequestHeader(X_FORWARDED_FOR_HEADER, xforwardedfor);
}
//是否添加Host頭信息
if (this.properties.isAddHostHeader()) {
ctx.addZuulRequestHeader(HttpHeaders.HOST, toHostHeader(ctx.getRequest()));
}
}
}
// ==== 沒(méi)有匹配到路由信息
else {
log.warn("No route found for uri: " + requestURI);
String fallBackUri = requestURI;
String fallbackPrefix = this.dispatcherServletPath; // default fallback
// servlet is
// DispatcherServlet
if (RequestUtils.isZuulServletRequest()) {
// remove the Zuul servletPath from the requestUri
log.debug("zuulServletPath=" + this.properties.getServletPath());
fallBackUri = fallBackUri.replaceFirst(this.properties.getServletPath(), "");
log.debug("Replaced Zuul servlet path:" + fallBackUri);
}
else {
// remove the DispatcherServlet servletPath from the requestUri
log.debug("dispatcherServletPath=" + this.dispatcherServletPath);
fallBackUri = fallBackUri.replaceFirst(this.dispatcherServletPath, "");
log.debug("Replaced DispatcherServlet servlet path:" + fallBackUri);
}
if (!fallBackUri.startsWith("/")) {
fallBackUri = "/" + fallBackUri;
}
String forwardURI = fallbackPrefix + fallBackUri;
forwardURI = forwardURI.replaceAll("http://", "/");
ctx.set(FORWARD_TO_KEY, forwardURI);
}
return null;
}
總結(jié):
- 只要引入了spring-cloud-starter-zuul就會(huì)間接引入Ribbon、Hystrix
- 路由信息可能是從配置文件中加載的晚缩,也可能是通過(guò)DiscoveryClient從注冊(cè)中心加載的
- zuul是通過(guò)前置過(guò)濾器PreDecorationFilter找到與當(dāng)前requestURI匹配的路由信息尾膊,并在RequestContext中設(shè)置相關(guān)屬性的,后續(xù)的Route Filter會(huì)根據(jù)RequestContext中的這些屬性判斷如何路由轉(zhuǎn)發(fā)
- Route Filter主要使用 SimpleHostRoutingFilter 和 RibbonRoutingFilter
- 當(dāng)RequestContext請(qǐng)求上下文中存在routeHost荞彼,即URL直連信息時(shí)冈敛,使用SimpleHostRoutingFilter簡(jiǎn)單Host路由
- 當(dāng)RequestContext請(qǐng)求上下文中存在serviceId,即服務(wù)id時(shí)(可能會(huì)與注冊(cè)中心關(guān)聯(lián)獲取服務(wù)列表鸣皂,或者讀取配置文件中serviceId.ribbon.listOfServers的服務(wù)列表)抓谴,使用RibbonRoutingFilter暮蹂,會(huì)使用Ribbon、Hystrix