通过前几章形成的微服务基础架构:
在该架构中,我们的服务集群包含内部服务ServiceA和ServiceB, 它们都会向Eureka Server集群进行注册与订阅服务,而OpenService是一个对外的RESTfulAPI服务,它通过FS、 Nginx等网络设备或工具软件实现对各个微服务的路由与负载均衡,并公开给外部的客户端调用。
什么是API网关服务:Spring Cloud Zuul
API网关是一个更为智能的应用服务器,它的存在就像是整个微服务架构系统的门面一样,所有的外部客户端访问都需要经过它来进行调度和过滤。它除了要实现请求路由、 负载均衡、 校验过滤等功能之外,还需要更多能力,比如与服务治理框架的结合、请求转发时的熔断机制、服务的聚合等一系列高级功能。
Spring Cloud Zuul既具备路由转发功能,又具备过滤器功能。
注意:另外有Spring Cloud Gateway也可实现网关功能。
一、快速入门
1.构建网关
新建一个SpringBoot项目,这里命名api-gateway,然后导入相关依赖:
1
2
3
4
5
6 1<dependency>
2 <groupId>org.springframework.cloud</groupId>
3 <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
4</dependency>
5
6
主类:
1
2
3
4
5
6
7
8
9
10
11 1@EnableEurekaClient
2@EnableZuulProxy
3@SpringBootApplication
4public class SpringcloudzuulApplication {
5
6 public static void main(String[] args) {
7 SpringApplication.run(SpringcloudzuulApplication.class, args);
8 }
9}
10
11
配置:
1
2
3
4 1spring.application.name=api-gateway
2server.port=5555
3
4
2.请求路由
传统路由:
增加配置:
1
2
3
4 1zuul.routes.api-a-url.path=/api-a-url/**
2zuul.routes.api-a-url.url=http://localhost:8080/
3
4
面向服务路由:
1
2
3
4
5
6
7
8
9 1zuul.routes.api-a.path=/api-a/**
2zuul.routes.api-a.serviceId=hello-service
3
4zuul.routes.api-b.path=/api-a/**
5zuul.routes.api-b.serviceId=hello-service
6
7eureka.client.serviceUrl.defaultZone=http://localhost:1111/eureka/
8
9
3.请求过滤
例子:检查是否有accessToken参数
过滤器类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41 1public class AccessFilter extends ZuulFilter {
2
3 private static Logger log = LoggerFactory.getLogger(AccessFilter.class);
4
5 @Override
6 public String filterType() {
7 return "pre";
8 }
9
10 @Override
11 public int filterOrder() {
12 return 0;
13 }
14
15 @Override
16 public boolean shouldFilter() {
17 return true;
18 }
19
20 @Override
21 public Object run() {
22 RequestContext ctx = RequestContext.getCurrentContext();
23 HttpServletRequest request = ctx.getRequest();
24 log.info("send {} request to{}", request.getMethod(), request.getRequestURL().toString());
25 Object accessToken = request.getParameter("accessToken");
26 if (accessToken == null) {
27 log.warn("accessToken is empty");
28 ctx.setSendZuulResponse(false);
29 ctx.setResponseStatusCode(401);
30 try {
31 ctx.getResponse().getWriter().write("accessToken is empty");
32 } catch (Exception e) {
33 }
34 return null;
35 }
36 log.info("access is ok");
37 return null;
38 }
39}
40
41
filterType: 过滤器的类型,它决定过滤器在请求的哪个生命周期中执行。
filterOrder: 过滤器的执行顺序。
shouldFilter: 判断该过滤器是否需要被执行。
run: 过滤器的具体逻辑。
在主类中增加:
1
2
3
4
5
6 1@Bean
2 public AccessFilter accessFilter() {
3 return new AccessFilter();
4 }
5
6
总结一下API网关:
它作为系统的统一入口,屏蔽了系统内部各个微服务的细节
可以和服务治理框架结合,实现自动化服务实例维护和负载均衡
实现接口权限校验与微服务业务逻辑的解耦
通过网关中的过滤器,在各生命周期中去校验请求的内容,将原本在对外的服务层做的校验前移,保证微服务的无状态性,降低测试难度,解放程序员专注业务的处理
二、路由详解
1.传统路由配置
单实例配置:
通过zuul.routes.<路由名>.path与zuul.routes.<路由名>.url
多实例配置:
通过zuul.routes.<路由名>.path与zuul.routes.<路由名>.serviceId
传统路由方式都需要手动为每一对映射指定一个名称,每个<路由名>对应了一条路由规则;每条路由规则都必须有一个path用来匹配请求路径表达式,并通过与之相对应的url或serviceId属性来请求表达式映射的url或服务名
2.服务路由配置
通过zuul.routes.<路由名>.path与zuul.routes.<路由名>.serviceId成对配置
简洁方式:zuul.routes.<服务名>=<映射地址>
1
2
3 1zuul.routes.user-service=/user-service/**
2
3
当有外部请求到达API网关的时候,根据请求的URL路径去匹配path的规则,通过path找到路由名,去找对应的serviceId的服务名。
传统路由就会去根据这个服务名去找listOfServers参数,从而进行负载均衡和请求转发。
面向服务路由会从注册到服务治理框架中取出服务实例清单,通过清单直接找到对应的实例地址清单,从而通过Ribbon进行负载均衡选取实例进行路由(请求转发)。
3.服务路由的默认规则
Zuul在注册到Eureka服务中心之后,它会为Eureka中的每个服务都创建一个默认的路由规则,默认规则的path会使用serviceId配置的服务名作为请求前缀。
我们可以使用zuul.ignored-services参数来设置一个不自动创建该服务的默认路由。
4.自定义路由映射规则
1
2
3
4
5
6
7
8 1@Bean
2public PatternServiceRouteMapper serviceRouteMapper() {
3 return new PatternServiceRouteMapper(
4 "(?<name>^.+)-(?<version>v.+$)",
5 "${version}/${name}");
6}
7
8
5、路径匹配
为路由规则定义匹配表达式-path参数
path通常需要使用通配符:
如果有一个可以同时满足多个path的匹配的情况,此时匹配结果取决于路由规则的定义顺序。
这里需要注意的是:properties无法保证路由规则的顺序,推荐使用yml格式配置文件
6.忽略表达式
Zuul提供了用于忽略路径表达式的参数zuul.ignored-patterns。使用该参数可以用来设置不希望被API网关进行路由的URL表达式。
1
2
3 1zuul.ignored-patterns: /**/hello/**
2
3
7.路由前缀
为了方便全局为路由path增加前缀信息,Zuul提供了zuul.prefix参数来进行设置。
代理前缀会从默认路径中移除掉,可以使用zuul.stripPrefix=false 来关闭移除代理前缀的动作,也可以通过zuul.routes.<路由名>.strip-prefix=false来指定服务关闭移除代理前缀的动作。
8.本地跳转
Zuul代理是一个有用的工具,因为可以使用它来处理来自旧端点客户端的所有流量,但会将某些请求重定向到新端点。
1
2
3
4 1zuul.routes.api-b.path=/api-b/**
2zuul.routes.api-b.url=forward:/local
3
4
9.Cookie与头信息
默认情况下,Zuul在请求路由时会过滤掉HTTP请求头信息中的一些敏感信息,防止这些敏感的头信息传递到下游外部服务器。
可以通过zuul.sensitiveHeaders参数定义,包括Cookie、Set-Cookie、Authorization三个属性来使Cookie可以被传递。
全局放行:
1
2
3 1zuul.sensitiveHeaders=
2
3
指定路由名放行(推荐使用):
1
2
3
4 1zuul.routes.<router>.customSensitiveHeaders=true
2zuul.routes.<router>.sensitiveHeaders=
3
4
10.Hystrix和Ribbon支持
Zuul中包含了Hystrix和Ribbon的依赖,所以Zuul拥有线程隔离和断路器的自我保护功能,以及对服务调用的客户端负载均衡。
1.设置Hystrix超时时间
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds
2.设置Ribbon连接超时时间
ribbon.ConnectTimeout
3. 设置Ribbon的请求转发超时时间
ribbon.ReadTimeout
4.关闭重试配置
全局配置: zuul.retryable=false
针对路由配置: zuul.routes.<路由名>.retryable=false
三、过滤器详解
1.过滤器
在Spring Cloud Zuul 中实现过滤器必须包含4 个基本特征:过滤类型、执行顺序、执行条件、具体操作。实际上就是ZuulFilter抽象类中定义的抽象方法:
String filterType();
int filterOrder();
boolean shouldFilter();
Object run();
filterType:该方法需要返回一个字符串来代表过滤器的类型,而这个类型就是Zuul中的4种不同生命周期的过滤器类型,如下
—pre:在请求到达路由前被调用
—route:在路由请求时被调用
—error: 处理请求时发生的错误时被调用。
—post:在route和error过滤器之后被调用,最后调用。
filterOrder:通过int值定义过滤器执行顺序,数值越小优先级越高。
shouldFilter:返回布尔值来判断该过滤器是否执行。
run:过滤器的具体逻辑。可以在此确定是否拦截当前请求等。
2.请求生命周期
HTTP请求到达Zuul,最先来到pre过滤器,在这里会去映射url patern到目标地址上,
然后将请求与找到的地址交给route类型的过滤器进行求转发,请求服务实例获取响应,
通过post类型过滤器对处理结果进行加工与转换等操作返回。
error类型的过滤器比较特殊,在这整个请求过程中只要有异常才会触发,将异常结果交给post类型过滤器加工返回。
3.异常处理
1.严格的try-catch处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35 1public class ThrowExceptionFilter extends ZuulFilter {
2
3 private static Logger log = Logger.getLogger(ThrowExceptionFilter.class);
4
5 @Override
6 public String filterType() {
7 return "pre";
8 }
9
10 @Override
11 public int filterOrder() {
12 return 0;
13 }
14
15 @Override
16 public boolean shouldFilter() {
17 return true;
18 }
19
20 @Override
21 public Object run() {
22 log.info("This is a pre filter, it will throw a RuntimeException");
23 RequestContext ctx = RequestContext.getCurrentContext();
24 try {
25 doSomething();
26 } catch (Exception e) {
27 ctx.set("error.status_code", HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
28 ctx.set("error.exception", e);
29 }
30 return null;
31 }
32
33}
34
35
2.ErrorFilter处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32 1public class ErrorFilter extends ZuulFilter {
2
3 Logger log = Logger.getLogger(ErrorFilter.class);
4
5 @Override
6 public String filterType() {
7 return "error";
8 }
9
10 @Override
11 public int filterOrder() {
12 return 10;
13 }
14
15 @Override
16 public boolean shouldFilter() {
17 return true;
18 }
19
20 @Override
21 public Object run() {
22 RequestContext ctx = RequestContext.getCurrentContext();
23 Throwable throwable = ctx.getThrowable();
24 log.error("this is a ErrorFilter :" + throwable.getCause().getMessage(), throwable);
25 ctx.set("error.status_code", HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
26 ctx.set("error.exception", throwable.getCause());
27 return null;
28 }
29
30}
31
32
3.不足优化
问题:error类型的过滤器处理完毕之后,除了来自post阶段的异常之外,都会再被post过滤器进行处理。而对于从post过滤器中抛出异常的情况,在经过了error过滤器处理之后,就没有其他类型的过滤器来接手了。
解决:需要在ErrorFilter过滤器之后再定义一个error类型的过滤器,让它来实现SendErrorFilter的功能,但是这个error过滤器并不需要处理所有出现异常的情况,它仅对post过滤器抛出的异常才有效。
判断引起异常的过滤器是来自什么阶段:
扩展processZuulFilter(ZuulFilter filter)方法,当过滤器执行抛出异常的时候,捕获它,并往请求上下中记录一些信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 1public class DidiFilterProcessor extends FilterProcessor {
2
3 @Override
4 public Object processZuulFilter(ZuulFilter filter) throws ZuulException {
5 try {
6 return super.processZuulFilter(filter);
7 } catch (ZuulException e) {
8 RequestContext ctx = RequestContext.getCurrentContext();
9 ctx.set("failed.filter", filter);
10 throw e;
11 }
12 }
13
14}
15
16
完善ErrorExtFilter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27 1@Component
2public class ErrorExtFilter extends SendErrorFilter {
3
4 @Override
5 public String filterType() {
6 return "error";
7 }
8
9 @Override
10 public int filterOrder() {
11 return 30; // 大于ErrorFilter的值
12 }
13
14 @Override
15 public boolean shouldFilter() {
16 // 判断:仅处理来自post过滤器引起的异常
17 RequestContext ctx = RequestContext.getCurrentContext();
18 ZuulFilter failedFilter = (ZuulFilter) ctx.get("failed.filter");
19 if (failedFilter != null && failedFilter.filterType().equals("post")) {
20 return true;
21 }
22 return false;
23 }
24
25}
26
27
在应用主类中,通过调用FilterProcessor.setProcessor(new DidiFilterProcessor());方法来启用自定义的核心处理器。
4.禁用过滤器
zuul.<过滤器名>.<过滤器类型>.disable=true
四、动态加载
在微服务中,API网关担负着外部统一入口的重任,与其他服务不同,它必须保证不关闭不停机,从而确保整个系统的对外服务。所以我们就不能关机改东西再上线,这样会影响到其它服务。Zuul实现了不关机状态下的动态路由和动态添加/删除过滤器等功能。
需要和下一章Spring Cloud Config 组合起来,起到动态刷新配置路由的功能,学习下一章后再学习这部分