springcloud Zuul动态路由的实现
前言
zuul 是netflix 提供的一个开源组件,致力于在云平台上提供动态路由,监控,弹性,安全等边缘服务的框架。也有很多公司使用它来作为网关的重要组成部分,碰巧今年公司的架构组决定自研一个网关产品,集动态路由,动态权限,限流配额等功能为一体,为其他部门的项目提供统一的外网调用管理,最终形成产品(这方面阿里其实已经有成熟的网关产品了,但是不太适用于个性化的配置,也没有集成权限和限流降级)。
不过这里并不想介绍整个网关的架构,而是想着重于讨论其中的一个关键点,并且也是经常在交流群中听人说起的:动态路由怎么做?
再阐释什么是动态路由之前,需要介绍一下架构的设计。
传统互联网架构图
上图是没有网关参与的一个最典型的互联网架构(本文中统一使用book代表应用实例,即真正提供服务的一个业务系统)
加入eureka的架构图
book注册到eureka注册中心中,zuul本身也连接着同一个eureka,可以拉取book众多实例的列表。服务中心的注册发现一直是值得推崇的一种方式,但是不适用与网关产品。因为我们的网关是面向众多的其他部门的已有或是异构架构的系统,不应该强求其他系统都使用eureka,这样是有侵入性的设计。
最终架构图
要强调的一点是,gateway最终也会部署多个实例,达到分布式的效果,在架构图中没有画出,请大家自行脑补。
本博客的示例使用最后一章架构图为例,带来动态路由的实现方式,会有具体的代码。
动态路由
动态路由需要达到可持久化配置,动态刷新的效果。如架构图所示,不仅要能满足从spring的配置文件properties加载路由信息,还需要从数据库加载我们的配置。另外一点是,路由信息在容器启动时就已经加载进入了内存,我们希望配置完成后,实施发布,动态刷新内存中的路由信息,达到不停机维护路由信息的效果。
zuul–helloworlddemo
项目结构
<groupid>com.sinosoft</groupid> <artifactid>zuul-gateway-demo</artifactid> <packaging>pom</packaging> <version>1.0</version> <parent> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-parent</artifactid> <version>1.5.2.release</version> </parent> <modules> <module>gateway</module> <module>book</module> </modules> <dependencymanagement> <dependencies> <dependency> <groupid>org.springframework.cloud</groupid> <artifactid>spring-cloud-dependencies</artifactid> <version>camden.sr6</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencymanagement>
tip:springboot-1.5.2对应的springcloud的版本需要使用camden.sr6,一开始想专门写这个demo时,只替换了springboot的版本1.4.0->1.5.2,结果启动就报错了,最后发现是版本不兼容的锅。
gateway项目:
启动类:gatewayapplication.java
@enablezuulproxy @springbootapplication public class gatewayapplication { public static void main(string[] args) { springapplication.run(gatewayapplication.class, args); } }
配置:application.properties
#配置在配置文件中的路由信息 zuul.routes.books.url=http://localhost:8090 zuul.routes.books.path=/books/** #不使用注册中心,会带来侵入性 ribbon.eureka.enabled=false #网关端口 server.port=8080
book项目:
启动类:bookapplication.java
@restcontroller @springbootapplication public class bookapplication { @requestmapping(value = "/available") public string available() { system.out.println("spring in action"); return "spring in action"; } @requestmapping(value = "/checked-out") public string checkedout() { return "spring boot in action"; } public static void main(string[] args) { springapplication.run(bookapplication.class, args); } }
配置类:application.properties
server.port=8090
测试访问:http://localhost:8080/books/available
上述demo是一个简单的静态路由,简单看下源码,zuul是怎么做到转发,路由的。
@configuration @enableconfigurationproperties({ zuulproperties.class }) @conditionalonclass(zuulservlet.class) @import(serverpropertiesautoconfiguration.class) public class zuulconfiguration { @autowired //zuul的配置文件,对应了application.properties中的配置信息 protected zuulproperties zuulproperties; @autowired protected serverproperties server; @autowired(required = false) private errorcontroller errorcontroller; @bean public hasfeatures zuulfeature() { return hasfeatures.namedfeature("zuul (simple)", zuulconfiguration.class); } //核心类,路由定位器,最最重要 @bean @conditionalonmissingbean(routelocator.class) public routelocator routelocator() { //默认配置的实现是simpleroutelocator.class return new simpleroutelocator(this.server.getservletprefix(), this.zuulproperties); } //zuul的控制器,负责处理链路调用 @bean public zuulcontroller zuulcontroller() { return new zuulcontroller(); } //mvc handlermapping that maps incoming request paths to remote services. @bean public zuulhandlermapping zuulhandlermapping(routelocator routes) { zuulhandlermapping mapping = new zuulhandlermapping(routes, zuulcontroller()); mapping.seterrorcontroller(this.errorcontroller); return mapping; } //注册了一个路由刷新监听器,默认实现是zuulrefreshlistener.class,这个是我们动态路由的关键 @bean public applicationlistener<applicationevent> zuulrefreshrouteslistener() { return new zuulrefreshlistener(); } @bean @conditionalonmissingbean(name = "zuulservlet") public servletregistrationbean zuulservlet() { servletregistrationbean servlet = new servletregistrationbean(new zuulservlet(), this.zuulproperties.getservletpattern()); // the whole point of exposing this servlet is to provide a route that doesn't // buffer requests. servlet.addinitparameter("buffer-requests", "false"); return servlet; } // 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() { return new sendresponsefilter(); } @bean public senderrorfilter senderrorfilter() { return new senderrorfilter(); } @bean public sendforwardfilter sendforwardfilter() { return new sendforwardfilter(); } @configuration protected static class zuulfilterconfiguration { @autowired private map<string, zuulfilter> filters; @bean public zuulfilterinitializer zuulfilterinitializer() { return new zuulfilterinitializer(this.filters); } } //上面提到的路由刷新监听器 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); } } } } }
我们要解决动态路由的难题,第一步就得理解路由定位器的作用。
很失望,因为从接口关系来看,spring考虑到了路由刷新的需求,但是默认实现的simpleroutelocator没有实现refreshableroutelocator接口,看来我们只能借鉴discoveryclientroutelocator去改造simpleroutelocator使其具备刷新能力。
public interface refreshableroutelocator extends routelocator { void refresh(); }
discoveryclientroutelocator比simpleroutelocator多了两个功能,第一是从discoveryclient(如eureka)发现路由信息,之前的架构图已经给大家解释清楚了,我们不想使用eureka这种侵入式的网关模块,所以忽略它,第二是实现了refreshableroutelocator接口,能够实现动态刷新。
对simpleroutelocator.class的源码加一些注释,方便大家阅读:
public class simpleroutelocator implements routelocator { //配置文件中的路由信息配置 private zuulproperties properties; //路径正则配置器,即作用于path:/books/** private pathmatcher pathmatcher = new antpathmatcher(); private string dispatcherservletpath = "/"; private string zuulservletpath; private atomicreference<map<string, zuulroute>> routes = new atomicreference<>(); public simpleroutelocator(string servletpath, zuulproperties properties) { this.properties = properties; if (servletpath != null && stringutils.hastext(servletpath)) { this.dispatcherservletpath = servletpath; } this.zuulservletpath = properties.getservletpath(); } //路由定位器和其他组件的交互,是最终把定位的routes以list的方式提供出去,核心实现 @override public list<route> getroutes() { if (this.routes.get() == null) { this.routes.set(locateroutes()); } list<route> values = new arraylist<>(); for (string url : this.routes.get().keyset()) { zuulroute route = this.routes.get().get(url); string path = route.getpath(); values.add(getroute(route, path)); } return values; } @override public collection<string> getignoredpaths() { return this.properties.getignoredpatterns(); } //这个方法在网关产品中也很重要,可以根据实际路径匹配到route来进行业务逻辑的操作,进行一些加工 @override public route getmatchingroute(final string path) { if (log.isdebugenabled()) { log.debug("finding route for path: " + path); } 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()); } string adjustedpath = adjustpath(path); zuulroute route = null; if (!matchesignoredpatterns(adjustedpath)) { for (entry<string, zuulroute> entry : this.routes.get().entryset()) { string pattern = entry.getkey(); log.debug("matching pattern:" + pattern); if (this.pathmatcher.match(pattern, adjustedpath)) { route = entry.getvalue(); break; } } } if (log.isdebugenabled()) { log.debug("route matched=" + route); } return getroute(route, adjustedpath); } private route getroute(zuulroute route, string path) { if (route == null) { return null; } string targetpath = path; string prefix = this.properties.getprefix(); if (path.startswith(prefix) && this.properties.isstripprefix()) { targetpath = path.substring(prefix.length()); } if (route.isstripprefix()) { int index = route.getpath().indexof("*") - 1; if (index > 0) { string routeprefix = route.getpath().substring(0, index); targetpath = targetpath.replacefirst(routeprefix, ""); prefix = prefix + routeprefix; } } boolean retryable = this.properties.getretryable(); if (route.getretryable() != null) { retryable = route.getretryable(); } return new route(route.getid(), targetpath, route.getlocation(), prefix, retryable, route.iscustomsensitiveheaders() ? route.getsensitiveheaders() : null); } //注意这个类并没有实现refresh接口,但是却提供了一个protected级别的方法,旨在让子类不需要重复维护一个private atomicreference<map<string, zuulroute>> routes = new atomicreference<>();也可以达到刷新的效果 protected void dorefresh() { this.routes.set(locateroutes()); } //具体就是在这儿定位路由信息的,我们之后从数据库加载路由信息,主要也是从这儿改写 /** * compute a map of path pattern to route. the default is just a static map from the * {@link zuulproperties}, but subclasses can add dynamic calculations. */ protected map<string, zuulroute> locateroutes() { linkedhashmap<string, zuulroute> routesmap = new linkedhashmap<string, zuulroute>(); for (zuulroute route : this.properties.getroutes().values()) { routesmap.put(route.getpath(), route); } return routesmap; } 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; } private string adjustpath(final string path) { string adjustedpath = path; if (requestutils.isdispatcherservletrequest() && stringutils.hastext(this.dispatcherservletpath)) { if (!this.dispatcherservletpath.equals("/")) { adjustedpath = path.substring(this.dispatcherservletpath.length()); log.debug("stripped dispatcherservletpath"); } } else if (requestutils.iszuulservletrequest()) { if (stringutils.hastext(this.zuulservletpath) && !this.zuulservletpath.equals("/")) { adjustedpath = path.substring(this.zuulservletpath.length()); log.debug("stripped zuulservletpath"); } } else { // do nothing } log.debug("adjustedpath=" + path); return adjustedpath; } }
重写过后的自定义路由定位器如下:
public class customroutelocator extends simpleroutelocator implements refreshableroutelocator{ public final static logger logger = loggerfactory.getlogger(customroutelocator.class); private jdbctemplate jdbctemplate; private zuulproperties properties; public void setjdbctemplate(jdbctemplate jdbctemplate){ this.jdbctemplate = jdbctemplate; } public customroutelocator(string servletpath, zuulproperties properties) { super(servletpath, properties); this.properties = properties; logger.info("servletpath:{}",servletpath); } //父类已经提供了这个方法,这里写出来只是为了说明这一个方法很重要!!! // @override // protected void dorefresh() { // super.dorefresh(); // } @override public void refresh() { dorefresh(); } @override protected map<string, zuulroute> locateroutes() { linkedhashmap<string, zuulroute> routesmap = new linkedhashmap<string, zuulroute>(); //从application.properties中加载路由信息 routesmap.putall(super.locateroutes()); //从db中加载路由信息 routesmap.putall(locateroutesfromdb()); //优化一下配置 linkedhashmap<string, zuulroute> values = new linkedhashmap<>(); for (map.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; } private map<string, zuulroute> locateroutesfromdb(){ map<string, zuulroute> routes = new linkedhashmap<>(); list<zuulroutevo> results = jdbctemplate.query("select * from gateway_api_define where enabled = true ",new beanpropertyrowmapper<>(zuulroutevo.class)); for (zuulroutevo result : results) { if(org.apache.commons.lang3.stringutils.isblank(result.getpath()) || org.apache.commons.lang3.stringutils.isblank(result.geturl()) ){ continue; } zuulroute zuulroute = new zuulroute(); try { org.springframework.beans.beanutils.copyproperties(result,zuulroute); } catch (exception e) { logger.error("=============load zuul route info from db with error==============",e); } routes.put(zuulroute.getpath(),zuulroute); } return routes; } public static class zuulroutevo { /** * the id of the route (the same as its map key by default). */ private string id; /** * the path (pattern) for the route, e.g. /foo/**. */ private string path; /** * the service id (if any) to map to this route. you can specify a physical url or * a service, but not both. */ private string serviceid; /** * a full physical url to map to the route. an alternative is to use a service id * and service discovery to find the physical address. */ private string url; /** * flag to determine whether the prefix for this route (the path, minus pattern * patcher) should be stripped before forwarding. */ private boolean stripprefix = true; /** * flag to indicate that this route should be retryable (if supported). generally * retry requires a service id and ribbon. */ private boolean retryable; private boolean enabled; public string getid() { return id; } public void setid(string id) { this.id = id; } public string getpath() { return path; } public void setpath(string path) { this.path = path; } public string getserviceid() { return serviceid; } public void setserviceid(string serviceid) { this.serviceid = serviceid; } public string geturl() { return url; } public void seturl(string url) { this.url = url; } public boolean isstripprefix() { return stripprefix; } public void setstripprefix(boolean stripprefix) { this.stripprefix = stripprefix; } public boolean getretryable() { return retryable; } public void setretryable(boolean retryable) { this.retryable = retryable; } public boolean getenabled() { return enabled; } public void setenabled(boolean enabled) { this.enabled = enabled; } } }
配置这个自定义的路由定位器:
@configuration public class customzuulconfig { @autowired zuulproperties zuulproperties; @autowired serverproperties server; @autowired jdbctemplate jdbctemplate; @bean public customroutelocator routelocator() { customroutelocator routelocator = new customroutelocator(this.server.getservletprefix(), this.zuulproperties); routelocator.setjdbctemplate(jdbctemplate); return routelocator; } }
现在容器启动时,就可以从数据库和配置文件中一起加载路由信息了,离动态路由还差最后一步,就是实时刷新,前面已经说过了,默认的zuulconfigure已经配置了事件监听器,我们只需要发送一个事件就可以实现刷新了。
public class refreshrouteservice { @autowired applicationeventpublisher publisher; @autowired routelocator routelocator; public void refreshroute() { routesrefreshedevent routesrefreshedevent = new routesrefreshedevent(routelocator); publisher.publishevent(routesrefreshedevent); } }
具体的刷新流程其实就是从数据库重新加载了一遍,有人可能会问,为什么不自己是手动重新加载locator.dorefresh?非要用事件去刷新。这牵扯到内部的zuul内部组件的工作流程,不仅仅是locator本身的一个变量,具体想要了解的还得去看源码。
到这儿我们就实现了动态路由了,所以的实例代码和建表语句我会放到github上,下载的时候记得给我star qaq !!!
链接:
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。