在使用SpringCloud的时候准备使用Zuul作为微服务的网关,Zuul的默认路由方式主要是两种,一种是在配置 文件里直接指定静态路由,另一种是根据注册在Eureka的服务名自动匹配。比如如果有一个名为service1的服 务,通过 domain.com/service1/xxx 就能访问到这个服务。但是这和我预想的需求还是有些差距。 网上有许多有关动态路由的实现方法,大致思想是不从Eureka拉取注册服务信息,而是在资料库里自己维护一 份路由表,定时读取资料库拉取路由,来实现自动更新。而我的需求更进一步,我希望对外暴露的网关介面是 一个固定的url,如domain.com/gateway ,然后根据一个头信息service来指定想要访问的服务,而不是在 url后面拼接服务名。同时我也不想将我注册到Eureka的服务名直接暴露在api中,而是做一层映射,让我可以 灵活指定serivce名。

例如:

  • 访问 domain.com/gateway ,headers包含service=service.a,调用后端service1
  • 访问 domain.com/gateway ,headers包含service=service.b,调用后端service2

现在研究一下Zuul的源码来看看怎么实现这个功能。

我们都知道在Springboot启动类上加一个@EnableZuulProxy就启用了Zuul。从这个注解点进去看看:

@EnableCircuitBreaker
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(ZuulProxyMarkerConfiguration.class)
public @interface EnableZuulProxy {
}

它引入了ZuulProxyMarkerConfiguration这个配置,进去看下。

/**
* Responsible for adding in a marker bean to trigger activation of
* {@link ZuulProxyAutoConfiguration}.
*
* @author Biju Kunjummen
*/

@Configuration
public class ZuulProxyMarkerConfiguration {

@Bean
public Marker zuulProxyMarkerBean() {
return new Marker();
}

class Marker {

}

}

从注释中看到这个是用于激活ZuulProxyAutoConfiguration,看看这个类

@Configuration
@Import({ RibbonCommandFactoryConfiguration.RestClientRibbonConfiguration.class,
RibbonCommandFactoryConfiguration.OkHttpRibbonConfiguration.class,
RibbonCommandFactoryConfiguration.HttpClientRibbonConfiguration.class,
HttpClientConfiguration.class })
@ConditionalOnBean(ZuulProxyMarkerConfiguration.Marker.class)
public class ZuulProxyAutoConfiguration extends ZuulServerAutoConfiguration {
//...
}

这个配置下面注册了很多组件,不过先暂时不看,它同时继承自ZuulServerAutoConfiguration,看看这个类:

@Configuration
@EnableConfigurationProperties({ ZuulProperties.class })
@ConditionalOnClass({ ZuulServlet.class, ZuulServletFilter.class })
@ConditionalOnBean(ZuulServerMarkerConfiguration.Marker.class)
// Make sure to get the ServerProperties from the same place as a normal web app would
// FIXME @Import(ServerPropertiesAutoConfiguration.class)
public class ZuulServerAutoConfiguration {

@Autowired
protected ZuulProperties zuulProperties;

@Autowired
protected ServerProperties server;

@Autowired(required = false)
private ErrorController errorController;

private Map<String, CorsConfiguration> corsConfigurations;

@Autowired(required = false)
private List<WebMvcConfigurer> configurers = emptyList();

@Bean
public HasFeatures zuulFeature() {
return HasFeatures.namedFeature("Zuul (Simple)",
ZuulServerAutoConfiguration.class);
}

@Bean
@Primary
public CompositeRouteLocator primaryRouteLocator(
Collection<RouteLocator> routeLocators) {
return new CompositeRouteLocator(routeLocators);
}

@Bean
@ConditionalOnMissingBean(SimpleRouteLocator.class)
public SimpleRouteLocator simpleRouteLocator() {
return new SimpleRouteLocator(this.server.getServlet().getContextPath(),
this.zuulProperties);
}

@Bean
public ZuulController zuulController() {
return new ZuulController();
}

@Bean
public ZuulHandlerMapping zuulHandlerMapping(RouteLocator routes) {
ZuulHandlerMapping mapping = new ZuulHandlerMapping(routes, zuulController());
mapping.setErrorController(this.errorController);
mapping.setCorsConfigurations(getCorsConfigurations());
return mapping;
}

protected final Map<String, CorsConfiguration> getCorsConfigurations() {
if (this.corsConfigurations == null) {
ZuulCorsRegistry registry = new ZuulCorsRegistry();
this.configurers.forEach(configurer -> configurer.addCorsMappings(registry));
this.corsConfigurations = registry.getCorsConfigurations();
}
return this.corsConfigurations;
}

@Bean
public ApplicationListener<ApplicationEvent> zuulRefreshRoutesListener() {
return new ZuulRefreshListener();
}

@Bean
@ConditionalOnMissingBean(name = "zuulServlet")
@ConditionalOnProperty(name = "zuul.use-filter", havingValue = "false", matchIfMissing = true)
public ServletRegistrationBean zuulServlet() {
ServletRegistrationBean<ZuulServlet> servlet = new ServletRegistrationBean<>(
new ZuulServlet(), this.zuulProperties.getServletPattern());
// The whole point of exposing this servlet is to provide a route that doesnt
// buffer requests.
servlet.addInitParameter("buffer-requests", "false");
return servlet;
}

@Bean
@ConditionalOnMissingBean(name = "zuulServletFilter")
@ConditionalOnProperty(name = "zuul.use-filter", havingValue = "true", matchIfMissing = false)
public FilterRegistrationBean zuulServletFilter() {
final FilterRegistrationBean<ZuulServletFilter> filterRegistration = new FilterRegistrationBean<>();
filterRegistration.setUrlPatterns(
Collections.singleton(this.zuulProperties.getServletPattern()));
filterRegistration.setFilter(new ZuulServletFilter());
filterRegistration.setOrder(Ordered.LOWEST_PRECEDENCE);
// The whole point of exposing this servlet is to provide a route that doesnt
// buffer requests.
filterRegistration.addInitParameter("buffer-requests", "false");
return filterRegistration;
}

// pre filters

@Bean
public ServletDetectionFilter servletDetectionFilter() {
return new ServletDetectionFilter();
}

@Bean
public FormBodyWrapperFilter formBodyWrapperFilter() {
return new FormBodyWrapperFilter();
}

@Bean
public DebugFilter debugFilter() {
return new DebugFilter();
}

@Bean
public Servlet30WrapperFilter servlet30WrapperFilter() {
return new Servlet30WrapperFilter();
}

// post filters

@Bean
public SendResponseFilter sendResponseFilter(ZuulProperties properties) {
return new SendResponseFilter(zuulProperties);
}

@Bean
public SendErrorFilter sendErrorFilter() {
return new SendErrorFilter();
}

@Bean
public SendForwardFilter sendForwardFilter() {
return new SendForwardFilter();
}

@Bean
@ConditionalOnProperty("zuul.ribbon.eager-load.enabled")
public ZuulRouteApplicationContextInitializer zuulRoutesApplicationContextInitiazer(
SpringClientFactory springClientFactory) {
return new ZuulRouteApplicationContextInitializer(springClientFactory,
zuulProperties);
}

@Configuration
protected static class ZuulFilterConfiguration {

@Autowired
private Map<String, ZuulFilter> filters;

@Bean
public ZuulFilterInitializer zuulFilterInitializer(CounterFactory counterFactory,
TracerFactory tracerFactory) {
FilterLoader filterLoader = FilterLoader.getInstance();
FilterRegistry filterRegistry = FilterRegistry.instance();
return new ZuulFilterInitializer(this.filters, counterFactory, tracerFactory,
filterLoader, filterRegistry);
}

}

@Configuration
@ConditionalOnClass(MeterRegistry.class)
protected static class ZuulCounterFactoryConfiguration {

@Bean
@ConditionalOnBean(MeterRegistry.class)
@ConditionalOnMissingBean(CounterFactory.class)
public CounterFactory counterFactory(MeterRegistry meterRegistry) {
return new DefaultCounterFactory(meterRegistry);
}

}

@Configuration
protected static class ZuulMetricsConfiguration {

@Bean
@ConditionalOnMissingClass("io.micrometer.core.instrument.MeterRegistry")
@ConditionalOnMissingBean(CounterFactory.class)
public CounterFactory counterFactory() {
return new EmptyCounterFactory();
}

@ConditionalOnMissingBean(TracerFactory.class)
@Bean
public TracerFactory tracerFactory() {
return new EmptyTracerFactory();
}

}

private static class ZuulRefreshListener
implements ApplicationListener<ApplicationEvent> {

@Autowired
private ZuulHandlerMapping zuulHandlerMapping;

private HeartbeatMonitor heartbeatMonitor = new HeartbeatMonitor();

@Override
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ContextRefreshedEvent
|| event instanceof RefreshScopeRefreshedEvent
|| event instanceof RoutesRefreshedEvent
|| event instanceof InstanceRegisteredEvent) {
reset();
}
else if (event instanceof ParentHeartbeatEvent) {
ParentHeartbeatEvent e = (ParentHeartbeatEvent) event;
resetIfNeeded(e.getValue());
}
else if (event instanceof HeartbeatEvent) {
HeartbeatEvent e = (HeartbeatEvent) event;
resetIfNeeded(e.getValue());
}
}

private void resetIfNeeded(Object value) {
if (this.heartbeatMonitor.update(value)) {
reset();
}
}

private void reset() {
this.zuulHandlerMapping.setDirty(true);
}

}

private static class ZuulCorsRegistry extends CorsRegistry {

@Override
protected Map<String, CorsConfiguration> getCorsConfigurations() {
return super.getCorsConfigurations();
}

}

}

这个配置类里注册了很多bean:

  • SimpleRouteLocator:默认的路由定位器,主要负责维护配置文件中的路由配置。
  • DiscoveryClientRouteLocator:继承自SimpleRouteLocator,该类会将配置文件中的静态路由配置以及服务发现(比如eureka)中的路由信息进行合并,主要是靠它路由到具体服务。
  • CompositeRouteLocator:组合路由定位器,看入参就知道应该是会保存好多个RouteLocator,构造过程中其实仅包括一个DiscoveryClientRouteLocator。
  • ZuulController:Zuul创建的一个Controller,用于将请求交由ZuulServlet处理。
  • ZuulHandlerMapping:这个会添加到SpringMvc的HandlerMapping链中,只有选择了ZuulHandlerMapping的请求才能出发到Zuul的后续流程。

还有一些其他的Filter,不一一看了。

其中,ZuulServlet是整个流程的核心,请求的过程是具体这样的,当Zuulservlet收到请求后, 会创建一个ZuulRunner对象,该对象中初始化了RequestContext:作为存储整个请求的一些数据,并被所有的Zuulfilter共享。ZuulRunner中还有一个 FilterProcessor,FilterProcessor作为执行所有的Zuulfilter的管理器。FilterProcessor从filterloader 中获取zuulfilter,而zuulfilter是被filterFileManager所 载入,并支持groovy热载入,采用了轮询的方式热载入。有了这些filter之后,zuulservelet首先执行的Pre类型的过滤器,再执行route类型的过滤器, 最后执行的是post 类型的过滤器,如果在执行这些过滤器有错误的时候则会执行error类型的过滤器。执行完这些过滤器,最终将请求的结果返回给客户端。 RequestContext就是会一直跟著整个请求周期的上下文对象,filters之间有什么信息需要传递就set一些值进去就行了。

有个示例图可以帮助理解一下:

ZuulServlet中的service方法:

@Override
public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {
try {
init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);

// Marks this request as having passed through the "Zuul engine", as opposed to servlets
// explicitly bound in web.xml, for which requests will not have the same data attached
RequestContext context = RequestContext.getCurrentContext();
context.setZuulEngineRan();

try {
//执行pre阶段的filters
preRoute();
} catch (ZuulException e) {
error(e);
postRoute();
return;
}
try {
//执行route阶段的filters
route();
} catch (ZuulException e) {
error(e);
postRoute();
return;
}
try {
//执行post阶段的filters
postRoute();
} catch (ZuulException e) {
error(e);
return;
}

} catch (Throwable e) {
error(new ZuulException(e, 500, "UNHANDLED_EXCEPTION_" + e.getClass().getName()));
} finally {
RequestContext.getCurrentContext().unset();
}
}

可以顺带说明一下ZuulFilter,他包括4个基本特征:过滤类型、执行顺序、执行条件、具体操作。

String filterType();

int filterOrder();

boolean shouldFilter();

Object run();

它们各自的含义与功能总结如下:

  • filterType:该函数需要返回一个字元串来代表过滤器的类型,而这个类型就是在HTTP请求过程中定义的各个阶段。在Zuul中默认定义了四种不同生命周期的过滤器类型,具体如下:
  • pre:可以在请求被路由之前调用。
  • routing:在路由请求时候被调用。
  • post:在routing和error过滤器之后被调用。
  • error:处理请求时发生错误时被调用。
  • filterOrder:通过int值来定义过滤器的执行顺序,数值越小优先顺序越高。
  • shouldFilter:返回一个boolean类型来判断该过滤器是否要执行。我们可以通过此方法来指定过滤器的有效范围。
  • run:过滤器的具体逻辑。在该函数中,我们可以实现自定义的过滤逻辑,来确定是否要拦截当前的请求,不对其进行后续的路由,或是在请求路由返回结果之后,对处理结果做一些加工等。

下图源自Zuul的官方WIKI中关于请求生命周期的图解,它描述了一个HTTP请求到达API网关之后,如何在各个不同类型的过滤器之间流转的详细过程。

Zuul默认实现了一批过滤器,如下:

|过滤器 |order |描述 |类型 |:---|:---:|:---:|---:| |ServletDetectionFilter| -3| 检测请求是用 DispatcherServlet还是 ZuulServlet| pre| |Servlet30WrapperFilter| -2| 在Servlet 3.0 下,包装 requests| pre| |FormBodyWrapperFilter| -1| 解析表单数据| pre| |SendErrorFilter| 0| 如果中途出现错误| error| |DebugFilter| 1| 设置请求过程是否开启debug| pre| |PreDecorationFilter| 5| 根据uri决定调用哪一个route过滤器| pre| |RibbonRoutingFilter| 10| 如果写配置的时候用ServiceId则用这个route过滤器,该过滤器可以用Ribbon 做负载均衡,用hystrix做熔断| route| |SimpleHostRoutingFilter| 100| 如果写配置的时候用url则用这个route过滤| route| |SendForwardFilter| 500| 用RequestDispatcher请求转发| route| |SendResponseFilter| 1000| 用RequestDispatcher请求转发| post|

回到我的需求,我不需要静态配置,所有请求都是调用在eureka注册的服务,所以每次请求都要在route阶段转到RibbonRoutingFilter,由它使用Ribbon向其它服务发起请求,因此看一下这个类的shouldFilter()方法:

@Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
return (ctx.getRouteHost() == null && ctx.get(SERVICE_ID_KEY) != null
&& ctx.sendZuulResponse());
}

原来进入这个Filter的条件是RequestContext中getRouteHost为空且ctx.get(SERVICE_ID_KEY)不为空,即serviceId有值! 那么Zuul在默认情况下是怎么选择route阶段的Filter的呢?看到上面的pre阶段有一个PreDecorationFilter,这个类主要就是根据uri来给RequestContext添加不同的内容来控制之后走哪个route过滤器。 看下它的Run方法:

@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
final String requestURI = this.urlPathHelper
.getPathWithinApplication(ctx.getRequest());
//已经包含的路由配置里是否有能匹配到的route
Route route = this.routeLocator.getMatchingRoute(requestURI);
if (route != null) {
String location = route.getLocation();
if (location != null) {
ctx.put(REQUEST_URI_KEY, route.getPath());
ctx.put(PROXY_KEY, route.getId());
if (!route.isCustomSensitiveHeaders()) {
this.proxyRequestHelper.addIgnoredHeaders(
this.properties.getSensitiveHeaders().toArray(new String[0]));
}
else {
this.proxyRequestHelper.addIgnoredHeaders(
route.getSensitiveHeaders().toArray(new String[0]));
}

if (route.getRetryable() != null) {
ctx.put(RETRYABLE_KEY, route.getRetryable());
}
//根据各种情况设置context
//http:开头的
if (location.startsWith(HTTP_SCHEME + ":")
|| location.startsWith(HTTPS_SCHEME + ":")) {
ctx.setRouteHost(getUrl(location));
ctx.addOriginResponseHeader(SERVICE_HEADER, location);
}
//forward:开头的
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;
}
//这里设置了serviceId,走Ribbon
else {
// set serviceId for use in filters.route.RibbonRequest
ctx.set(SERVICE_ID_KEY, location);
ctx.setRouteHost(null);
ctx.addOriginResponseHeader(SERVICE_ID_HEADER, location);
}
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);
}
if (this.properties.isAddHostHeader()) {
ctx.addZuulRequestHeader(HttpHeaders.HOST,
toHostHeader(ctx.getRequest()));
}
}
}
else {
log.warn("No route found for uri: " + requestURI);
String forwardURI = getForwardUri(requestURI);
//都不满足的话,设置一个forward.to,走SendForwardFilter
ctx.set(FORWARD_TO_KEY, forwardURI);
}
return null;
}

情况比较复杂,实际根据我的需求,我只要让route阶段时候使用RibbonRoutingFilter,因此我只要保证进入route阶段时RequestContext里包含对应服务的serviceId就行了。 我可以在pre阶段将请求头内的service转化为所需要的服务serviceId,设置到context内,同时移除context中其它有影响的值就行了。

听上去挺简单的,我们自定义一个pre阶段的Filter。

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.netflix.zuul.filters.Route;
import org.springframework.cloud.netflix.zuul.filters.RouteLocator;
import org.springframework.web.util.UrlPathHelper;

import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;

public class HeaderPreDecorationFilter extends ZuulFilter {

private static final Log log = LogFactory.getLog(HeaderPreDecorationFilter.class);
private RouteLocator routeLocator;
private UrlPathHelper urlPathHelper = new UrlPathHelper();

private Map<String, Service> serviceMap = new HashMap();

public HeaderPreDecorationFilter(RouteLocator routeLocator, String dispatcherServletPath) {
this.routeLocator = routeLocator;
//举个小例子,假如我在后端有一个名为platform-server的服务,服务内有一个/mwd/client/test的介面
serviceMap.put("mwd.service.test", new Service("platform-server", "/mwd/client/test"));
}

public String filterType() {
return "pre";
}

public int filterOrder() {
return 6;
}

public boolean shouldFilter() {
return true;
}

public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String requestURI = this.urlPathHelper.getPathWithinApplication(ctx.getRequest());
//取得头信息
String serviceName = request.getHeader("service");
//获取头信息映射成对应的服务信息
Service service = serviceMap.get(serviceName);
String serviceURI = service.getServiceId() + service.getPath();
//TODO 判断服务是否存在,可以做额外异常处理
Route route = this.routeLocator.getMatchingRoute("/" + serviceURI);
//设置context
ctx.set("serviceId", service.getServiceId());
ctx.put("requestURI", service.getPath());
ctx.put("proxy", service.getServiceId());
ctx.put("retryable", false);
// ctx.remove("forward.to");
log.info(String.format("send %s request to %s", request.getMethod(), request.getRequestURL().toString()));
return null;
}

class Service {

public Service(String serviceId, String path) {
this.serviceId = serviceId;
this.path = path;
}

String serviceId;
String path;

public String getServiceId() {
return serviceId;
}

public void setServiceId(String serviceId) {
this.serviceId = serviceId;
}

public String getPath() {
return path;
}

public void setPath(String path) {
this.path = path;
}
}
}

然后可以将之前的PreDecorationFilter禁用,以免它对RequestContext的操作影响我们,例如,如果没有匹配到任何规则,它会在RequestContext中添加一个forward.to 这个key会调用post阶段的SendForwardFilter导致报错。

在配置文件设置zuul.PreDecorationFilter.pre.disable=true即可。

现在将这个类纳入spring容器中,写法可以参照ZuulProxyAutoConfiguration中其它Filter的实例化方式,我们也做一个自己的配置类:

@Configuration
@EnableConfigurationProperties({ZuulProperties.class})
public class Config {

@Autowired
protected ZuulProperties zuulProperties;
@Autowired
protected ServerProperties server;

@Bean
public HeaderPreDecorationFilter geaderPreDecorationFilter(RouteLocator routeLocator) {
return new HeaderPreDecorationFilter(routeLocator, this.server.getServlet().getContextPath());
}
}

这样每次请求进来后,在pre阶段会去取service头信息,然后匹配成对应的serviceId(取不到或者匹配不到自然就报错了),在route阶段就直接触发RibbonRoutingFilter调用服务返回了!

现在还剩一个网关入口的问题,我是想让所有的请求走一个固定的url,先试著直接访问一下:localhost:8080/gateway ,直接报404了。很正常,我们还没有做这个url path的映射! SpringMvc的DispatcherServlet没有查到这个path的处理方法自然报404了!怎样才能让gateway这个路由进入zuul中呢?

我们记得在上面Zuul的配置类中有一个ZuulHandlerMapping, 当一个请求进入SpringMvc的DispatchServlet后,会根据路由看能否匹配到ZuulHandlerMapping,匹配成功才会走zuul后续的流程。

以下是DispatcherServlet中doDispatch方法的代码:

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

try {
try {
ModelAndView mv = null;
Object dispatchException = null;

try {
processedRequest = this.checkMultipart(request);
multipartRequestParsed = processedRequest != request;
mappedHandler = this.getHandler(processedRequest);
if (mappedHandler == null) {
this.noHandlerFound(processedRequest, response);
return;
}
//这里选择ZuulHandlerMapping,如果路由匹配成功,会返回包含ZuulController的ha
HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());

// ... 省略代码

//从这里进入调用ZuulController
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}

this.applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
} catch (Exception var20) {
dispatchException = var20;
} catch (Throwable var21) {
dispatchException = new NestedServletException("Handler dispatch failed", var21);
}

this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);
} catch (Exception var22) {
this.triggerAfterCompletion(processedRequest, response, mappedHandler, var22);
} catch (Throwable var23) {
this.triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", var23));
}

} finally {
if (asyncManager.isConcurrentHandlingStarted()) {
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
} else if (multipartRequestParsed) {
this.cleanupMultipart(processedRequest);
}

}
}

那么怎样才能让请求进入ZuulHandlerMapping呢,看下DispatchServlet中的的这个方法:

@Nullable
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
Iterator var2 = this.handlerMappings.iterator();
//按顺序遍历所有的HandlerMapping,直到取得一个
while(var2.hasNext()) {
HandlerMapping mapping = (HandlerMapping)var2.next();
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
}

return null;
}

我们需要ZuulHandlerMapping在mapping.getHandler的时候返回非空。研究下ZuulHandlerMapping,看下它的结构先:

ZuulHandlerMapping继承了AbstractUrlHandlerMapping,AbstractUrlHandlerMapping又继承自AbstractHandlerMapping。在上面的方法中调用ZuulHandlerMapping的mapping.getHandler(request)的时候 实际会调用到AbstractHandlerMapping的getHandlerInternal(request),再进入ZuulHandlerMapping的lookupHandler(String urlPath, HttpServletRequest request)这个方法。

看下这个方法:

@Override
protected Object lookupHandler(String urlPath, HttpServletRequest request)
throws Exception {
if (this.errorController != null
&& urlPath.equals(this.errorController.getErrorPath())) {
return null;
}
if (isIgnoredPath(urlPath, this.routeLocator.getIgnoredPaths())) {
return null;
}
RequestContext ctx = RequestContext.getCurrentContext();
if (ctx.containsKey("forward.to")) {
return null;
}
if (this.dirty) {
synchronized (this) {
if (this.dirty) {
registerHandlers();
this.dirty = false;
}
}
}
//实际会调用这里
return super.lookupHandler(urlPath, request);
}

调用父类的AbstractUrlHandlerMapping.lookupHandler(urlPath, request)。

这个方法里代码比较多,其中的关键信息是:this.handlerMap.get(urlPath),也就是说我们输入的url path只要能从handlerMap里匹配到,就可以了! 现在需要看下ZuulHandlerMapping里的这个handlerMap是怎么维护的。类中有这么一个方法:

private void registerHandlers() {
Collection<Route> routes = this.routeLocator.getRoutes();
if (routes.isEmpty()) {
this.logger.warn("No routes found from RouteLocator");
}
else {
for (Route route : routes) {
registerHandler(route.getFullPath(), this.zuul);
}
}
}

它会从routeLocator里取出所有的route,一个一个注册到handlerMap里。这样的话就简单了,我只要自己定义一个RouteLocator,把我想要的路由设置好,再让它自动被注册进去就行了吧!

定义一个GatewayRouteLocator:

public class GatewayRouteLocator extends SimpleRouteLocator implements RefreshableRouteLocator {

public final static Logger logger = LoggerFactory.getLogger(GatewayRouteLocator.class);

public GatewayRouteLocator(String servletPath, ZuulProperties properties) {
super(servletPath, properties);
}
public void refresh() {
doRefresh();
}

@Override
protected Map<String, ZuulProperties.ZuulRoute> locateRoutes() {
LinkedHashMap<String, ZuulProperties.ZuulRoute> routesMap = new LinkedHashMap<String, ZuulProperties.ZuulRoute>();
routesMap.put("gateway", new ZuulProperties.ZuulRoute());
return routesMap;
}

@Override
public List<Route> getRoutes() {
//假设我希望网关API为http://www.domain.com/gateway
List<Route> values = new ArrayList<Route>();
values.add(new Route("gateway1", "/gateway/", "/gateway", "", true, new HashSet<String>()));
values.add(new Route("gateway2", "/gateway", "/gateway", "", true, new HashSet<String>()));
return values;
}
}

现在我要将这个类也实例化到spring容器中。

观察下ZuulProxyAutoConfiguration中的RouteLocator是怎么实例化的,照葫芦画瓢弄一下,把这个类也添加到配置类里:

@Configuration
@EnableConfigurationProperties({ZuulProperties.class})
public class Config {

@Autowired
protected ZuulProperties zuulProperties;
@Autowired
protected ServerProperties server;

@Bean
public GatewayRouteLocator gatewayRouteLocator() {
return new GatewayRouteLocator(this.server.getServlet().getContextPath(), zuulProperties);
}

@Bean
public HeaderPreDecorationFilter geaderPreDecorationFilter(RouteLocator routeLocator) {
return new HeaderPreDecorationFilter(routeLocator, this.server.getServlet().getContextPath());
}
}

好了!这样每次输入domain.com/gateway 的时候,DispatchServlet就会为我们匹配到ZuulHandlerMapping,进而往下走到ZuulController中了。

再看下ZuulController的代码:

ZuulController:

public class ZuulController extends ServletWrappingController {

public ZuulController() {
//在这里已经设置了ZuulServlet
setServletClass(ZuulServlet.class);
setServletName("zuul");
setSupportedMethods((String[]) null); // Allow all
}

@Override
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
try {
//在这里面会调用ZuulServlet的service方法
return super.handleRequestInternal(request, response);
}
finally {
// @see com.netflix.zuul.context.ContextLifecycleFilter.doFilter
RequestContext.getCurrentContext().unset();
}
}

}

就是将Request送入ZuulServlet,这样就跟上面的流程衔接上了!

总结一下,一次请求流程为 DispatcherServlet->ZuulHandlerMapping->ZuulController->ZuulServlet->ZuulRunner-> FilterProcessor->ZuulFilter->PreDecorationFilter(替换为自定义的HeaderPreDecorationFilter)->RibbonRoutingFilter

至此,对Zuul的改造就完成了!现在我对外暴露一个统一的api:domain.com/gateway,所有的服务都从这里调用,同时通过传入一个service的头信息来指定调用具体 的服务,服务列表可以维护在其它地方动态刷新,这样就不会将serviceName暴露出去了!


推荐阅读:
相关文章