写在前面
该文参考来自 程序猿DD 的Spring Cloud 微服务实战一书,该文是作为阅读了 spring cloud Zuul 一章的读书笔记。书中版本比较老,我选择了最新稳定版的 spring cloud Greenwich.SR2 版本,该版本较书中版本有些变动。非常感谢作者提供了这么好的学习思路,谢谢!文章也参考了 Spring-cloud-netflix 的官方文档。
路由是微服务体系结构的一个组成部分。例如 /
可能映射到你的 web应用程序,/api/users
映射到用户服务,/api/shop
映射到商店服务。 Zuul
是一个来自 Netflix
的基于JVM
的路由器和服务端负载均衡器。DD 书里这样一段话描述的非常好。“API 网关是一个更为智能的应用服务器,它的定义类似于面向对象设计模式中的 Facade 模式,它的存在就像是整个微服务架构系统的门面一样,所有的外部客户端访问都需要经过它来进行调度和过滤。”
1. 入门
新建 api-gateway
工程
1.1 导入依赖:
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
1.2 添加启动类
@EnableZuulProxy
@SpringCloudApplication
public class Application {
public static void main(String[] args) {
new SpringApplicationBuilder().sources(Application.class).web(WebApplicationType.SERVLET).run(args);
}
}
@EnableZuulProxy
注解开启 Zuul 的 API 网关服务功能。
1.3 添加配置文件
spring:
application:
name: api-gateway
server:
port: 60010
eureka:
client:
service-url:
defaultZone: http://localhost:1112/eureka/
zuul:
routes:
api-a:
# 配置对路径 /api-a/ 的所有访问将会路由到 hello-service 服务上
path: /api-a/**
serviceId: hello-service
api-b:
# 配置对路径 /api-b/ 的所有访问将会路由到 feign-consumer 服务上
path: /api-b/**
serviceId: feign-consumer
运行工程,访问地址 /api-b/feign-consumer
。
以上是我们基于服务的路由方式,那么对于传统的路由,我们应该怎样配置呢?
zuul:
routes:
api-a-url:
# 配置对路径 /api-a-url/ 的所有访问将会路由到 http://localhost:8080/provider/ 服务上
path: /api-a-url/**
url: http://localhost:8080/provider/
api-b-url:
# 配置对路径 /api-b-url/ 的所有访问将会路由到 http://localhost:8082/ 服务上
path: /api-b-url/**
serviceId: http://localhost:8082/
该配置和上面基于服务的路由配置是一样的,访问地址/api-b-url/feign-consumer
。
2. 嵌入式 Zuul 反向代理
Spring Cloud创建了一个嵌入式 Zuul代理,以简化UI应用程序希望对一个或多个后端服务进行代理调用的常见用例的开发。该特性对于用户界面代理到所需的后端服务非常有用,从而避免了对所有后端独立管理 CORS 和身份验证问题的需要。
要启用它,请使用@EnableZuulProxy
注释Spring引导主类。这样做会导致本地调用被转发到适当的服务。按照惯例,具有用户ID的服务接收位于/users的代理的请求(去掉前缀)。代理使用Ribbon
来定位要通过服务发现转发到的服务实例。所有请求都在一个hystrix
命令中执行,因此失败将出现在hystrix
指标中。一旦电路打开,代理服务器就不会试图联系服务。
Zuul Starter
并不包含服务发现客户端,所以基于服务路由的方式,我们需要引入服务发现客户端的依赖包。
简单的路由方式并不会作为 HystrixCommand
执行,也不会用 Ribbon
来负载均衡多个 URL 。为了实现这一目标,我们可以使用静态的服务器列表来指定 serviceId,如下:
zuul:
routes:
echo:
path: /myusers/**
serviceId: myusers-service
stripPrefix: true
hystrix:
command:
myusers-service:
execution:
isolation:
thread:
timeoutInMilliseconds: ...
myusers-service:
ribbon:
NIWSServerListClassName: com.netflix.loadbalancer.ConfigurationBasedServerList
listOfServers: https://example1.com,http://example2.com
ConnectTimeout: 1000
ReadTimeout: 3000
MaxTotalHttpConnections: 500
MaxConnectionsPerHost: 100
3. 管理端点
默认情况下,使用 @EnableZuulProxy
注解后,将启用两个额外的端点(需要通过management.endpoints.web.exposure.include=routes,filters
对外暴露):
-
routes
使用 GET 请求访问
/actuator/routes
,将返回路由映射列表:{ "/api-a/**": "hello-service", "/api-b/**": "feign-consumer", "/api-a-url/**": "http://localhost:8080/provider/", "/api-b-url/**": "http://localhost:8082/", "/feign-consumer/**": "feign-consumer", "/eureka-server-node1/**": "eureka-server-node1", "/hello-service/**": "hello-service", "/eureka-server-node2/**": "eureka-server-node2" }
你会发现,这里包含了我们未在配置文件中配置的路径映射。原因是引入服务发现客户端后,会有服务的自动映射。我们可以通过
zuul.ignored-services='*'
来忽略它,对于我们额外的路由配置,并不会被忽略。为我们的配置文件添加完该配置后,再次访问路由端点:
{ "/api-a/**":"hello-service", "/api-b/**":"feign-consumer", "/api-a-url/**":"http://localhost:8080/provider/", "/api-b-url/**":"http://localhost:8082/" }
额外的,我们可以通过
?format=details
或者/routes/details
来获取路由详细信息:GET /routes/details.
{ "/api-a/**": { "id": "api-a", "fullPath": "/api-a/**", "location": "hello-service", "path": "/**", "prefix": "/api-a", "retryable": false, "customSensitiveHeaders": false, "prefixStripped": true }, "/api-b/**": { "id": "api-b", "fullPath": "/api-b/**", "location": "feign-consumer", "path": "/**", "prefix": "/api-b", "retryable": false, "customSensitiveHeaders": false, "prefixStripped": true }, "/api-a-url/**": { "id": "api-a-url", "fullPath": "/api-a-url/**", "location": "http://localhost:8080/provider/", "path": "/**", "prefix": "/api-a-url", "retryable": false, "customSensitiveHeaders": false, "prefixStripped": true }, "/api-b-url/**": { "id": "api-b-url", "fullPath": "/api-b-url/**", "location": "http://localhost:8082/", "path": "/**", "prefix": "/api-b-url", "retryable": false, "customSensitiveHeaders": false, "prefixStripped": true } }
通过
POST
请求访问/routes
端点将强制刷新已经存在的路由,我们可以通过endpoints.routes.enabled = false
来禁用该端点。 -
filters
GET
请求访问/filters
端点将返回过滤器类型映射。{ "error": [{ "class": "org.springframework.cloud.netflix.zuul.filters.post.SendErrorFilter", "order": 0, "disabled": false, "static": true }], "post": [{ "class": "org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter", "order": 1000, "disabled": false, "static": true }], "pre": [{ "class": "org.springframework.cloud.netflix.zuul.filters.pre.DebugFilter", "order": 1, "disabled": false, "static": true }, { "class": "org.springframework.cloud.netflix.zuul.filters.pre.FormBodyWrapperFilter", "order": -1, "disabled": false, "static": true }, { "class": "com.duofei.filter.AccessFilter", "order": 0, "disabled": false, "static": true }, { "class": "org.springframework.cloud.netflix.zuul.filters.pre.Servlet30WrapperFilter", "order": -2, "disabled": false, "static": true }, { "class": "org.springframework.cloud.netflix.zuul.filters.pre.ServletDetectionFilter", "order": -3, "disabled": false, "static": true }, { "class": "org.springframework.cloud.netflix.zuul.filters.pre.PreDecorationFilter", "order": 5, "disabled": false, "static": true }], "route": [{ "class": "org.springframework.cloud.netflix.zuul.filters.route.SimpleHostRoutingFilter", "order": 100, "disabled": false, "static": true }, { "class": "org.springframework.cloud.netflix.zuul.filters.route.RibbonRoutingFilter", "order": 10, "disabled": false, "static": true }, { "class": "org.springframework.cloud.netflix.zuul.filters.route.SendForwardFilter", "order": 500, "disabled": false, "static": true }] }
其中,
com.duofei.filter.AccessFilter
是我自定义的过滤器,代码如下:public class AccessFilter extends ZuulFilter { private static Logger log = LoggerFactory.getLogger(AccessFilter.class); @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 currentContext = RequestContext.getCurrentContext(); HttpServletRequest request = currentContext.getRequest(); log.info("send {} request to {}",request.getMethod(),request.getRequestURL().toString()); String accessToken = request.getParameter("accessToken"); if (accessToken == null) { log.warn("access token is empty"); currentContext.setSendZuulResponse(false); currentContext.setResponseStatusCode(401); return null; } log.info("access token ok"); return null; } }
还需要在启动类中将该类添加到 Spring环境中:
@Bean public AccessFilter accessFilter(){ return new AccessFilter(); }
4. 扼杀模式和本地转发
迁移现有应用程序或 API
时的一种常见模式是“扼杀”旧端点,用不同的实现缓慢地替换它们。Zuul
代理在这方面是一个有用的工具,因为您可以使用它来处理来自旧端点的所有流量,但将一些请求重定向到新端点。
application.yml
zuul:
routes:
first:
path: /first/**
url: https://first.example.com
second:
path: /second/**
url: forward:/second
third:
path: /third/**
url: forward:/3rd
legacy:
path: /**
url: https://legacy.example.com
在这个示例中,我们扼杀了 legacy
遗留程序;/first/**
中的路径将映射到外部 URL 的新服务中,/second/**
的路径将被转发,因此我们可以在本地处理它,(像普通的 @RequestMapping
), /third/**
中的路径也被转发,但前缀不同(例如/third/foo
将被转发到/3rd/foo
)。
以上来自于 Spring 官方文档,我对文档中提到的功能做了实践,在此做下记录。