本文介绍Zuul路由的源码以及实现动态路由的几种方式,可以先阅读
Zuul1.x核心源码解析
了解路由源码流程
本文基于spring-cloud-starter-zuul(SpringCloud版本为Edgware.SR3)
1、源码解析
1)、路由转发
route类型的
SimpleHostRoutingFilter
、
RibbonRoutingFilter
负责实际的请求,核心代码如下:
public class SimpleHostRoutingFilter extends ZuulFilter {
@Override
public boolean shouldFilter() {
return RequestContext.getCurrentContext().getRouteHost() != null
&& RequestContext.getCurrentContext().sendZuulResponse();
}
@Override
public Object run() {
RequestContext context = RequestContext.getCurrentContext();
HttpServletRequest request = context.getRequest();
MultiValueMap<String, String> headers = this.helper
.buildZuulRequestHeaders(request);
MultiValueMap<String, String> params = this.helper
.buildZuulRequestQueryParams(request);
String verb = getVerb(request);
InputStream requestEntity = getRequestBody(request);
if (request.getContentLength() < 0) {
context.setChunkedRequestBody();
}
String uri = this.helper.buildZuulRequestURI(request);
this.helper.addIgnoredHeaders();
try {
CloseableHttpResponse response = forward(this.httpClient, verb, uri, request,
headers, params, requestEntity);
setResponse(response);
}
catch (Exception ex) {
throw new ZuulRuntimeException(ex);
}
return null;
}
public class RibbonRoutingFilter extends ZuulFilter {
@Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
return (ctx.getRouteHost() == null && ctx.get(SERVICE_ID_KEY) != null
&& ctx.sendZuulResponse());
}
@Override
public Object run() {
RequestContext context = RequestContext.getCurrentContext();
this.helper.addIgnoredHeaders();
try {
RibbonCommandContext commandContext = buildCommandContext(context);
ClientHttpResponse response = forward(commandContext);
setResponse(response);
return response;
}
catch (ZuulException ex) {
throw new ZuulRuntimeException(ex);
}
catch (Exception ex) {
throw new ZuulRuntimeException(ex);
}
}
两个Filter都需要满足
sendZuulResponse()
,即需要将Response反馈给客户端
不同点在于SimpleHostRoutingFilter需要
RouteHost
不为空,通过HttpClient来转发请求;RibbonRoutingFilter需要
serviceId
不为空而且
RouteHost
为空,通过Ribbon、Hystrix完成客户端负载均衡
配置示例如下
:
#单实例配置 SimpleHostRoutingFilter
zuul.routes.service1.path=/service1/**
zuul.routes.service1.url=http://localhost:8081/
对符合
/service1/**
规则的请求路由转发到http://localhost:8081地址的路由规则,该路由规则由SimpleHostRoutingFilter实现路由转发
#多实例配置 RibbonRoutingFilter
zuul.routes.service2.path=/service2/**
zuul.routes.service2.service-id=service2
service2.ribbon.listOfServers=http://localhost:8082/,http://localhost:8083/
对符合
/service2/**
规则的请求路由转发到http://localhost:8082和http://localhost:8083两个实例地址的路由规则,该路由规则由RibbonRoutingFilter实现路由转发
2)、预处理
pre类型ZuulFilter中,
PreDecorationFilter
会根据路由信息进行预处理,其处理结果决定了使用哪个route类型ZuulFilter来实际处理请求
public class PreDecorationFilter extends ZuulFilter {
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
final String requestURI = this.urlPathHelper.getPathWithinApplication(ctx.getRequest());
//根据requestURI获取路由信息
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());
}
//以https或http开头,设置RouteHost
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,RouteHost置空
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 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("//", "/");
ctx.set(FORWARD_TO_KEY, forwardURI);
}
return null;
}
routeLocator.getMatchingRoute()
根据请求URL获取Route,再根据Route的location是否匹配http:、https:、forward:前缀来设置属性
使用上面的
/service1/**
和
/service2/**
规则,访问
http://localhost:8080/service1/helloworld
和
http://localhost:8080/service2/helloworld
获取的Route信息如下:
Route{id='service1', fullPath='/service1/helloworld', path='/helloworld', location='http://localhost:8081/', prefix='/service1', retryable=false, sensitiveHeaders=[], customSensitiveHeaders=false, prefixStripped=true}
Route{id='service2', fullPath='/service2/helloworld', path='/helloworld', location='service2', prefix='/service2', retryable=false, sensitiveHeaders=[], customSensitiveHeaders=false, prefixStripped=true}
3)、路由定位
PreDecorationFilter中通过RouteLocator根据URL获取Route
public interface RouteLocator {
Collection<String> getIgnoredPaths();
List<Route> getRoutes();
Route getMatchingRoute(String path);
}
RouteLocator主要功能如下:
- 根据path获取Route
- 获取所有Route
RouteLocator继承关系图如下:
1)SimpleRouteLocator
简单路由定位器,路由信息来自ZuulProperties,
locateRoutes()
是定位路由的核心,从ZuulProperties中加载了路由数据
public class SimpleRouteLocator implements RouteLocator, Ordered {
//routes存储路由信息
private AtomicReference<Map<String, ZuulRoute>> routes = new AtomicReference<>();
//获取路由信息
protected Map<String, ZuulRoute> locateRoutes() {
LinkedHashMap<String, ZuulRoute> routesMap = new LinkedHashMap<String, ZuulRoute>();
//提取ZuulProperties中的ZuulRoute
for (ZuulRoute route : this.properties.getRoutes().values()) {
routesMap.put(route.getPath(), route);
}
return routesMap;
}
//刷新路由缓存
protected void doRefresh() {
this.routes.set(locateRoutes());
}
2)DiscoveryClientRouteLocator
DiscoveryClientRouteLocator基于DiscoveryClient,路由数据来自properties中的静态配置和DiscoveryClient从注册中心获取的数据
DiscoveryClientRouteLocator主要功能如下:
- 动态添加Route
- 刷新路由
- 从DiscoveryClient获取路由信息
public interface RefreshableRouteLocator extends RouteLocator {
void refresh();
}
public class DiscoveryClientRouteLocator extends SimpleRouteLocator
implements RefreshableRouteLocator {
//动态添加路由,会同步把路由信息添加到ZuulProperties,参数也可以是ZuulRoute
public void addRoute(String path, String location) {
this.properties.getRoutes().put(path, new ZuulRoute(path, location));
refresh();
}
@Override
protected LinkedHashMap<String, ZuulRoute> locateRoutes() {
LinkedHashMap<String, ZuulRoute> routesMap = new LinkedHashMap<String, ZuulRoute>();
//通过父类SimpleRouteLocator获取静态路由信息
routesMap.putAll(super.locateRoutes());
//通过DiscoveryClient获取路由信息
if (this.discovery != null) {
Map<String, ZuulRoute> staticServices = new LinkedHashMap<String, ZuulRoute>();
for (ZuulRoute route : routesMap.values()) {
String serviceId = route.getServiceId();
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();
String[] ignored = this.properties.getIgnoredServices()
.toArray(new String[0]);
for (String serviceId : services) {
// Ignore specifically ignored services and those that were manually
// configured
String key = "/" + mapRouteToService(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);
}
}
if (!PatternMatchUtils.simpleMatch(ignored, serviceId)
&& !routesMap.containsKey(key)) {
// Not ignored
routesMap.put(key, new ZuulRoute(key, serviceId));
}
}
}
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);
}
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;
}
@Override
public void refresh() {
//调用SimpleRouteLocator的doRefresh()方法
doRefresh();
}
3)CompositeRouteLocator
CompositeRouteLocator用于组合多个RouteLocator,用Collection存储多个RouteLocator,调用
getRoutes()
、
getMatchingRoute()
、
refresh()
时都会逐一调用每个RouteLocator相应的方法
public class CompositeRouteLocator implements RefreshableRouteLocator {
private final Collection<? extends RouteLocator> routeLocators;
private ArrayList<RouteLocator> rl;
@Override
public List<Route> getRoutes() {
List<Route> route = new ArrayList<>();
for (RouteLocator locator : routeLocators) {
route.addAll(locator.getRoutes());
}
return route;
}
@Override
public Route getMatchingRoute(String path) {
for (RouteLocator locator : routeLocators) {
Route route = locator.getMatchingRoute(path);
if (route != null) {
return route;
}
}
return null;
}
@Override
public void refresh() {
for (RouteLocator locator : routeLocators) {
if (locator instanceof RefreshableRouteLocator) {
((RefreshableRouteLocator) locator).refresh();
}
}
}
2、动态路由实现
1)、刷新路由的方式
Zuul提供了ZuulRefreshListener,监听到RoutesRefreshedEvent事件后,会调用ZuulHandlerMapping的
setDirty()
方法,进而调用RouteLocator的
refresh()
方法
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) {
//设置为脏,下一次匹配到路径时,如果发现为脏,则会去刷新路由信息
this.zuulHandlerMapping.setDirty(true);
}
else if (event instanceof HeartbeatEvent) {
if (this.heartbeatMonitor.update(((HeartbeatEvent) event).getValue())) {
this.zuulHandlerMapping.setDirty(true);
}
}
}
}
public class ZuulHandlerMapping extends AbstractUrlHandlerMapping {
public void setDirty(boolean dirty) {
this.dirty = dirty;
if (this.routeLocator instanceof RefreshableRouteLocator) {
((RefreshableRouteLocator) this.routeLocator).refresh();
}
}
2)、实现动态路由加载
#多实例配置
zuul.routes.service2.path=/service2/**
zuul.routes.service2.service-id=service2
service2.ribbon.listOfServers=http://localhost:8082/,http://localhost:8083/
多实例配置中的配置包含两部分:Zuul的路由配置和Ribbon service-id对应的后端路径,
zuul.routes.service2.service-id
的值和
<service-id>.ribbon.listOfServers
key的前缀相匹配,所以动态路由的实现也包含刷新Zuul的路由信息和Ribbon service-id对应的后端路径两部分
@Data
@NoArgsConstructor
public class DynamicRouteEntity {
private String serviceId;
private String path;
private String serverListStr;
}
DynamicRouteEntity为自定义的动态路由实体,和多实例配置中的key一一对应
public class CustomServerList implements ServerList<Server>, IClientConfigAware {
private IClientConfig clientConfig;
@Override
public List<Server> getInitialListOfServers() {
return getUpdatedListOfServers();
}
@Override
public List<Server> getUpdatedListOfServers() {
return ClientServerListCacheControl.getInstance()
.getServerListCacheByClientName(clientConfig.getClientName());
}
@Override
public void initWithNiwsConfig(IClientConfig iClientConfig) {
this.clientConfig = iClientConfig;
}
}
CustomServerList为自定义ServerList类,等价于重写listOfServer配置,通过
getUpdatedListOfServers()
来维护service-id对应的后端路径。
Ribbon中的PollingServerListUpdater会定时调用
getUpdatedListOfServers()
方法,这就是实现动态加载Ribbon service-id对应后端路径的关键
public class ClientServerListCacheControl {
private static final ClientServerListCacheControl CLIENT_SERVER_LIST_CACHE_CONTROL =
new ClientServerListCacheControl();
/**
* key和zuulRoute中serviceId相对应 value为转发的后端地址 多个使用,分隔
*/
private Map<String, String> clientServerListCache = new ConcurrentHashMap<>();
private ClientServerListCacheControl() {
}
public static ClientServerListCacheControl getInstance() {
return CLIENT_SERVER_LIST_CACHE_CONTROL;
}
public List<Server> getServerListCacheByClientName(String clientName) {
List<Server> serverList = Lists.newArrayList();
String serverListStr = clientServerListCache.get(clientName);
if (!Strings.isNullOrEmpty(serverListStr)) {
for (String s : serverListStr.split(",")) {
serverList.add(new Server(s.trim()));
}
}
return serverList;
}
public void setClientServerListCache(String clientName, String serverList) {
clientServerListCache.put(clientName, serverList);
}
}
ClientServerListCacheControl主要是用来存储service-id对应的后端路径
public class CustomRibbonClientConfiguration extends RibbonClientConfiguration {
@Override
@Bean
@ConditionalOnMissingBean
public ServerList<Server> ribbonServerList(IClientConfig config) {
CustomServerList serverList = new CustomServerList();
serverList.initWithNiwsConfig(config);
return serverList;
}
}
@Configuration
@RibbonClients(defaultConfiguration = {CustomRibbonClientConfiguration.class})
public class CustomRibbonAutoConfiguration {
}
CustomRibbonClientConfiguration和CustomRibbonAutoConfiguration都是配置类
#ribbon服务列表刷新频率1s 默认值是30s
ribbon.ServerListRefreshInterval=1000
配置文件中修改Ribbon服务列表刷新频率,也就是Ribbon的PollingServerListUpdater定时任务的频率
@Component
public class DynamicRoutesProcessor implements InitializingBean {
@Autowired
private ApplicationContext applicationContext;
@Autowired
private CompositeRouteLocator compositeRouteLocator;
@Autowired
private ZuulProperties zuulProperties;
/**
* 初始化路由信息
*
* @throws Exception
*/
@Override
public void afterPropertiesSet() throws Exception {
Map<String, ZuulProperties.ZuulRoute> routes = zuulProperties.getRoutes();
ZuulProperties.ZuulRoute zuulRoute = new ZuulProperties.ZuulRoute();
zuulRoute.setId("service3");
zuulRoute.setPath("/service3/**");
//location以http:和https:开头,设置的是ZuulRoute中的url属性;否则设置serviceId属性
//设置serviceId
zuulRoute.setLocation("service3");
routes.put("service3", zuulRoute);
//刷新路由
applicationContext.publishEvent(new RoutesRefreshedEvent(compositeRouteLocator));
//设置ClientServerListCache
//PollingServerListUpdater定时任务更新时,会调用CustomServerList的getUpdatedListOfServers()方法,从ClientServerListCache中拿到最新的后端地址
ClientServerListCacheControl.getInstance().setClientServerListCache(
"service3", "http://localhost:8081/,http://localhost:8082/");
}
/**
* 动态刷新
*
* @param dynamicRouteEntityList 路由信息和转发的后端地址
*/
public void refreshRoutes(List<DynamicRouteEntity> dynamicRouteEntityList) {
Map<String, ZuulProperties.ZuulRoute> routes = zuulProperties.getRoutes();
//提取routeList数据并添加到routes中
for (DynamicRouteEntity dynamicRouteEntity : dynamicRouteEntityList) {
if (StringUtils.isEmpty(dynamicRouteEntity.getServiceId())
|| StringUtils.isEmpty(dynamicRouteEntity.getPath())
|| StringUtils.isEmpty(dynamicRouteEntity.getServerListStr())) {
continue;
}
//Zuul中的路由信息
ZuulProperties.ZuulRoute route = new ZuulProperties.ZuulRoute();
route.setId(dynamicRouteEntity.getServiceId());
route.setPath(dynamicRouteEntity.getPath());
route.setLocation(dynamicRouteEntity.getServiceId());
routes.put(route.getId(), route);
//设置ClientServerListCache 从而更新Ribbon service-id对应的后端路径
ClientServerListCacheControl.getInstance().setClientServerListCache(
route.getServiceId(), dynamicRouteEntity.getServerListStr());
}
//刷新路由
applicationContext.publishEvent(new RoutesRefreshedEvent(compositeRouteLocator));
}
}
DynamicRoutesProcessor是实现Zuul路由动态刷新的关键,分为两部分:初始化路由的
afterPropertiesSet()
方法和动态刷新路由的
refreshRoutes(List<DynamicRouteEntity> dynamicRouteEntityList)
方法
刷新路由时,通过触发RoutesRefreshedEvent事件刷新Zuul的路由,同时更新ClientServerListCache中存储的service-id对应的后端对应路径的关系,Ribbon中的PollingServerListUpdater定时1s调用CustomServerList的
getUpdatedListOfServers()
方法,从而从ClientServerListCache中实现动态刷新Ribbon service-id对应的后端路径
参考
:
https://chenyongjun.vip/articles/113
https://github.com/wpstan/CustomZuulRibbon