评论

收藏

[Java] springcloud Zuul动态路由的实现

编程语言 编程语言 发布于:2021-10-07 16:51 | 阅读数:339 | 评论:0

这篇文章主要介绍了springcloud Zuul动态路由的实现,详细的介绍了什么是Zuu及其动态路由的实现,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
前言
zuul 是netflix 提供的一个开源组件,致力于在云平台上提供动态路由,监控,弹性,安全等边缘服务的框架。也有很多公司使用它来作为网关的重要组成部分,碰巧今年公司的架构组决定自研一个网关产品,集动态路由,动态权限,限流配额等功能为一体,为其他部门的项目提供统一的外网调用管理,最终形成产品(这方面阿里其实已经有成熟的网关产品了,但是不太适用于个性化的配置,也没有集成权限和限流降级)。
不过这里并不想介绍整个网关的架构,而是想着重于讨论其中的一个关键点,并且也是经常在交流群中听人说起的:动态路由怎么做?
再阐释什么是动态路由之前,需要介绍一下架构的设计。
传统互联网架构图
DSC0000.png

上图是没有网关参与的一个最典型的互联网架构(本文中统一使用book代表应用实例,即真正提供服务的一个业务系统)
加入eureka的架构图
DSC0001.jpg

book注册到eureka注册中心中,zuul本身也连接着同一个eureka,可以拉取book众多实例的列表。服务中心的注册发现一直是值得推崇的一种方式,但是不适用与网关产品。因为我们的网关是面向众多的其他部门的已有或是异构架构的系统,不应该强求其他系统都使用eureka,这样是有侵入性的设计。
最终架构图
DSC0002.jpg

要强调的一点是,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);
  }
   }
  }
 
 }
 
}
我们要解决动态路由的难题,第一步就得理解路由定位器的作用。
DSC0003.jpg

很失望,因为从接口关系来看,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 !!!
链接:https://github.com/lexburner/zuul-gateway-demo
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持CodeAE代码之家
原文链接:https://blog.csdn.net/u013815546/article/details/68944039

关注下面的标签,发现更多相似文章