SpringCloud微服务知识整理七:API网关服务:Spring Cloud Zuul

释放双眼,带上耳机,听听看~!

通过前几章形成的微服务基础架构:
SpringCloud微服务知识整理七:API网关服务:Spring Cloud Zuul
在该架构中,我们的服务集群包含内部服务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        &quot;(?&lt;name&gt;^.+)-(?&lt;version&gt;v.+$)&quot;,
5        &quot;${version}/${name}&quot;);
6}
7
8

5、路径匹配

为路由规则定义匹配表达式-path参数
path通常需要使用通配符:
SpringCloud微服务知识整理七:API网关服务:Spring Cloud Zuul
如果有一个可以同时满足多个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.&lt;router&gt;.customSensitiveHeaders=true
2zuul.routes.&lt;router&gt;.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.请求生命周期

SpringCloud微服务知识整理七:API网关服务:Spring Cloud Zuul
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 &quot;pre&quot;;
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(&quot;This is a pre filter, it will throw a RuntimeException&quot;);
23        RequestContext ctx = RequestContext.getCurrentContext();
24        try {
25            doSomething();
26        } catch (Exception e) {
27            ctx.set(&quot;error.status_code&quot;, HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
28            ctx.set(&quot;error.exception&quot;, 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 &quot;error&quot;;
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(&quot;this is a ErrorFilter :&quot; + throwable.getCause().getMessage(), throwable);
25        ctx.set(&quot;error.status_code&quot;, HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
26        ctx.set(&quot;error.exception&quot;, 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(&quot;failed.filter&quot;, 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 &quot;error&quot;;
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(&quot;failed.filter&quot;);
19        if (failedFilter != null &amp;&amp; failedFilter.filterType().equals(&quot;post&quot;)) {
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 组合起来,起到动态刷新配置路由的功能,学习下一章后再学习这部分

给TA打赏
共{{data.count}}人
人已打赏
安全经验

Google Adsense的技巧、诀窍和秘密

2021-10-11 16:36:11

安全经验

安全咨询服务

2022-1-12 14:11:49

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索