SpringCloud组件--Zuul入门解析

一、Zuul简介

1、网关的作用

分布式架构中,服务节点数量较多,而对于客户端而言,多个服务节点暴露出来的API应该是统一的,否则每个节点地址不同,客户端就需要维护所有服务节点地址然后再选择一个访问,很明显客户端的维护成本就很高。

此时就需要有一个暴露统一API的网关服务将多服务节点封装,客户端访问网关,网关再将客户端的请求分发给被封装的服务节点,这样就避免了客户端和服务端的直接交互。

另外网关除了可以实现动态路由功能外,还可以实现参数校验、鉴权、日志、缓存、负载均衡、监控、限流等多种功能。总之对于非业务需求的统一功能都可以交给网关来实现,设计思想有点类似于Spring的面向切面编程。

2、Zuul是什么

Zuul是Netfilx开源的微服务系统的网关组件。

Zuul的作用有很多,如下几个方面

1.配合Ribbon、Eureka可以实现智能动态路由和负载均衡功能,将请求按策略分发到集群中合适的服务实例;

2.将服务和客户端之间进行解耦,客户端访问的是网关暴露出来的统一API,客户端不需要关系服务的运行情况;

3.统一鉴权,监控,日志,缓存,限流,参数校验等功能;

4.通过流量监控,可以根据流量进行服务降级;

3、Zuul工作流程

Zuul本质是一个Servlet来对请求进行控制,核心是创建了一系列的过滤器。Zuul包括以下四种过滤器:

1、PRE过滤器:请求路由到具体的服务之前执行,可以用作安全校验、身份校验、参数校验等前置工作;

2、ROUTING过滤器:用于将请求路由到具体的服务实例,默认使用Http Client进行网络请求;

3、POST过滤器:在请求已经被路由到微服务后执行,通常用于收集统计信息、指标并将响应返回给客户端;

4、ERROR过滤器:在其他过滤器出现异常时执行;

各个类型的过滤器执行顺序依次为PRE过滤器->ROUTING过滤器->POST过滤器,如果出现异常就执行ERROR过滤器

二、Zuul实践

Zuul也是一个服务,所以需要构建Zuul-Server服务

添加Zuul相关依赖

 1 <dependencies>
 2         <dependency>
 3             <groupId>org.springframework.cloud</groupId>
 4             <artifactId>spring-cloud-starter-eureka</artifactId>
 5             <version>1.4.7.RELEASE</version>
 6         </dependency>
 7         <dependency>
 8             <groupId>org.springframework.cloud</groupId>
 9             <artifactId>spring-cloud-starter-zuul</artifactId>
10             <version>1.4.7.RELEASE</version>
11         </dependency>
12         <dependency>
13             <groupId>org.springframework.boot</groupId>
14             <artifactId>spring-boot-starter-web</artifactId>
15             <version>2.5.0</version>
16         </dependency>
17         <dependency>
18             <groupId>org.springframework.boot</groupId>
19             <artifactId>spring-boot-starter-test</artifactId>
20             <scope>test</scope>
21         </dependency>
22     </dependencies>

 

添加Zuul相关配置application.yml

 1 server:
 2   port: 5000
 3 spring:
 4   application:
 5     name: zuul-server
 6 eureka:
 7   client:
 8     serviceUrl:
 9       defaultZone: http://localhost:8761/eureka/
10 zuul:
11   routes:
12     goods:
13       path: /goods/**
14       serverId: goods
15     order:
16       path: /order/**
17       serverId: order

 

其中server表示Zuul服务配置,eureka表示Zuul也是一个Eureka客户端需要配置Eureka,最后就是网关配置zuul,routes是路由配置,需要配置服务的serverId表示路由的服务名称,path表示路由的服务地址,配置之后zuul可以将指定serverId的请求分发到指定的服务。

添加启动类,并添加相关注解

 1 @SpringBootApplication
 2 @EnableEurekaClient
 3 @EnableZuulProxy
 4 public class ZuulApplication {
 5 
 6     public static void main(String[] args){
 7         SpringApplication.run(ZuulApplication.class);
 8         System.out.println("Zuul starting...");
 9     }
10 }

 

@EnableEurekaClient注解表示ZuulServer是一个Eureka客户端,@EnableZuulProxy注解表示当前是一个Zuul服务器,开启Zuul功能。

启动ZuulApplication类,此时就可以完成请求路由功能了,访问http://localhost:5000/goods/xxxx就可以将请求转发到goods服务的API

添加不同逻辑的过滤器,如进行参数校验,自定义CheckFilter过滤器继承父类ZuulFilter,实现ZuulFilter的抽象方法。

@Component
public class CheckFilter 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 {
        /**处理路由过滤等逻辑,根据filterType在不同的时机执行具体的逻辑 */
        //1.获取请求上下文
        RequestContext context = RequestContext.getCurrentContext();
        HttpServletRequest request = context.getRequest();
        Iterator<Map.Entry<String,String[]>> iterator = request.getParameterMap().entrySet().iterator();
        while (iterator.hasNext()){
            Map.Entry<String ,String[]> entry = iterator.next();
            if(entry.getValue()==null || entry.getValue().length==0){
                System.out.println("参数校验失败:参数为空:" + entry.getKey());
                return null;
            }
        }
        return null;
    }
}

 

实现filterType方法表示当前是一个PRE过滤器,filterOrder方法返回过滤器顺序,值越小优先级越高, run方法就是过滤器的具体处理逻辑。

三、Zuul实现原理

3.1、@EnableZuulProxy注解

Zuul使用时比较简单,只需要在启动类添加@EnableZuulProxy注解即可,所以Zuul的所有功能都围绕该注解进行,@EnableZuulProxy的作用是加载ZuulProxyMarkerConfiguration的实例,@EnableZuulProxy定义如下:

1 @EnableCircuitBreaker
2 @Target(ElementType.TYPE)
3 @Retention(RetentionPolicy.RUNTIME)
4 @Import(ZuulProxyMarkerConfiguration.class)
5 public @interface EnableZuulProxy {
6 }

 

而ZuulProxyMarkerConfiguration的作用是注入一个ZuulProxy的标记实例,定义如下:

 1 @Configuration
 2 public class ZuulProxyMarkerConfiguration {
 3     @Bean
 4     public Marker zuulProxyMarkerBean() {
 5         return new Marker();
 6     }
 7 
 8     class Marker {
 9     }
10 }

 

内部类Marker唯一的作用就是用于标记,所以需要看哪里需要用到该标记,通过引用搜索发现引用的地方是ZuulProxyAutoConfiguration,父类是ZuulServerAutoConfiguration,定义如下:

@Configuration
@EnableConfigurationProperties({ ZuulProperties.class })
@ConditionalOnClass(ZuulServlet.class)
@ConditionalOnBean(ZuulServerMarkerConfiguration.Marker.class)
// Make sure to get the ServerProperties from the same place as a normal web app would
// FIXME @Import(ServerPropertiesAutoConfiguration.class)
public class ZuulServerAutoConfiguration {

 

父类ZuulServerAutoConfiguration中会向Spring容器中注入ServletRegistrationBean实例,该实例中创建了一个ZuulServlet。而Zuul的核心功能就在于这个ZuulServlet,Zuul接收到的所有请求最终都会由于ZuulServlet的service方法来完成

@Bean
    @ConditionalOnMissingBean(name = "zuulServlet")
    public ServletRegistrationBean zuulServlet() {
        ServletRegistrationBean<ZuulServlet> servlet = new ServletRegistrationBean<>(new ZuulServlet(),
                this.zuulProperties.getServletPattern());
        // The whole point of exposing this servlet is to provide a route that doesn't
        // buffer requests.
        servlet.addInitParameter("buffer-requests", "false");
        return servlet;
    }

 

3.2、ZuulServlet

ZuulServlet继承之HttpServlet,初始化init方法创建了ZuulRunner对象,而核心功能在于service方法,所有请求都会先执行到service方法,源码如下:

@Override
    public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {
        try {
            /** 1.初始化ZuulRunner 对象*/
            init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);
            /** 2.获取HttpRequest请求上下文*/
            RequestContext context = RequestContext.getCurrentContext();
            context.setZuulEngineRan();

            try {
                /** 3.执行pre过滤器*/
                preRoute();
            } catch (ZuulException e) {
                /** 异常时执行error过滤器和post过滤器*/
                error(e);
                postRoute();
                return;
            }
            try {
                /** 4.执行route过滤器*/
                route();
            } catch (ZuulException e) {
                /** 异常时执行error过滤器和post过滤器*/
                error(e);
                postRoute();
                return;
            }
            try {
                /** 5.执行post过滤器*/
                postRoute();
            } catch (ZuulException e) {
                /** 异常时执行error过滤器*/
                error(e);
                return;
            }

        } catch (Throwable e) {
            error(new ZuulException(e, 500, "UNHANDLED_EXCEPTION_" + e.getClass().getName()));
        } finally {
            /** 清理本地缓存RequestContext*/
            RequestContext.getCurrentContext().unset();
        }
    }

 

整体流程比较清晰,依次执行 PRE过滤器 -> ROUTE过滤器 -> POST过滤器, 如果有异常就执行 ERROR过滤器, 另外如果POST过滤器执行之前异常同样也会执行POST过滤器, 所以可以保证POST过滤器肯定会被执行,

而ERROR过滤器只会在其他过滤器异常时才会执行。另外在执行过滤器逻辑之前会初始化上下文RequestContext,过滤器执行完之后清理RequestContext。

ZuulServlet的preRoute、route、postRoute、error方法都没有具体实现,都委托给ZuulRunner执行对应的方法,而ZuulRunner也不是具体的执行者,而是交给了FilterProcessor来执行。

3.3、FilterProcessor

FilterProcessor是过滤器的具体执行者,是一个单例对象。ZuulServlet的各种过滤器最终分别执行了FilterProcessor的preRoute()、route()、postRoute()、error()方法,而这四个方法最终都是执行了FilterProcessor的内部方法runFilters(String filterType)方法。

runFilter源码如下:

 1 public Object runFilters(String sType) throws Throwable {
 2         if (RequestContext.getCurrentContext().debugRouting()) {
 3             Debug.addRoutingDebug("Invoking {" + sType + "} type filters");
 4         }
 5         boolean bResult = false;
 6         /** 1.获取指定类型的过滤器列表*/
 7         List<ZuulFilter> list = FilterLoader.getInstance().getFiltersByType(sType);
 8         if (list != null) {
 9             for (int i = 0; i < list.size(); i++) {
10                 ZuulFilter zuulFilter = list.get(i);
11                 /** 2.遍历执行过滤器*/
12                 Object result = processZuulFilter(zuulFilter);
13                 if (result != null && result instanceof Boolean) {
14                     bResult |= ((Boolean) result);
15                 }
16             }
17         }
18         return bResult;
19     }

 

首先调用FilterLoader对象的getFilterByType方法根据过滤器类型查询过滤器列表,然后执行processZuulFilter方法执行过滤器逻辑

3.3.1、获取过滤器

所有Zuul的过滤器都需要实现ZuulFilter接口,并且需要被@Component注解修饰从而注入到Spring容器中,而ZuulServerAutoConfiguration类中有一个静态内部类ZuulFilterConfiguration类,该类的作用是将Spring容器中所有的ZuulFilter实例取出来,代码如下:

/** ZuulServerAutoConfiguration内部类 */
    @Configuration
    protected static class ZuulFilterConfiguration {

        @Autowired
        private Map<String, ZuulFilter> filters;

        @Bean
        public ZuulFilterInitializer zuulFilterInitializer(
                CounterFactory counterFactory, TracerFactory tracerFactory) {
            FilterLoader filterLoader = FilterLoader.getInstance();
            FilterRegistry filterRegistry = FilterRegistry.instance();
            return new ZuulFilterInitializer(this.filters, counterFactory, tracerFactory, filterLoader, filterRegistry);
        }
    }

 

ZuulFilterConfigruation首先注入所有的ZuulFilter对象,采用Map存储,而ZuulFilterInitiailizer的作用就是初始化过滤器注册器,代码如下:

    @PostConstruct
    public void contextInitialized() {
        log.info("Starting filter initializer");

        TracerFactory.initialize(tracerFactory);
        CounterFactory.initialize(counterFactory);

        for (Map.Entry<String, ZuulFilter> entry : this.filters.entrySet()) {
            filterRegistry.put(entry.getKey(), entry.getValue());
        }
    }

 

FilterRegistry是过滤器注册器,内部采用Map<String, ZuulFilter> filters 存储所有ZuulFilter过滤器实例。

再回到ZuulRunner类的runFilters方法,通过FilterLoader的getFiltersByType方法获取指定类型过滤器,所以FilterLoader的作用就是将FilterRegistry中过滤器按不同类型进行归类,采用Map<String, List<ZuulFilter>> hashFiltersByType 存储不同类型的过滤器列表。

所以获取过滤器的逻辑比较简单,就是从Spring容器中获取所有ZuulFilter实例,然后按不同的类型进行分组存在Map中缓存起来即可。

3.3.2、执行过滤器

 1 /** 执行过滤器 */
 2     public Object processZuulFilter(ZuulFilter filter) throws ZuulException {
 3         /** 1.获取请求上下文 */
 4         RequestContext ctx = RequestContext.getCurrentContext();
 5         boolean bDebug = ctx.debugRouting();
 6         final String metricPrefix = "zuul.filter-";
 7         long execTime = 0;
 8         String filterName = "";
 9         try {
10             long ltime = System.currentTimeMillis();
11             filterName = filter.getClass().getSimpleName();
12 
13             RequestContext copy = null;
14             Object o = null;
15             Throwable t = null;
16 
17             if (bDebug) {
18                 Debug.addRoutingDebug("Filter " + filter.filterType() + " " + filter.filterOrder() + " " + filterName);
19                 copy = ctx.copy();
20             }
21             /** 2.执行过滤器的runFilter方法 */
22             ZuulFilterResult result = filter.runFilter();
23             ExecutionStatus s = result.getStatus();
24             execTime = System.currentTimeMillis() - ltime;
25 
26             switch (s) {
27                 /** 3.将执行结果存在RequestContext上下文中 */
28                 case FAILED:
29                     t = result.getException();
30                     ctx.addFilterExecutionSummary(filterName, ExecutionStatus.FAILED.name(), execTime);
31                     break;
32                 case SUCCESS:
33                     o = result.getResult();
34                     ctx.addFilterExecutionSummary(filterName, ExecutionStatus.SUCCESS.name(), execTime);
35                     if (bDebug) {
36                         Debug.addRoutingDebug("Filter {" + filterName + " TYPE:" + filter.filterType() + " ORDER:" + filter.filterOrder() + "} Execution time = " + execTime + "ms");
37                         Debug.compareContextState(filterName, copy);
38                     }
39                     break;
40                 default:
41                     break;
42             }
43 
44             if (t != null) throw t;
45 
46             usageNotifier.notify(filter, s);
47             /** 4.返回执行结果 */
48             return o;
49 
50         } catch (Throwable e) {
51             if (bDebug) {
52                 Debug.addRoutingDebug("Running Filter failed " + filterName + " type:" + filter.filterType() + " order:" + filter.filterOrder() + " " + e.getMessage());
53             }
54             usageNotifier.notify(filter, ExecutionStatus.FAILED);
55             if (e instanceof ZuulException) {
56                 throw (ZuulException) e;
57             } else {
58                 ZuulException ex = new ZuulException(e, "Filter threw Exception", 500, filter.filterType() + ":" + filterName);
59                 ctx.addFilterExecutionSummary(filterName, ExecutionStatus.FAILED.name(), execTime);
60                 throw ex;
61             }
62         }
63     }

 

整体逻辑就是先获取全局请求上下文RequestContext,然后调用ZuulFilter的runFilter方法执行过滤器逻辑,最后将zuulFilter执行的结果存入上下文RequestContext中并返回执行结果即可。

3.4、Zuul内置过滤器

Zuul过滤器可以通过自定义ZuulFilter实现类来添加,同时Zuul还提供了内置的众多过滤器,分别如下图示,比较核心的就是RibbonRoutingFilter,这个过滤器负载路由转发并且集成了Ribbon的负载均衡功能.

 

总结:

Zuul本质上就是一个Servlet,并且通过Spring容器管理了一系列的过滤器ZuulFilter实例,各个ZuulFilter有一个类型,包括preRoute、route、postRoute、error四种类型,不同的类型执行的顺序和时机不同。当请求进入Zuul时,由ZuulServlet接收并通过service方法处理。service方法逻辑就是从Spring容器中找到各种类型的ZuulFilter过滤器实例,然后遍历按顺序和时机来执行过滤器的处理逻辑。使用时可以自定义ZuulFilter来对请求的不同时间短进行功能扩展。

 

posted @ 2022-01-07 11:44  Lucky帅小武  阅读(514)  评论(0编辑  收藏  举报