spring-cloud-zuul服务网关
API网关服务:Spring Cloud Zuul:
Zuul包含了对请求的路由和过滤两个最主要的功能:
其中路由功能负责将外部请求转发到具体的微服务实例上,是实现外部访问统一入口的基础,类似于保安的职能,而过滤器功能则负责对请求的处理过程进行干预,是实现请求校验,服务聚合等功能的基础,Zuul和Eureka进行整合,将Zuul自身注册为Eureka服务治理下的应用,同时从Eureka中获取其他的微服务消息,也即以后访问微服务是通过Zuul跳转后获得,最终Zuul服务还是会注册进Eureka。提供 服务代理 ,路由,过滤三大功能。
从以上这张架构图中,我们可以看到所有的请求都必须通过API GateWay服务才能到达后面的服务,这就是Zuul所需要承担起来的责任。可见他的存在是很重要的
构建网关:
1.新建 boot 工程 修改pom文件
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<spring-cloud.version>Finchley.SR3</spring-cloud.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<!-- SpringCloud 所有子项目 版本集中管理. 统一所有SpringCloud依赖项目的版本依赖-->
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-zuul -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zuul</artifactId>
<version>1.4.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin><!-- SpringBoot 项目打jar包的Maven插件 -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
2.修改application.yml
server:
port: 9110
spring:
application:
name: cloud-zuul-getway #服务注册到Eureka上使用的名称
eureka:
client:
service-url: # 集群情况下如下,如果是单机版,只需要配置单机版Eureka地址
defaultZone: http://localhost:7001/eureka/,http://localhost:7002/eureka/
instance:
instance-id: cloud-zuul-getway-9110
prefer-ip-address: true #访问路径显示IP地址
info: # 在Eureka上点击服务时会跳转到个404页面,可配置这里让他跳转到服务简介的一个页面,信息如下配置
app.name: wuzz
company.name: www.wuzz.com
build.artifactId: cloud-zuul-getway
build.version: 1.0
zuul:
# prefix: /wuzz #统一的公共前缀
# ignored-services: feign-server # "*"所有忽略原有服务名
routes:
feign:
url: http://localhost:9012/
path: /fegin/**
3.修改主启动类
@SpringBootApplication
@EnableZuulProxy
@EnableDiscoveryClient
public class ZuulApp {
private final static Logger log = LoggerFactory.getLogger(ZuulApp.class);
public static void main(String[] args) {
SpringApplication.run(ZuulApp.class,args);
log.info("服务启动成功");
}
}
启动Eureka集群,再启动 一个微服务服务提供者,再启动本开启了Zuul服务的微服务。这样就配置好了。
传统路由方式:
像上面的 application.yml里面配置的,就是传统路由的方式:单实例
zuul:
# prefix: /wuzz #统一的公共前缀
# ignored-services: feign-server # "*"所有忽略原有服务名
routes:
feign:
url: http://localhost:9012/
path: /fegin/**
这个配置代表着,访问zuul服务路径 :http://localhost:9110/fegin/hello ,会转发到 http://localhost:9012/hello 这个路径。
多实例的配置如下:
zuul:
routes:
feign:
path: /fegin/**
serviceId: fegin-server
ribbon:
eureka:
enabled: false
fegin-server:
ribbon:
listOfServers: http://localhost:8080/,http://localhost:8081/
该 配 置 实 现了对 符 合 /fegin/** 规 则 的 请 求 路 径 转 发 到http://localhost:8080/和http://localhost:8081/两个实例地址的路由规则。 它的配置 方式与服务 路由的配置 方式一 样,都采用了zuul.routes.< route>.path与zuul.routes. < route>.serviceId参数对的映射方式,只是这 里的 serviceId 是由用户手工命名 的 服 务 名 称 , 配 合 ribbon.listOfServers 参数实现服务与实例的维护。由于存在多个实例,API网关在进行路由转发时需要实现负载均衡策略, 于是这里还需要Spring CloudRibbon的配合。由千在Spring Cloud Zuul中自带了对Ribbon的依赖, 所以我们只需做 一 些配置即可,比如上面示例中关 于Ribbon的各个配置,它们的具体作用如下所示
- ribbon.eureka.enabled:由于zuul.routes. < route>.serviceId指定的是服务名称,默认清况下Ribbon会根据服务发现机制来 获取配置服务名对应的实例清单。 但是,该示例并没有整合类似Eureka之类的服务治理框架,所以需要将该参数设置为false, 否则配置 的serviceId获取不到对应实例的 清单。
- fegin-server.ribbon.listOfServers: 该 参数内容与 zuul.routes.<route>.serviceId 的配置相对 应, 开头的 fegin-server 对应了serviceId的值, 这两个参数的配置相当于在该应用内部手工维护了服务与实例的 对应关系。
面向服务的路由:
很显然, 传统 路由的配置方式对于我们来说并不友好, 它同样需要运维人员花费大量的时间来维护各个路由 path与url的关系 。 为了解决这个问题, SpringCloud Zuul实现了与Spring Cloud Eureka的无缝整合, 我们可以让路由的path不是映射具体的url, 而是让它映射到某个具体的服务 , 而具体的url则交给Eureka的服务发现机制去自动维护, 我们称这类路由为面向服务的路由。 在Zuul中使用服务 路由也同样简单, 只需做下面这些配置。
1.添加依赖:这个我们上面已经加进去了
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
2.修改配置:
zuul:
routes:
feign:
serviceId: feign-server
path: /fegin/**
这样子如果我们访问 http://localhost:9110/fegin/hello ,那么会转发到服务名为 feign-server 的节点上。
对于面向服务的路由配置,除 了使用path与serviceId映射的配置方式 之外,还有一 种更简洁的配置方式:zuul.routes.<serviceid>=<path>, 其中<serviceid>用来指定路由的具体服务名, <path>用来配置匹配的请求表达式。比如下面的例子, 它的路由规则等价于上面通过path与serviceId组合使用的配置方式。
zuul:
routes:
feign:
feign-server: /fegin/**
由于默认情况下所有Eureka上的服务都会被Zuul自动地创建映射关系来进行路由,这会使得 一 些我们不希望对外开放的服务也可能被外部访问到。 这个时候, 我们可以使用zuul.ignored-services参数来设置 一 个服务名匹配表达式来定义不自动创建路由的规则。 Zuul在自动创建服务路由的时候会根据该表达式来进行判断, 如果服务名匹配表达式 , 那 么 Zuul 将跳过 该 服 务 , 不 为 其创 建 路 由 规 则 。 比如, 设 置 为zuul.ignored-services=*的时候,Zuul将对所有的服务都不自动创建路由规则。 在这种情况下,我们就要在配置文件中逐个为需要路由的服务添加映射规则(可以使用path与 serviceId组合的配置方式, 也可使用更简洁的 zuul.routes.<serviceid>=<path>配置方式),只有在配置文件中出现的映射规则会被创建路由,而从Eureka中获取的其他服务,Zuul将不会再为它们创建路由规则。
自定义路由映射规则
我们在构建微服务系统进行业务逻辑开发的时候, 为了兼容外部不同版本的客户端程序(尽量不强迫用户升级客户端),一 般都会采用开闭原则来进行设计与开发。 这使得系统在迭代过程中, 有时候会需要我们为 一 组互相配合的微服务定义 一 个版本标识来方便管理它们的版本关系,根据这个 标识我们可以 很容易地知道这些服务需要 一 起启动并配合使用。比如可 以 采 用 类 似这 样 的 命 名: userservice-v1、 userservice-v2、orderservice-v1、 orderservice-v2。 默认情况下,Zuul自动为服务创建的路由表达式会采用服务名作为前缀, 比如针对上面的userservice-v1 和userservice-v2,它会产生/userservice-v1 和/userservice-v2两个路径表达式来映射,但是这样生成出来的表达式规则较为单 一 , 不利于通过路径规则来进行管理。 通常的做法是为这些不同版 本的 微 服 务 应 用 生 成以版 本代号 作 为 路 由 前 缀 定 义 的 路 由 规 则 , 比如/v1/userservice/ 。 这时候, 通过这样具有版本号前缀的 URL 路径,我们就可以很容易地通过路径表达式来归类和管理这些具有版本信息的微服务了。针对上面所述的需求,如果我们的各个微服务应用都遵循了类似userservice-v1这样的命名规则,通过-分隔的规范来定义服务名和服务版本标识的话,那么,我们可以使用Zuul中自定义服务与路由映射 关系的功能,来实现为符合上述规则的微服务自动化地创建类似/v1/userservice/** 的路由匹配规则。 实现步骤非常简单, 只需在 API 网关程序中, 增加如下Bean的创建即可:
@Bean
public PatternServiceRouteMapper serviceRouteMapper () {
return new PatternServiceRouteMapper(
"(?<name> A .+)-(?<version>v.+$)",
"${version}/${name}");
}
路径匹配:
不论是使用传统路由的配置方式还是服务路由的配置方式,我们都需要为每个路由规则定义匹配表达式, 也就是上面所说的path参数。 在Zuul中, 路由匹配的路径表达式采用了Ant风格定义。Ant风格的路径表达式使用起来非常简单,它 一 共有下面这三种通配符。
- ? :匹配任意单 个字符
- * :匹配任意数量的字符
- ** :匹配任意数址的字符, 支待多级目录
例如:
- /user-service/? 它可以匹 配/user-service/之后拼接一个任务字符的路径, 比如/user-service/a 、 /user-service/b 、 /user-service/c
- /user-service/* 它可以匹 配/user-service/之后拼接任 意字 符的路径, 比如/user-service/a 、 /user-service/aaa 、 /user-service/bbb。 但是它无法匹配/user-service/a/b
- /user-service/** 它可以匹 配/user-service/*包含的内容之外, 还可以匹 配形如/user-service/a/b的多级目录路径
那么如果一 个URL 路径可能会被多个不同路由的表达式匹配上。例如如下这么个配置:
zuul.routes.user-service.path=/user-service/**
zuul.routes.user-service.serviceId=user-service
zuul.routes.user-service-ext.path=/user-service/ext/**
zuul.routes.user-service-ext.serviceId=user-service-ext
此时, 调用 user-service-ext 服务的 URL 路径实际上会同时被 /userservice/** 和 /user-service/ext/** 两个表达式所匹配。 在逻辑上, API 网关服务需要优先选择 /user-service/ext/** 路由,然后再匹配 /user-service/** 路由才能实现上述需求。但是如果使用上面的配置方式,实际上是无法保证这样的路由优先顺序的。从下面的路由匹配算法中, 我们可以看到它在使用路由规则匹配请求路径的时候是通过线性遍历的方式,在请求路径获取到第 一 个匹配的路由规则之后就返回并结束匹配过程。所以当存在多个匹配的路由规则时, 匹配结果完全取决于路由规则的保存顺序。定位到 SimpleRouteLocator
protected ZuulRoute getZuulRoute(String adjustedPath) {
if (!matchesIgnoredPatterns(adjustedPath)) {
for (Entry<String, ZuulRoute> entry : getRoutesMap().entrySet()) {
String pattern = entry.getKey();
log.debug("Matching pattern:" + pattern);
if (this.pathMatcher.match(pattern, adjustedPath)) {
return entry.getValue();
}
}
}
return null;
}
下面所示的代码是基础的路由规则加载算法, 我们可以看到这些路由规则是通过LinkedHashMap保存的, 也就是说, 路由规则的保存是有序的, 而内容的加载是通过遍历配置文件中路由规则依次加入的,所以导致问题的根本原因是对配置文件中内容的读取。还是这个类:
protected Map<String, ZuulRoute> locateRoutes() {
LinkedHashMap<String, ZuulRoute> routesMap = new LinkedHashMap<>();
for (ZuulRoute route : this.properties.getRoutes().values()) {
routesMap.put(route.getPath(), route);
}
return routesMap;
}
所以上面这个例子的正确配置方式就是把更加明确的路径匹配写的更前面,也就是:
zuul:
routes:
user-service-ext:
path: /user-service/ext/**
serviceld: user-service-ext
user-service:
path: /user-service/**
serviceld: user-service
忽略表达式:
通过 path 参数定义的 Ant 表达式已经能够完成 API 网关上的路由规则配置功能,但是为了更细粒度和更为灵活地配置路由规则, Zuul 还提供了一 个忽略表达式参数 zuul.ignored-patterns 。 该参数可以用来设置不希望被 API 网关进行路由的 URL 表达式。如果不希望 /hello 接口被路由, 那么我们可以这样设置:zuul.ignored-patterns = /**/hello/** 。然后, 可以尝试通过网关来访间 fegin-server 的 /hello。虽然该访问路径完全符合 path 参数定义的规则,但是由于该路径符合 zuul.ignored-patterns 参数定义的规则,所以不会被正确路由。 同时,我们在控制台或日志中还能看到没有匹配路由的输出信息
本地跳转
在 Zuul 实现的 API 网关路由功能中, 还支持 forward 形式的服务端跳转配置。 实现方式非常简单,只需通过使用 path 与 url 的配置方式就能完成,通过 url 中使用 forward来指定需要跳转的服务器资源路径。下面的配置实现了两个路由规则, api-a 路由实现了将符合 /api-a/** 规则的请求转发到 http://localhost:8001/; 而 api-b 路由则使用了本地跳转, 它实现了将符合/api-b/** 规则的请求转发到API网关中以 /local 为前缀的请求上,由API网关进行本地处理。 比如, 当 API 网关接收到请求 /api-b/hello, 它符合 api-b 的路由规则,所以该请求会被 API 网关转发到网关的 /local/hello 请求上进行本地处理。
zuul.routes.api-a.path=/api-a/**
zuul.routes.api-a.url=http://localhost:8001/
zuul.routes.api-b.path=/api-b/**
zuul.routes.api-b.url=forward:/local
这里要注意, 由于需要在 API 网关上实现本地跳转,所以相应的我们也需要为本地跳转实现对应的请求接口。
请求过滤:
在实现了请求路由功能之后, 我们的微服务应用提供的接口 就可以通过统一 的 API网关入口被客户端访问到了。 但是, 每个客户端用户请求微服务应用提供的接口时, 它们的访问权限往往都有一 定的限制,系统 并不会将所有的微服务接口都 对它们开放。 然而, 目前的服务路由并没有限制权限这样的功能, 所有请求都会被毫无保留地转发到具体的应用并返回结果,为了 实现对客户端请求的安全校验和权限控制, 最简单和粗暴的方法就是为每个微服务应用都 实现一 套用于校验签名和鉴别权限的过滤器或拦截器。
为了在API网关中实现对客户端请求的校验,我们将继续学习Spring Cloud Zuul 的另外 一 个核心功能:请求过滤。 Zuul 允许开发者在API网关上通过定义过滤器来实现对请求的拦截与过滤,实现的方法非常简单,我们只需要继承 ZuulFilter 抽象类并实现它定义的4个抽象函数就可以完成对请求的拦截和过滤了。
@Component
public class AccessFilter extends ZuulFilter {
private static Logger log = LoggerFactory.getLogger(AccessFilter.class);
@Override
public String filterType() {
// 过滤器的类型, 它决定过滤器在请求的哪个生命周期中执行。 这里
// 定义为pre, 代表会在请求被路由之前执行。
return "pre";
}
@Override
public int filterOrder() {
// 过滤器的执行顺序.当请求在一个阶段中存在多个过滤器时,
// 需要根据该方法返回的值来依次执行。
return 0;
}
@Override
public boolean shouldFilter() {
// 判断该过滤器是否需要被执行。 这里我们直接返回了true,
// 因此该过滤器对所有请求都会生效。
// 实际运用中我们可以利用该函数来指定过滤器的有效范围。
return true;
}
// 过滤器的具体逻辑。 这里我们通过ctx.setSendZuulResponse(false)
// 令zuul过滤该请求, 不对其进行路由, 然后通过 ctx.setResponseStatus
// Code (401)设置了其返回的错误码, 当然也可以进一步优化我们的返回,
// 比如, 通过ctx.setResponseBody(body)对返回的body内容进行编辑等。
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
log.info("send {} request to{}", request.getMethod(),
request.getRequestURL().toString());
Object accessToken = request.getParameter("accessToken");
if (accessToken == null) {
log.warn("access token is empty");
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(401);
return "failed..............";
}
log.info("access token ok");
return "access token ok";
}
}
这样就实现了过滤了。当我们继续访问 http://localhost:9110/fegin/hello 就会报401了。所以我们要 http://localhost:9110/fegin/hello?accessToken=1111 才能访问得到。
通过对Spring Cloud Zuul两个核心功能的介绍, 相信大家已经能够体会到API网关服务对微服务架构的重要性了, 就目前掌握的API网关知识, 我们可以将具体原因总结如下:
- 它作为系统的统一 入口, 屏蔽了系统内部各个微服务的细节。
- 它可以与服务治理框架结合,实现自动化的服务实例维护以及负载均衡的路由转发。
- 它可以实现接口权限校验与微服务业务逻辑的解耦。
- 通过服务网关中的过炖器, 在各生命周期中去校验请求的内容, 将原本在对外服务层做的校验前移, 保证了微服务的无状态性, 同时降低了微服务的测试难度, 让服务本身更集中关注业务逻辑的处理。
Cookie与头信息:
默认情况下, SpringCloud Zuul在请求路由时, 会过滤掉HTTP请求头信息中的一 些敏感 信 息, 防止它们被传 递到下游的外部服 务器。 默 认的 敏感 头 信 息 通 过 zuul.sensitiveHeaders参数定义,包括Cookie、Set-Cookie、Authorization三个属性。所以, 我们在开发Web项目时常用的Cookie在 SpringCloud Zuul网关 中默认是不会传递 的 , 这就会引发一 个常见的问题: 如果我们要将使用了Spring Security、 Shiro等安全框架构建的Web应用通过SpringCloud Zuul构建的网关来进行路由时,由于Cook迳信息无法传递, 我们的Web应用将无法实现登录和鉴权。 为 了解决这个问题, 配置的 方法有很多。
通过设置全局参数为空来覆盖默认值, 具体如下:zuul.sensitiveHeaders =
这种方法并不推荐, 虽然可以实现Cookie的传递, 但是破坏了默认设置的用意。在微服务架构的API网关之内, 对于无状态的RESTful API请求肯定是要远多于这些Web类应用请求的, 甚至还有 一 些架构 设计会 将Web类应用和App客户端一 样都归为API 网关之外的客户端应用。通过指定路由的参数来配置, 方法有下面两种。
#方法一:对指定路由开启自定义敏感头
zuul.routes.<router>.customSensitiveHeaders = true
#方法二:将指定路由的敏感头设置为空
zuul.routes.<router>.sensitiveHeaders =
推荐使用这两种方法 , 仅对指定的Web应用开启对敏感信息的传递,影响范围小, 不至于引起其他服务的信息泄露间题。
重定向问题:
虽然可以通过网关访问登录页面并发起登录请求, 但是登录成功之后, 我们跳转到的页面URL却是 具体Web应用实例的地址, 而不是通过网关的路由地址。原因是由于SpringSecurity或Shiro在登录完成之后,通过重定向的方式跳转到登录后的页面,此时登录后的请求结果状态码为302, 请求响应头信息中的 Location指向了具体的服务实例地址, 而请求头信息中的Host也指向 了具体的服务实例 IP地址和端口。 所以, 该问题的根本原因在于Spring Cloud Zuul在路由请求时,并没有将最初的Host信息设置正确。那么如何解决 这个问题呢?使得网关在进行路由转发前为请求设置Host头信息,以标识最初的服务端请求地址。 具体配置方式如下:
zuul:
add-host-header: true
Hystrix 和 Ribbon 支持:
spring-cloud-starter-zuul依赖,它自身就包含了对 spring-cloud-starter-hys七rix 和 spring-cloud-starter-ribbon模块的依赖, 所以 Zuul天生就拥有线程隔离和断路器的自我保护功能,以及 对服务调用的客户端负载均衡功能。 但是需要注意, 当使用path与url的映射关系来配置路由规则的时候, 对于路由转发的请求不会采用HystrixCommand来包装, 所以 这类路由请求没有线程隔离和断路器的保护,并且也不会有负载均衡的能力。因此,我们在使用Zuul的时候尽量使用path和serviceId的组合来进行配置, 这样不仅可以保证API网关的健壮和稳定,也能用到Ribbon的客户端负载均衡功能。
所以在 zuul服务中可以使用到 ribbon 以及 hystrix 的配置,比如下面这些参数的设置。
- hystrix.command.default.execution.isolation.thread.timeoutinMilliseconds : 该 参 数可以用 来 设 置API 网 关中路 由 转 发 请 求的HystrixCommand 执行超时时间, 单位为毫秒。 当路由转发请求的命令执行时间超过该配置值之后,Hystrix会将该执行命令标记为TIMEOUT并抛出异常
- ribbon.ConnectTimeout: 该参数用来设置路由转发请求的时候, 创建请求连接的超时时间。当ribbon.ConnectTimeout的配置值小于hystrix.command.default.execution.isolation.thread.timeoutlnMilliseconds 配置值的时候, 若出现路由请求出现连接超时, 会自动进行重试路由请求, 如果重试依然失败。如果 ribbon.ConnectTimeo江的配置值大于hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds配置值的时候,当出现路由请求连接超时时, 由于此时对于路由转发的请求命令已经超时, 所以不会进行重试路由请求, 而是直接按请求命令超时处理, 返回TIMEOUT的错误信息。
- ribbon.ReadTimeout: 该参数用来设置路由转发请求的超时时间。 它的处理与ribbon.ConnectTimeout类似, 只是它的超时是对请求连接建立之后的处理时间。 当 ribbon.ReadTimeout的配置值小于 hystrix.command.default.execution.isolation.thread.TimeoutinMilliseconds配置值的时候,若 路由请求的处理时间超过该配置值且依赖服务的请求还未响应的时候, 会自动进行 重 试 路 由 请 求 。
在使用Zuul的服务路由时,如果路由转发请求发生超时(连接超时或处理超时),只要超时时间的设置小于Hystrix的命令超时时间,那么它就会自动发起重试。 但是在有些情况下,我们可能需要关闭该 重试机制,那么可以通过下面的两个参数来进行设置:
zuul.retryable = false //全局关闭重试机制
zuul.routes.<route>.retryable = false //指定路由关闭重试机制
核心过滤器:
在Spring Cloud Zuul中为了让 API 网关组件可以被更方便地使用, 它在 HTTP 请求生命周期的各个阶段默认实现了一 批核心过滤器, 它们会在 API 网关服务启动的时候被自动加载和启用 。 我们可以在源码中查看和了解它们, 它们定义在 org.springframework.cloud.netflix.zuul.filters包下。
关于这三类的过滤器的具体功能大家可以参考《Spring Cloud微服务实战》,这里把过滤器根据顺序、 名称、 功能、 类型做了综合整理形成一张图:
禁用过滤器:
不论是核心过滤器还是自定义过滤器, 只要在API网关应用中为它们创建了实例, 那么默认情况下, 它们都是启用状态的。 那么如果有些过滤器我们不想使用了, 如何禁用它们呢?大多情况下初识Zuul的使用者第 一 反应就是通过重写shouldFilter 逻辑, 让它返回false, 这样该过滤器对于任何请求都不会被执行, 基本实现了对过滤器的禁用。 但是, 对于自定义过滤器来说似乎是实现了过滤器不生效的功能, 但是这样的做法缺乏灵活性。 由于直接要修改过旃器逻辑, 我们不得不重新编译程序, 并且如果该过滤器在未来一段时间还有可能被启用的时候, 那么就又得修改代码并编译程序。 同时, 对于核心过滤器来说, 就更为麻烦, 我们不得不获取源码来进行修改和编译。实际上,在Zuul中特别提供了 一 个参数来禁用指定的过滤器,该参数的配置格式如下:
zuul.<SimpleClassName>.<filterType>.disable = true
其中, <SimpleClassName>代表过滤器的类名, 比如上面示例中的AccessFilter;<filterType>代表过滤器类型, 比如快速入门示例中 AccessFilter 的过滤器类型pre。 所以, 如果我们想要禁用快速入门示例中的 AccessFilter 过滤器, 只需要在application.properties 配置文件中增加如下配置即可:
zuul.AccessFilter.pre.disable = true
该参数配置除了可以对自定义的过滤器进行禁用配置之外, 很多时候可以用它来禁用Spring Cloud Zuul中默认定义的核心过滤器。 这样我们就可以抛开Spring Cloud Zuul自带的那套核心过滤器, 实现 一 套更符合我们实际需求的处理机制。
动态过滤器:
既然通过 Zuul 构建的API网关服务能够轻松地实现动态路由的加载,那么对于API网关服务的另外 一 大重要功能 一— 请求过滤器的动态加载自然也不能放过, 只是对于请求过滤器的动态加载与请求路由的动态加载在实现机制上会有所不同。 这个不难理解,通过之前介绍的请求路由和请求过滤的示例, 我们可以看到请求路由通过配置文件就能实现,而请求过滤则都是通过编码实现。 所以,对于实现请求过滤器的动态加载, 我们需要借助基于NM实现的动态语言的帮助, 比如Groovy。
基于上文的演示 Demo中进行改造
1.添加依赖:版本在cloud内有定义
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
2.为了方便使用, 我们先自定义 一 些用来配置动态加载过滤器的参数, 并将它们的配置值加入到 application.properties 中, 比如:
zuul.filter.root=filter//指定动态加载的过滤器存储路径
zuul.filter.interval=5//配置动态加载的间隔时间, 以秒为单位。
创建用来加载自定义属性的配置类, 命名为 FilterConfiguration, 具体内容如下:
@ConfigurationProperties("zuul.fi1ter")
public class FilterConfiguration {
private String root;
private Integer interval;
//......getter/setter
}
创建应用启动主类, 并在该类中引入上面定义的 FilterConfiguration 配置,并创建动态加载过滤器的实例。 具体内容如下:
@SpringBootApplication
@EnableZuulProxy
@EnableDiscoveryClient
@EnableConfigurationProperties({FilterConfiguration.class})
public class ZuulApp {
private final static Logger log = LoggerFactory.getLogger(ZuulApp.class);
public static void main(String[] args) {
SpringApplication.run(ZuulApp.class, args);
log.info("服务启动成功");
}
@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 目录
3.在 filter/pre 目录下创建 一 个 pre 类型的过滤器,命名为 PreFilter.groovy 。
public class PreFilter extends ZuulFilter {
Logger log = LoggerFactory.getLogger(PreFilter.class);
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 1000;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
HttpServletRequest request = RequestContext.getCurrentContext().getRequest();
log.info("this is a pre filter: Send{ } request to{}",
request.getMethod(),
request.getRequestURL().toString());
return null;
}
}
4.在加入了该过滤器之后, 不需要重启 API 网关服务, 只需要稍等几秒就会生效。 我们可 以继续尝 试 向 API 网 关 服 务 发 起 请 求: http://localhost:9110/fegin/hello, 此时在控制台中可以看到 PreFilter.groovy 过滤器中定义的日志信息, 具体如下:
API网关服务的动态过滤器功能可以帮助我们增强API网关的待续服务能力, 对于网关中的处理逻辑维护也变得更为灵活, 不仅可以动态地实现请求校验, 还可以动态地实现对请求内容的干预。不过, 目前在实际使用过程中, 对于处理 一 些简单的常用过滤功能还是没有什么问题的, 只是需要注意 一 些已知的问题并避开这些情况来使用即可。 比如, 在使用Groovy定义动态过滤器的时候, 删除Groovy文件并不能从当前运行的API网关中移除这个过滤器, 所以如果要移除的话可以通过修改Groovy过滤器的shouldFilter返回false。 另外还需要注意 一 点, 目前的动态过滤器是无法直接注入API 网关服务的Spring容器中加载的实例来使用的, 比如, 我们是无法直接通过注入RestTemplate等实例, 在动态过滤器中对各个微服务发起请求的 。
动态路由:
通过之前对请求路由的详细介绍, 我们可以发现对于路由规则的控制几乎都可以在配置文件application.properties或application.yaml中完成。 既然这样, 对于如何实现Zuul的动态路由,我们很自然地会将它与SpringCloud Config的动态刷新机制联系到 一 起。 只需将API网关服务的配置文件通过Spring Cloud Config连接的Git仓库存储和管理, 我们就能轻松实现动态刷新路由规则的功能。直接上硬货。
Config-Server搭建:
1.创建springboot项目,添加依赖:
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency><!--不注册到eureka上可以不要-->
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
</dependencies>
2.配置 application.properties:
server.port = 8888
spring.application.name=config
spring.profiles.active=git
#github地址
spring.cloud.config.server.git.uri=https://github.com/wuzhenzhao/spring-cloud-config-repo.git
#配置文件在哪个目录下开始寻找
spring.cloud.config.server.git.search-paths=properties
# master
spring.cloud.config.server.git.default-label=master
#账号
spring.cloud.config.server.git.username=*********
#密码
spring.cloud.config.server.git.password=********
3.启用config server
@SpringBootApplication
@EnableConfigServer
public class App {
private final static Logger log = LoggerFactory.getLogger(App.class);
public static void main(String[] args) {
SpringApplication.run(App.class, args);
log.info("服务启动成功");
}
}
这样子 config-server就搭建好了。为了更清晰的配置动态路由,这里重新创建一个动态路由的zuul服务.
dynamic-zuul-server 搭建:
1.创建boot服务,添加依赖:
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-zuul -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zuul</artifactId>
<version>1.4.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
</dependencies>
2.在/resource文件夹下创建一个 bootstrap.properties 文件:
#应用名称,默认根据这个去获取配置文件,文件名称为dynamic-zuul-server.properties
spring.application.name=dynamic-zuul-server
server.port = 9111
spring.cloud.config.uri = http://localhost:8888/
#开启这个端点,便于等等我们查看配置
management.endpoints.web.exposure.include = routes
#Eureka服务地址
eureka.client.serviceUrl.defaultZone = http://localhost:7001/eureka/,http://localhost:7002/eureka/
3.修改启动类,并且开启动态更新配置
@SpringBootApplication
@EnableZuulProxy
public class DynamicZuulApp {
private final static Logger log = LoggerFactory.getLogger(DynamicZuulApp.class);
public static void main(String[] args) {
SpringApplication.run(DynamicZuulApp.class, args);
log.info("服务启动成功");
}
@RefreshScope
@ConfigurationProperties("zuul")
public ZuulProperties zuulProperties() {
return new ZuulProperties();
}
}
在完成了所有程序相关的编写之后, 我们还需要在 Git 仓库中增加网关的配置文件,取名为 dynamic-zuul-server.properties。 在配置文件中, 我们为 API 网关服务预定义以下路由规则, 比如:
zuul.routes.feign.serviceId = feign-server
zuul.routes.feign.path = /fegin/**
在完成了上述内容之后我们启动服务,在 dynamic-zuul-server 服务启动后,我们可以看到 config-server 应用的控制台打印出下列信息,说明配置没问题:
可以通过对 API 网关服务调用/routes 接口来获取当前网关上的路由规则, 根据上述配置我们可以得到如下返回信息:其他两个是系统生成的。
这个时候我们通过 http://localhost:9111/feign/hello 访问也是没问题的。
修改 Git 仓库中的 dynamic-zuul-server.properties 配置文件,在修改完 配 置文件之后, 将修 改内容推送到远程仓 库。 然后, 通过向dynamic-zuul-server 的 /refresh 接口发送 POST 请求来刷新配置信息。这里需要打开refresh端点:
management.endpoints.web.exposure.include = routes,refresh
当配置文件有修改的时候,该接口会返回被修改的属性名称,根据上面的修改,我们会得到如下返回信息:
再访问 http://localhost:9111/actuator/routes ,就会发生信息变化。当然现在接口的转发路由就会变化。