- 推荐序一
- 推荐序二
- 推荐序三
- 推荐语
- 前言
- 第1章 基础知识
- 第2章 微服务构建:Spring Boot
- 第3章 服务治理:Spring Cloud Eureka
- 第4章 客户端负载均衡:Spring Cloud Ribbon
- 第5章 服务容错保护:Spring Cloud Hystrix
- 第6章 声明式服务调用:Spring Cloud Feign
- 第7章 API网关服务:Spring Cloud Zuul
- 第8章 分布式配置中心:Spring Cloud Config
- 第9章 消息总线:Spring Cloud Bus
- 第10章 消息驱动的微服务:Spring Cloud Stream
- 附录 A Starter POMs
- 后记
动态加载
在微服务架构中,由于API网关服务担负着外部访问统一入口的重任,它同其他应用不同,任何关闭应用和重启应用的操作都会使系统对外服务停止,对于很多7×24小时服务的系统来说,这样的情况是绝对不被允许的。所以,作为最外部的网关,它必须具备动态更新内部逻辑的能力,比如动态修改路由规则、动态添加/删除过滤器等。
通过Zuul实现的API网关服务当然也具备了动态路由和动态过滤器的能力。我们可以在不重启API网关服务的前提下,为其动态修改路由规则和添加或删除过滤器。下面我们分别来看看如何通过Zuul来实现动态API网关服务。
动态路由
通过之前对请求路由的详细介绍,我们可以发现对于路由规则的控制几乎都可以在配置文件application.properties或application.yaml中完成。既然这样,对于如何实现Zuul的动态路由,我们很自然地会将它与Spring Cloud Config的动态刷新机制联系到一起。只需将API网关服务的配置文件通过Spring Cloud Config连接的Git仓库存储和管理,我们就能轻松实现动态刷新路由规则的功能。
在介绍如何具体实现API网关服务的动态路由之前,我们首先需要一个连接到Git仓库的分布式配置中心config-server应用。如果还没有搭建过分布式配置中心的话,建议先阅读第8章的内容,对分布式配置中心的运作机制有一个基础的了解,并构建一个config-server应用,以配合完成下面的内容。
在具备了分布式配置中心之后,为了方便理解,我们重新构建一个API网关服务,该服务的配置中心不再配置于本地工程中,而是从config-server中获取,构建过程如下所示。
- 创建一个基础的Spring Boot工程,命名为api-gateway-dynamic-route。
- 在pom.xml中引入对zuul、eureka和config的依赖,具体内容如下:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.3.7.RELEASE</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Brixton.SR5</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
- 在/resource目录下创建配置文件bootstrap.properties,并在该文件中指定 config-server 和 eureka-server 的具体地址,以获取应用的配置文件和实现服务注册与发现。
spring.application.name=api-gateway
server.port=5556
spring.cloud.config.uri=http://localhost:7001/
eureka.client.serviceUrl.defaultZone=http://localhost:1111/eureka/
- 创建用来启动API网关的应用主类。这里我们需要使用@RefreshScope注解来将Zuul的配置内容动态化,具体实现如下:
@EnableZuulProxy
@SpringCloudApplication
public class Application {
public static void main(String[]args){
new SpringApplicationBuilder(Application.class).web(true).run(args);
}
@Bean
@RefreshScope
@ConfigurationProperties("zuul")
public ZuulProperties zuulProperties(){
return new ZuulProperties();
}
}
- 在完成了所有程序相关的编写之后,我们还需要在Git仓库中增加网关的配置文件,取名为api-gateway.properties。在配置文件中,我们为API 网关服务预定义以下路由规则,比如:
zuul.routes.service-a.path=/service-a/**
zuul.routes.service-a.serviceId=hello-service
zuul.routes.service-b.path=/service-b/**
zuul.routes.service-b.url=http://localhost:8001/
对于 API 网关服务在 Git 仓库中的配置文件名称完全取决于网关应用配置文件bootstrap.properties 中 spring.application.name 属性的配置值。由于本章我们主要介绍API网关的使用,所以这里省略了关于label和profile的配置,默认会使用master分支和default配置文件,更细致的配置方式读者可查阅第8章介绍的内容。
测试与验证
在完成了上述内容之后,我们可以将 config-server、eureka-server、api-gateway-dynamic-route 以及配置文件中路由规则指向的具体服务,比如hello-service 启动起来。此时,在 API 网关应用 api-gateway-dynamic-route的控制台中,我们可以看到它输出了从config-server中获取配置文件过程的日志信息,根据这些信息,可以判断获取配置信息的路径等内容是否正确。具体输出内容如下所示:
c.c.c.ConfigServicePropertySourceLocator : Fetching config from server at:
http://localhost:7001/
c.c.c.ConfigServicePropertySourceLocator : Located environment: name=api-gateway,
profiles=[default],label=master,version=1dab6126ca2972c5409fcb089934b057cf2bf77d
b.c.PropertySourceBootstrapConfiguration : Located property source:
CompositePropertySource[name='configService',propertySources=[MapPropertySource
[name='overrides'],MapPropertySource
[name='http://git.oschina.net/didispace/SpringCloud-Learning/spring_cloud_in_action
/config-repo/api-gateway.properties']]]
com.didispace.Application : No active profile set,falling back to
default profiles: default
在 api-gateway-dynamic-route 启动完成之后,可以通过对 API 网关服务调用/routes接口来获取当前网关上的路由规则,根据上述配置我们可以得到如下返回信息:
{
"/service-a/**": "hello-service",
"/service-b/**": "http://localhost:8001/"
}
我们可以先尝试着通过上述路由规则访问一下对应路由服务提供的接口,验证当前的路由规则是否生效。接着,我们开始尝试动态修改这些路由规则,只需要以下两步。
- 修改Git仓库中的api-gateway.properties配置文件,比如,将service-a路由规则的 path 修改为/aaa/**,并把 service-b 的路由规则从 url 修改为serviceId方式。具体配置如下:
zuul.routes.service-a.path=/aaa/**
zuul.routes.service-a.serviceId=hello-service
zuul.routes.service-b.path=/service-b/**
zuul.routes.service-b.url=hello-service
- 在修改完配置文件之后,将修改内容推送到远程仓库。然后,通过向api-gateway-dynamic-route的/refresh接口发送POST请求来刷新配置信息。当配置文件有修改的时候,该接口会返回被修改的属性名称,根据上面的修改,我们会得到如下返回信息:
[
"zuul.routes.service-b.serviceId",
"zuul.routes.service-b.url",
"zuul.routes.service-a.path"
]
由于修改了service-b路由的url方式为serviceId方式,相当于删除了url参数配置,增加了 serviceId 参数配置,所以这里会出现两条关于 service-b路由的变更信息。
到这里,我们就已经完成了路由规则的动态刷新,可以继续通过 API 网关服务的/routes接口来查看当前的所有路由规则,该接口将返回如下信息:
{
"/aaa/**": "hello-service",
"/service-b/**": "hello-service"
}
从/routes接口的信息中我们可以看到,路由service-a的匹配表达式被修改成了/aaa/**,而路由service-b的映射目标从原来的物理地址http://localhost:8001/修改成了hello-service服务。
通过本节对动态路由加载内容的介绍,我们可以看到,通过Zuul构建的API网关服务对于动态路由的实现总体上来说还是非常简单的。美中不足的一点是,Spring Cloud Config并没有UI管理界面,我们不得不通过Git客户端来进行修改和配置,所以在使用的时候并不是特别方便,当然有条件的团队可以自己开发一套UI界面来帮助管理这些路由规则。
动态过滤器
既然通过 Zuul 构建的 API 网关服务能够轻松地实现动态路由的加载,那么对于 API网关服务的另外一大重要功能—请求过滤器的动态加载自然也不能放过,只是对于请求过滤器的动态加载与请求路由的动态加载在实现机制上会有所不同。这个不难理解,通过之前介绍的请求路由和请求过滤的示例,我们可以看到请求路由通过配置文件就能实现,而请求过滤则都是通过编码实现。所以,对于实现请求过滤器的动态加载,我们需要借助基于JVM实现的动态语言的帮助,比如Groovy。
下面,我们将通过一个简单的示例来演示如何构建一个具备动态加载Groovy过滤器能力的API网关服务的详细步骤。
- 创建一个基础的Spring Boot工程,命名为api-gateway-dynamic-filter。
- 在pom.xml中引入对zuul、eureka和groovy的依赖,具体内容如下:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.3.7.RELEASE</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Brixton.SR5</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
- 在/resource目录下创建配置文件application.properties,并在该文件中设置API网关服务的应用名和服务端口号,以及指定eureka-server的具体地址。同时,再配置一个用于测试的路由规则,我们可以用之前章节实现的 helloservice为例。具体配置内容如下:
spring.application.name=api-gateway
server.port=5555
eureka.client.serviceUrl.defaultZone=http://localhost:1111/eureka/
zuul.routes.hello.path=/hello-service/**
zuul.routes.hello.serviceId=hello-service
到这里一个基础的API网关服务已经构建完成,下面我们来为它增加动态加载过滤器的功能。
- 为了方便使用,我们先自定义一些用来配置动态加载过滤器的参数,并将它们的配置值加入到application.properties中,比如:
zuul.filter.root=filter
zuul.filter.interval=5
其中,zuul.filter.root用来指定动态加载的过滤器存储路径;zuul.filter.interval用来配置动态加载的间隔时间,以秒为单位。
- 创建用来加载自定义属性的配置类,命名为FilterConfiguration,具体内容如下:
@ConfigurationProperties("zuul.filter")
public class FilterConfiguration {
private String root;
private Integer interval;
public String getRoot(){
return root;
}
public void setRoot(String root){
this.root=root;
}
public Integer getInterval(){
return interval;
}
public void setInterval(Integer interval){
this.interval=interval;
}
}
- 创建应用启动主类,并在该类中引入上面定义的FilterConfiguration配置,并创建动态加载过滤器的实例。具体内容如下:
@EnableZuulProxy
@EnableConfigurationProperties({FilterConfiguration.class})
@SpringCloudApplication
public class Application {
public static void main(String[]args){
new SpringApplicationBuilder(Application.class).web(true).run(args);
}
@Bean
public FilterLoader filterLoader(FilterConfiguration filterConfiguration){
FilterLoader filterLoader=FilterLoader.getInstance();
filterLoader.setCompiler(new GroovyCompiler());
try {
FilterFileManager.setFilenameFilter(new GroovyFileFilter());
FilterFileManager.init(
filterConfiguration.getInterval(),
filterConfiguration.getRoot()+"/pre",
filterConfiguration.getRoot()+"/post");
} catch(Exception e){
throw new RuntimeException(e);
}
return filterLoader;
}
}
至此,我们就已经完成了为基础的API网关服务增加动态加载过滤器的能力。根据上面的定义,API 网关应用会每隔5秒,从 API 网关服务所在位置的 filter/pre 和filter/post目录下获取Groovy定义的过滤器,并对其进行编译和动态加载使用。对于动态加载的时间间隔,可通过zuul.filter.interval参数来修改。而加载过滤器实现类的根目录可通过zuul.filter.root调整根目录的位置来修改,但是对于根目录的子目录,这里写死了读取/pre和/post目录,实际使用的时候读者可以做进一步扩展。
在完成上述构建之后,我们可以将涉及的服务,比如 eureka-server、hello-service以及上述实现的API网关服务都启动起来。在没有加入任何自定义过滤器的时候,根据路由规则定义,我们可以尝试向 API 网关服务发起请求:http://localhost:5555/hello-service/hello,如果配置正确,该请求会被API网关服务路由到hello-service上,并返回输出Hello World。
接下来,我们可以在filter/pre和filter/post目录下,用Groovy来创建一些过滤器来看看实际效果,举例如下。
- 在filter/pre目录下创建一个pre类型的过滤器,命名为PreFilter.groovy。
由于pre类型的过滤器在请求路由前执行,通常用来做一些签名校验的功能,所以我们可以在过滤器中输出一些请求相关的信息,比如下面的实现:
class PreFilter extends ZuulFilter {
Logger log=LoggerFactory.getLogger(PreFilter.class)
@Override
String filterType(){
return "pre"
}
@Override
int filterOrder(){
return 1000
}
@Override
boolean shouldFilter(){
return true
}
@Override
Object run(){
HttpServletRequest request=RequestContext.getCurrentContext().getRequest()
log.info("this is a pre filter: Send {} request to {}",
request.getMethod(),
request.getRequestURL().toString())
return null
}
}
在加入了该过滤器之后,不需要重启API网关服务,只需要稍等几秒就会生效。我们可以继续尝试向 API 网关服务发起请求:http://localhost:5555/helloservice/hello,此时在控制台中可以看到 PreFilter.groovy 过滤器中定义的日志信息,具体如下:
com.didispace.filter.pre.PreFilter : this is a pre filter: Send GET request to
http://localhost:5555/ddd/hello
- 在filter/post目录下创建一个post类型的过滤器,命名为PostFilter.groovy。
由于 post 类型的过滤器在请求路由返回后执行,我们可以进一步对这个结果做一些处理,对微服务返回的信息做一些加工。比如下面的实现:
class PostFilter extends ZuulFilter{
Logger log=LoggerFactory.getLogger(PostFilter.class)
@Override
String filterType(){
return "post"
}
@Override
int filterOrder(){
return 2000
}
@Override
boolean shouldFilter(){
return true
}
@Override
Object run(){
log.info("this is a post filter: Receive response")
HttpServletResponse response=RequestContext.getCurrentContext().getResponse()
response.getOutputStream().print(",I am zhaiyongchao")
response.flushBuffer()
}
}
在加入了该过滤器之后,我们也不需要重启API网关服务,稍等几秒后就可以尝试向API 网关服务发起请求:http://localhost:5555/hello-service/hello,此时不仅可以在控制台中看到 PostFilter.groovy 过滤器中定义的日志输出信息,也可以从请求返回的内容发现过滤器的效果,该接口返回的内容不再是Hello World,而是经过加工处理后的Hello World,I am zhaiyongchao。
通过本节对动态过滤器加载的内容的介绍,可以看到,API 网关服务的动态过滤器功能可以帮助我们增强API网关的持续服务能力,对于网关中的处理逻辑维护也变得更为灵活,不仅可以动态地实现请求校验,还可以动态地实现对请求内容的干预。但是,目前版本下的动态过滤器还是一个半成品,从 org.springframework.cloud.netflix.zuul.ZuulFilterInitializer 的源码中我们也可以看到,对于动态过滤器的加载是被注释掉的,并且被标注了 TODO 状态。不过,目前在实际使用过程中,对于处理一些简单的常用过滤功能还是没有什么问题的,只是需要注意一些已知的问题并避开这些情况来使用即可。比如,在使用Groovy定义动态过滤器的时候,删除Groovy文件并不能从当前运行的 API 网关中移除这个过滤器,所以如果要移除的话可以通过修改 Groovy 过滤器的shouldFilter 返回 false。另外还需要注意一点,目前的动态过滤器是无法直接注入API 网关服务的 Spring 容器中加载的实例来使用的,比如,我们是无法直接通过注入RestTemplate等实例,在动态过滤器中对各个微服务发起请求的。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论