Zuul作为微服务系统的网关组件,用于构建边界服务(Edge Service),致力于动态路由、过滤、监控、弹性伸缩和安全。其在微服务架构中有着重要的作用,主要体现在以下六个方面:
使用Spring Cloud Zuul实现路由的规则是十分简单的。路由方式包括两种:传统路由方式,面向服务的路由方式。
下面我们看以下配置:
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映射关系的路由名称必须相同,下面面向服务的路由方式也是如此。
Spring Cloud Zuul 与 Spring Cloud Eureka 可以实现无缝对接实现面向服务的路由。我们让路由的path映射到具体的服务上,而具体的url交由Eureka的服务发现机制去自动维护。具体配置如下(其他配置参考下面的实战):
zuul.routes.holiday.path=/holiday/**
zuul.routes.holiday.service-id=holiday
通过上面的配置,我们不需要维护具体实例的位置,是得维护工作十分简单。另外,面向服务打的路由默认实现了负载均衡,而传统路由还需要手动添加所有实例的位置。
Spring Cloud Zuul提供了默认的路由规则,当然我们也可以修改这个路由规则。
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=*
表示对所有的服务不自动创建路由规则,这样我们就需要为每个服配置路由规则。
有这样一个场景,由于业务的扩展,版本的升级,服务存在不同的版本。比如我们有这样的命名:holiday-v1、holiday-v2,默认情况下,Zuul自动为服务创建的路由表达式会采用服务名做前缀,针对holiday-v1就会产生/holiday-v1,/holiday-v2两个路径来做映射,但这样生成的表达式规则较为单一,不利于路径规则的管理。通常,对于上面这种情况,我们希望是生成的路径为/v1/holiday,/v2/holiday。我们可以通过自定义路由规则来实现,具体代码如下:
@Bean public PatternServiceRouteMapper serviceRouteMapper(){ return new PatternServiceRouteMapper( "(?^.+)-(?v.+$)" , "${version}/${name}"); }
PatternServiceRouteMapper
对象可以让开发者通过正则表达式来自定义服务于路由映射的生成关系。
Zull有请求过滤的功能,其过滤器可以在Http请求的发起和响应返回期间执行一系列的过滤器。Zuul包扩以下四种过滤器:
Zuul过滤器具有以下关键特性:
示例如下:
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); } }
<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.parentgroupId>
<artifactId>parentWebServiceartifactId>
<version>2.0.1-SNAPSHOTversion>
<relativePath>../../parent/parentWebService/pom.xmlrelativePath>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>api-gatewayartifactId>
<groupId>com.southgis.ibase.systemassistancegroupId>
<version>2.0.1-SNAPSHOTversion>
<packaging>warpackaging>
<description>网关服务description>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-serverartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-config-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-zuulartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.retrygroupId>
<artifactId>spring-retryartifactId>
dependency>
<dependency>
<groupId>org.jasig.cas.clientgroupId>
<artifactId>cas-client-coreartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
<build>
<finalName>apiGatewayfinalName>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<configuration>
<mainClass>com.southgis.ibase.systemassistance.ApiGatewayCustomApplicationmainClass>
configuration>
<executions>
<execution>
<goals>
<goal>repackagegoal>
goals>
execution>
executions>
plugin>
plugins>
build>
project>
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
@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");
@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); } }
** * 路由网关服务部署启动类 * * @author simon **/ @SpringBootApplication(exclude = DataSourceAutoConfiguration.class) @EnableZuulProxy @EnableEurekaClient @SpringCloudApplication public class ApiGatewayMicroApplication { public static void main(String[] args) { SpringApplication.run(ApiGatewayMicroApplication.class, args); } }