本文摘自:《Spring Cloud 微服务实战》——翟永超
在微服务架构中,由于API网关服务担负着外部访问统一入口的重任,它同其他应用不同,任何关闭和重启应用的操作都会是系统对外停止服务,对于很多7*24小时服务的系统来说,这种请求是不允许的。所以作为最外部的网关,它必须具备动态更新内部逻辑的能力,比如动态修改路由规则、动态添加、删除过滤器等。
通过Zuul实现的API网关服务具备了动态路由和动态过滤的器能力,可以在不重启API网关服务的前提下为其动态修改路由规则和添加或删除过滤器。
动态路由
通过之前对请求路由的介绍,发现对于路由规则的控制都可以在配置文件 application.properties 或 application.yml 文件中完成。之前还介绍了 Spring Cloud Config 的动态刷新机制,所以只需要将API网关服务的配置文件通过 Spring Cloud Config 连接的Git仓库存储和管理,我们就能实现动态刷新路由规则的功能。
第一步,创建一个API网关服务,命名为api-gateway-dynamic-route,并引入Zuul、Config、Eureka的依赖。
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<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.RC1</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- Config客户端依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<!-- Eureka客户端依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- Zuul依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
</dependencies>
第二步,创建bootstrap.yml,指定 Config Sever 和 Eureka Server 的具体地址,以获取应用的配置文件和实现服务注册与发现。
spring:
application:
name: api-gateway-dynamic-route #应用名
cloud:
config:
name: api-gateway #对应application
uri: http://localhost:5666/ # 配置服务中心的地址
discovery:
enabled: true
service-id: config-server
fail-fast: true #没有读取成功则执行快速失败
server:
port: 5222
#指定服务注册中心位置
eureka:
client:
service-url:
defaultZone: http://localhost:1111/eureka/
instance:
prefer-ip-address: true
instance-id: ${spring.cloud.client.ip-address}:${server.port}
#actuator配置
management:
endpoints:
web:
exposure:
include: routes,refresh
第三步,创建应用主类,添加 @EnableDiscoveryClient、@EnableZuulProxy 注解,并使用 @RefreshScope 注解来将 Zuul 的配置内容动态化。
@SpringBootApplication
@EnableDiscoveryClient
@EnableZuulProxy
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@Bean(name="zuul.CONFIGURATION_PROPERTIES")
@RefreshScope
@ConfigurationProperties("zuul")
@Primary
public ZuulProperties zuulProperties() {
return new ZuulProperties();
}
}
第四步,在Git仓库添加Zuul的配置文件api-gateway.properties。
配置文件中配置了如下路由规则
zuul.routes.service-a.path=/service-a/**
zuul.routes.service-a.serviceId=hello-service
测试
启动服务注册中心,即eureka-server-vFinchley.Rc2工程
启动服务提供者hello-service,即eureka-client-vFinchley.Rc2 工程
启动Config Server,即config-server-vFinchley.RC2工程
启动API网关服务,即api-gateway-dynamic-route工程
启动过程可以看到控制台打印如下信息
2018-10-09 14:00:49.503 INFO 9440 --- [ main] c.c.c.ConfigServicePropertySourceLocator : Fetching config from server at: http://192.168.1.228:5666/
2018-10-09 14:00:50.973 INFO 9440 --- [ main] c.c.c.ConfigServicePropertySourceLocator : Located environment: name=api-gateway, profiles=[default], label=null, version=26b07dc73ae5119922a91b4c079e57ca0ea31d24, state=null
2018-10-09 14:00:50.974 INFO 9440 --- [ main] b.c.PropertySourceBootstrapConfiguration : Located property source: CompositePropertySource {name='configService', propertySources=[MapPropertySource {name='configClient'}, MapPropertySource {name='https://github.com/WYA1993/spring_cloud_config_demo/config/config_repo/api-gateway.properties'}]}
2018-10-09 14:00:50.979 INFO 9440 --- [ main] com.wya.springcloud.Application : No active profile set, falling back to default profiles: default
请求的/actuator/routes接口,返回如下路由规则
{
"/service-a/**": "hello-service",
"/hello-service/**": "hello-service",
"/config-server/**": "config-server"
}
其中/service-a/**路由是在api-gateway.properties读取的,而/hello-service/**和/config-server/**是Zuul默认生成的路由规则,为什么会生成默认路由我在讲解Zuul的时候有介绍,这里就不解释了,这说明已经成功通过Config Server读取到了配置文件。
请求http://192.168.1.228:5222/service-a/hello接口也可以成功返回信息--“hello”。
接下来修改api-gateway.properties,修改后内容如下:
zuul.routes.service-a.path=/service-aaa/**
zuul.routes.service-a.serviceId=hello-service
通过Post请求http://localhost:5222/actuator/refresh刷新路由规则。
再次请求的/actuator/routes接口,返回如下路由规则
{
"/service-aaa/**": "hello-service",
"/hello-service/**": "hello-service",
"/config-server/**": "config-server"
}
请求http://192.168.1.228:5222/service-aaa/hello接口也可以成功返回信息--“hello”。
这就实现了动态路由。
动态过滤器
实现请求过滤器的动态加载,需要借助基于JVM实现的动态语言的帮助,比如Groovy。
第一步,创建 Spring Boot 工程,命名api-gateway-dynamic-filter,并引入Zuul、Eureka、Groovy的依赖。
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.2.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<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.RC2</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.codehaus.groovy/groovy-all -->
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
第二步,创建application.yml文件,指定Eureka服务注册中心地址,并指定路由规则
spring:
application:
name: api-gateway-dynamic-filter #为服务命名
server:
port: 5111
eureka:
client:
service-url:
defaultZone: http://localhost:1111/eureka/ #指定服务注册中心位置
instance:
prefer-ip-address: true
instance-id: ${spring.cloud.client.ip-address}:${server.port}
#服务路由配置
zuul:
routes:
api-a:
path: /api-a/**
serviceId: hello-service
接下来为这个基础服务增加动态过滤器的功能。
第三步,为了方便使用,先自定义一些用来配置动态加载过滤器的参数,并将它们的配置值加入到application.yml中
zuul:
routes:
api-a:
path: /api-a/**
serviceId: hello-service
# 动态过滤器配置参数 自定义
filter:
root: filter
interval: 5
第四步,创建用来加载自定义属性的配置类,命名为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配置,并创建动态加载过滤器的实例。
@SpringBootApplication
@EnableDiscoveryClient
@EnableZuulProxy
@EnableConfigurationProperties({FilterConfiguration.class})
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, 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网关会每隔5秒,从API网关服务所在的位置的 filter/pre 和 filter/post目录下获取Groovy定义的过滤器,并对其进行编译和动态加载使用。对于动态加载的时间间隔可以通过zuul.filter.interval参数修改。而加载过滤器实现类的根目录可通过zuul.filter.root调整。
测试
启动服务注册中心,即eureka-server-vFinchley.Rc2工程
启动服务提供者hello-service,即eureka-client-vFinchley.Rc2 工程
启动API网关服务,即api-gateway-dynamic-filter工程
正常启动后,请求http://192.168.1.228:5111/api-a/hello可以返回hello。
接下来再pre目录下添加一个pre类型的过滤器。
package com.wya.springcloud.filter.pre
import org.slf4j.Logger
import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServletRequest;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
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
}
}
在post目录下添加一个post类型的过滤器。
package com.wya.springcloud.filter.post
import com.netflix.zuul.ZuulFilter
import com.netflix.zuul.context.RequestContext
import com.netflix.zuul.http.HttpServletResponseWrapper
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.cloud.netflix.zuul.util.RequestUtils
import javax.servlet.http.HttpServletResponse
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("debug request : {}", RequestContext.getCurrentContext().getBoolean("debugRequest"))
log.info("this is a post filter: Receive response")
HttpServletResponse response = RequestContext.getCurrentContext().getResponse()
response.getOutputStream().print(", dynamic filter")
response.flushBuffer()
}
}
不需要重新启动,5秒后会自动加载,再次请求http://192.168.1.228:5111/api-a/hello可以返回hello, dynamic filter。
控制台并打印如下内容:
2018-10-09 15:05:03.519 INFO 16128 --- [nio-5111-exec-5] com.didispace.filter.pre.PreFilter : this is a pre filter: Send GET request to http://192.168.1.228:5111/api-a/hello
2018-10-09 15:05:03.529 INFO 16128 --- [nio-5111-exec-5] com.didispace.filter.post.PostFilter : debug request: false
2018-10-09 15:05:03.529 INFO 16128 --- [nio-5111-exec-5] com.didispace.filter.post.PostFilter : this is a post filter: Receive response
2018-10-09 15:06:13.157 INFO 16128 --- [trap-executor-0] c.n.d.s.r.aws.ConfigClusterResolver : Resolving eureka endpoints via configuration
2018-10-09 15:08:02.608 INFO 16128 --- [nio-5111-exec-8] com.didispace.filter.pre.PreFilter : this is a pre filter: Send GET request to http://192.168.1.228:5111/api-a/hello
2018-10-09 15:08:02.619 INFO 16128 --- [nio-5111-exec-8] com.didispace.filter.post.PostFilter : debug request : false
2018-10-09 15:08:02.619 INFO 16128 --- [nio-5111-exec-8] com.didispace.filter.post.PostFilter : this is a post filter: Receive response
这虽然实现了动态过滤器的功能,但是有几个缺陷:
- 在filter目录下删除Groovy文件并不能从当前运行的API网关服务中移除这个过滤器,如果需要移除可以修改shouldFilter返回 false。
- 这样的动态过滤器无法注入Spring 容器中加载的实例来使用,比如在动态过滤器中注入RestTemplate对各个微服务发起请求。