一、Zuul简介
Zuul作为微服务系统的网关组件,用于构建边界服务(Edge Service),致力于动态路由、过滤、监控、弹性伸缩和安全。其在微服务架构中有着重要的作用,主要体现在以下六个方面:
- Zull、Ribbon以及Eureka相结合可以实现智能路由和负载均衡的功能,Zull可以按照某种策略将请求分发到不同的实例上;
- 网关作为边界服务,将内部服务的API接口进行聚合并统一对外暴露接口。保护内部服务的API接口,防止内部服务被外界调用泄露敏感信息;
- 网关可以对用户的身份权限进行认证,防止非法请求API接口;
- 网关可以实现监控功能,实时日志输出,对请求进行记录;
- 网关可以用来实现流量监控,在高流量的情况下,对服务进行降级;
- API接口从内部服务分离出来,便于测试
二、请求路由
使用Spring Cloud Zuul实现路由的规则是十分简单的。路由方式包括两种:传统路由方式,面向服务的路由方式。
2.1 传统路由
下面我们看以下配置:
zuul.routes.holiday.path=/holiday/**
zuul.routes.holiday.url=http://localhosst:8080/
该规则配置表示所有符合/holiday/** 规则的访问都会被路由转发到http://localhosst:8080/地址上,例如:当我们访问http://localhost:5555/holiday/getAllDays,API网关就会将请求转发到http://localhost:8080/holiday/getAllDays提供的微服务接口上。其中holiday为微服务的名称,可以任意定义,但是一组path和url映射关系的路由名称必须相同,下面面向服务的路由方式也是如此。
2.2 面向服务的路由
Spring Cloud Zuul 与 Spring Cloud Eureka 可以实现无缝对接实现面向服务的路由。我们让路由的path映射到具体的服务上,而具体的url交由Eureka的服务发现机制去自动维护。具体配置如下(其他配置参考下面的实战):
zuul.routes.holiday.path=/holiday/**
zuul.routes.holiday.service-id=holiday
通过上面的配置,我们不需要维护具体实例的位置,是得维护工作十分简单。另外,面向服务打的路由默认实现了负载均衡,而传统路由还需要手动添加所有实例的位置。
三、路由规则
Spring Cloud Zuul提供了默认的路由规则,当然我们也可以修改这个路由规则。
3.1 默认路由规则
Zull与Eureka的配合使用后,Zull会默认配置一个路由规则,这些默认规则的path会使用service-id配置的服务名作为请求的前缀。例如:有holiday服务,他的默认规则如下
zuul.routes.holiday.path=/holiday/**
zuul.routes.holiday.service-id=holiday
由于默认情况下所有Eureka上的服务都会被Zuul自动创建映射关系进行路由,这会使得一些我们不希望对外开放的服务也被外部访问到。这个时候我们可以配置zuul.ignored-services
参数来设置一个服务名匹配表达式进行判断,如果服务名匹配表达式,那么Zull将跳过这个服务,不为其创建路由规则。例如:zuul.ignored-services=*
表示对所有的服务不自动创建路由规则,这样我们就需要为每个服配置路由规则。
3.2 自定义路由规则
有这样一个场景,由于业务的扩展,版本的升级,服务存在不同的版本。比如我们有这样的命名:holiday-v1、holiday-v2,默认情况下,Zuul自动为服务创建的路由表达式会采用服务名做前缀,针对holiday-v1就会产生/holiday-v1,/holiday-v2两个路径来做映射,但这样生成的表达式规则较为单一,不利于路径规则的管理。通常,对于上面这种情况,我们希望是生成的路径为/v1/holiday,/v2/holiday。我们可以通过自定义路由规则来实现,具体代码如下:
@Bean
public PatternServiceRouteMapper serviceRouteMapper(){
return new PatternServiceRouteMapper(
"(?<name>^.+)-(?<version>v.+$)",
"${version}/${name}");
}
PatternServiceRouteMapper
对象可以让开发者通过正则表达式来自定义服务于路由映射的生成关系。
四、Zuul的过滤器
Zull有请求过滤的功能,其过滤器可以在Http请求的发起和响应返回期间执行一系列的过滤器。Zuul包扩以下四种过滤器:
- PRE: 该类型的filters在Request routing到源web-service之前执行。可以进行一些权限认证,日志记录,或者额外给Request增加一些属性供后续的filter使用;
- ROUTING:该类型的filters用于把Request routing到源web-service,源web-service是实现业务逻辑的服务。这里使用HttpClient请求web-service;
- POST:该类型的filters在ROUTING返回Response后执行。用来实现对Response结果进行修改,收集统计数据以及把Response传输会客户端;
- ERROR:上面三个过程中任何一个出现错误都交由ERROR类型的filters进行处理。
Zuul过滤器具有以下关键特性:
- Type(类型):Zuul过滤器的类型,这个类型决定过滤器在哪个阶段执行,例如:pre,post等阶段;
- Execution Order(执行顺序):规定了过滤器的执行顺序,Order的值越小,越先执行;
- Criteria(标准):Filters执行所需条件
- Action(行动):如果符合执行条件,则执行Action(具体逻辑代码)
示例如下:
public class MyFilter extends ZuulFilter {
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
System.out.println(String.format("%s AccessUserNameFilter request to %s", request.getMethod(), request.getRequestURL().toString()));
// 获取请求的参数
String username = request.getParameter("username");
// 如果请求的参数不为空,且值为chhliu时,则通过
if(null != username && username.equals("chhliu")) {
// 对该请求进行路由
ctx.setSendZuulResponse(true);
ctx.setResponseStatusCode(200);
// 设值,让下一个Filter看到上一个Filter的状态
ctx.set("isSuccess", true);
return null;
}else{
// 过滤该请求,不对其进行路由
ctx.setSendZuulResponse(false);
// 返回错误码
ctx.setResponseStatusCode(401);
// 返回错误内容
ctx.setResponseBody("{\"result\":\"username is not correct!\"}");
ctx.set("isSuccess", false);
return null;
}
}
@Override
public boolean shouldFilter() {
// 是否执行该过滤器,此处为true,说明需要过滤
return true;
}
@Override
public int filterOrder() {
// 优先级为0,数字越大,优先级越低
return 0;
}
@Override
public String filterType() {
// 前置过滤器
return "pre";
}
}
Zuul请求的生命周期如图所示:
五、设置熔断
通常在服务无法提供服务的时候,需要执行熔断。zuul上实现熔断需要实现FallbackProvider的接口。实现接口中的两个方法:getRoute()用于指定应用在哪个服务上;fallbackResponse()进入熔断功能的执行逻辑。示例如下:
@Component
public class CustomZuulFallbackHandler implements FallbackProvider {
private final Logger logger = LoggerFactory.getLogger(FallbackProvider.class);
/**
* 指定处理的service
*
* @return
*/
@Override
public String getRoute() {
return "*";
}
public ClientHttpResponse fallbackResponse(String route) {
return new ClientHttpResponse() {
@Override
public HttpStatus getStatusCode() throws IOException {
return HttpStatus.OK;
}
@Override
public int getRawStatusCode() throws IOException {
return 200;
}
@Override
public String getStatusText() throws IOException {
return "OK";
}
@Override
public void close() {
}
@Override
public InputStream getBody() throws IOException {
return new ByteArrayInputStream((route+" is unavailable.").getBytes());
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return headers;
}
};
}
@Override
public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
if (cause != null) {
String reason = cause.getMessage();
logger.info("Excption {}",reason);
}
return fallbackResponse(route);
}
}
六、实战
6.1 pom文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.southgis.ibase.parent</groupId>
<artifactId>parentWebService</artifactId>
<version>2.0.1-SNAPSHOT</version>
<relativePath>../../parent/parentWebService/pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>api-gateway</artifactId>
<groupId>com.southgis.ibase.systemassistance</groupId>
<version>2.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<description>网关服务</description>
<dependencies>
<!--服务注册与发现-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<!--配置中心-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-client</artifactId>
</dependency>
<!--路由网关-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<!--cas 客户端-->
<dependency>
<groupId>org.jasig.cas.client</groupId>
<artifactId>cas-client-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<finalName>apiGateway</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>com.southgis.ibase.systemassistance.ApiGatewayCustomApplication</mainClass>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
6.2 配置文件
-
bootstrap.properties
#服务名 对应配置文件中的{application}部分 spring.application.name=apiGateway #对应前配置文件中的{profile}部分 spring.cloud.config.profile=dev2 #配置访问路径 server.servlet.context-path=/eureka-server #注册中心 eureka.client.serviceUrl.defaultZone=http://localhost:8080/eureka-server/eureka #为监控端点 /info和/health端点也加上类似的前缀 management.server.servlet.context-path=/apiGateway eureka.instance.statusPageUrlPath=${management.server.servlet.context-path}/actuator/info eureka.instance.healthCheckUrlPath=${management.server.servlet.context-path}/actuator/health #通过服务连接配置中心 #spring.cloud.config.discovery.enabled=true #spring.cloud.config.discovery.serviceId=config-server spring.cloud.config.uri = http://localhost:8080/config-server #配置文件获取失败快速返回 spring.cloud.config.failFast=true #日志配置 #logging.config=classpath:logback-spring.xml #logging.path=D:/ibase/logs/holiday #logging.pattern.console=[%d{yyyy-MM-dd HH:mm:ss}] -- [%-5p]: [%c] -- %m%n #logging.pattern.file=[%d{yyyy-MM-dd HH:mm:ss}] -- [%-5p]: [%c] -- %m%n
-
apiGateway-dev2.properties
#访问端口 server.port=8080 #设置session超时时间为540分钟 server.servlet.session.timeout=PT540M #zuul默认为所有服务开启默认的路由,为了服务安全,此处关闭 zuul.ignored-services=* #代码字典服务路由 zuul.routes.codedict.path=/codedict/** zuul.routes.codedict.service-id=codedict #是否转发后还带转发特征的字符 zuul.routes.codedict.strip-prefix=false #行政区划服务路由 zuul.routes.adminzone.path=/adminzone/** zuul.routes.adminzone.service-id=adminzone zuul.routes.adminzone.strip-prefix=false #是否开启路由重试 zuul.retryable=true #对当前服务的重试次数 ribbon.MaxAutoRetries=2 #切换实例的重试次数 ribbon.MaxAutoRetriesNextServer=0 #请求处理的超时时间 ribbon.ReadTimeout=6000 #请求连接的超时时间 ribbon.ConnectTimeout=6000 #对所有操作请求都进行重试 ribbon.OkToRetryOnAllOperations=true #将 hystrix 的超时时间设置成 5000 毫秒(hystrix超时时间小于ribbon连接超时时间,先走hystrix) hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=5000
6.3 过滤器配置
@Configuration
public class ApiGatewayFilter extends ZuulFilter {
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext context = RequestContext.getCurrentContext();
HttpServletRequest request = context.getRequest();
Principal principal = request.getUserPrincipal();
//获取用户的登录id
String userId = principal.getName();
context.addZuulRequestHeader("X-AUTH-ID",userId);
return null;
}
}
在这里我们将获取的登录用户id设置到了请求头中传递给内部服务,内部服务可以通过下面的代码进行获取:
String user = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest().getHeader("X-AUTH-ID");
6.4 熔断配置
@Component
public class CustomZuulFallbackHandler implements FallbackProvider {
private final Logger logger = LoggerFactory.getLogger(FallbackProvider.class);
/**
* 指定处理的service
*
* @return
*/
@Override
public String getRoute() {
return "*";
}
public ClientHttpResponse fallbackResponse(String route) {
return new ClientHttpResponse() {
@Override
public HttpStatus getStatusCode() throws IOException {
return HttpStatus.OK;
}
@Override
public int getRawStatusCode() throws IOException {
return 200;
}
@Override
public String getStatusText() throws IOException {
return "OK";
}
@Override
public void close() {
}
@Override
public InputStream getBody() throws IOException {
return new ByteArrayInputStream((route+" is unavailable.").getBytes());
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return headers;
}
};
}
@Override
public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
if (cause != null) {
String reason = cause.getMessage();
logger.info("Excption {}",reason);
}
return fallbackResponse(route);
}
}
6.5 启动类
**
* 路由网关服务部署启动类
*
* @author simon
**/
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableZuulProxy
@EnableEurekaClient
@SpringCloudApplication
public class ApiGatewayMicroApplication {
public static void main(String[] args) {
SpringApplication.run(ApiGatewayMicroApplication.class, args);
}
}