spring cloud zuul运用与源码分析

zuul简介

zuul作为一个微服务的路由网关组件,主要用于动态路由,过滤,监控以及安全方面。通俗的讲,可以理解为zuul就是系统的守门人,所有访问后端服务的请求都得经过他。
zuul 主要的功能点:

  • 实现智能路由和负载均衡,可以按照某种策略将请求流量分配到集群实例服务上。
  • 所有api接口由网关进行统一聚合及暴露,外部调用无法知道接口的调用过程,保护了敏感信息。
  • 网关可以做接口的用户认证以及权限认证,可以防止非法请求api,提高了安全性。
  • 网关可以实现监控,对每个请求的调用过程进行监控以及日志记录,出先异常方便定位。
  • 对流量进行监控,可以实现升降级功能。

项目搭建

pom依赖的添加

添加zuul以及eureka client的依赖

   <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
        </dependency>

##application.properties配置文件的修改

# 服务名称
spring.application.name=spring-zuul
# 服务端口
server.port=8001
# 指定子hizuul对应的服务名称, zuul.routes后跟上的服务名称自定义,笔者暂定义一个为hizuul
zuul.routes.hizuul.service-id=eureka-client
# 指定访问子服务的路径
zuul.routes.hizuul.path=/hizuul/**
# 访问eureka 注册中心
eureka.client.serviceUrl.defaultZone=http://localhost:1101/eureka/
# 显示指定hizuul的访问地址,使用url配置的时候,就不需要zuul.routes.hizuul.service-id
这个配置,没啥意义,而且也不会进行负责均衡,如果要进行负载均衡,就得本地维护一份服务列表
并且不从注册中心获取最新的服务列表
#zuul.routes.hizuul.url=localhost:2101
# 给所有接口加个版本号前缀
#zuul.prefix=v2

启动文件的修改

本模块基于spring boot 2.0以上,所以spring cloud已经默认集成了eureka client的启动依赖,不需要显示的编码服务发现注解。

//  添加zuul注解开启zuul服务
@EnableZuulProxy
@SpringBootApplication
public class ZuulApplication {

    public static void main(String[] args) {
        SpringApplication.run(ZuulApplication.class, args);
    }

}

接下来启动,eureka server 端,以及eureka client端。访问http://localhost:8001/hizuul/hello/?name=aa。
在这里插入图片描述
观察到zuul已经正确生效。如果启动多个相同eureka client实例,不停访问的时候,zuul会自动做负载均衡。

zuul的熔断功能实现

只需做好相应配置,zuul就会自主实现负载均衡,若实现熔断功能,则需要进行代码层面的实现。

/**实现熔断功能必须实现FallbackProvider接口 */
public class ZuulFallback implements FallbackProvider {
    /** getRoute方法指定了熔断器作用的服务,如果要指定多个服务,用逗号来分割,指定全部服务,直接return ”*“ 就可以了 */
    @Override
    public String getRoute() {
        return "eureka-client";
    }

   /** fallbackResponse方法熔断功能的执行逻辑 */
    @Override
    public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
        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("service is error , fallback".getBytes());
            }

            @Override
            public HttpHeaders getHeaders() {
                HttpHeaders headers = new HttpHeaders();
                headers.setContentType(MediaType.APPLICATION_JSON);
                return headers;
            }
        };
    }
}

此时启动应用,关掉对应的eureka-client服务,使用浏览器去访问http://localhost:8001/hizuul/hello/?name=aaj 就可以得出我们预期的结果。
在这里插入图片描述

zuul过滤功能的实现

实现zuul的过滤功能需要继承ZuulFilter类

@Component
public class MyFilter extends ZuulFilter {
/** 过滤器类型,枚举值ERROR_TYPE,POST_TYPE,PRE_TYPE,ROUTE_TYPE */
    @Override
    public String filterType() {
        return PRE_TYPE;
    }
    
/** 过滤顺序,值越小越优先过滤 */
    @Override
    public int filterOrder() {
        return 0;
    }
    
/** 是否过滤,true执行run()方法, false则不执行*/
    @Override
    public boolean shouldFilter() {
        return true;
    }

/** 自定义过滤器的具体实现逻辑,判断消息参数里是否有token*/
    @Override
    public Object run() throws ZuulException {
        RequestContext requestContext = RequestContext.getCurrentContext();
        HttpServletRequest servletRequest = requestContext.getRequest();
        Object token = servletRequest.getParameter("token");
        if (token == null) {
            requestContext.setSendZuulResponse(false);
            requestContext.setResponseStatusCode(401);
            try {
                requestContext.getResponse().getWriter().write("the token is empty");
            } catch (IOException e) {
                e.printStackTrace();
                return null;
            }
        }
        return null;
    }
}

此时启动应用,使用浏览器去访问http://localhost:8001/hizuul/hello/?name=aa&&token==bb就可以得出我们预期的结果。
在这里插入图片描述
不加token这个参数,再运行一下
在这里插入图片描述
通常安全验证就是通过过滤器来实现的

源码解析

首先来看@EnableZuulProxy 这个注解


@EnableCircuitBreaker
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Import({ZuulProxyMarkerConfiguration.class})
public @interface EnableZuulProxy 

导入了ZuulProxyMarkerConfiguration类,进入这个类下,查看


@Configuration
public class ZuulProxyMarkerConfiguration {
    public ZuulProxyMarkerConfiguration() {
    }

    @Bean
    public ZuulProxyMarkerConfiguration.Marker zuulProxyMarkerBean() {
        return new ZuulProxyMarkerConfiguration.Marker();
    }

/** 通过cglib代理实例化一个maker */
    class Marker {
        Marker() {
        }
    }
}

debug,查看运行记录
在这里插入图片描述
知道进入了到了ZuulProxyAutoConfiguration 这个类里,查看下类代码

@Configuration
/** 注入个各种http连接器的负载均衡类 */
@Import({RestClientRibbonConfiguration.class, OkHttpRibbonConfiguration.class, HttpClientRibbonConfiguration.class, HttpClientConfiguration.class})
/** 标明了maker所指向的bean
@ConditionalOnBean({Marker.class})
public class ZuulProxyAutoConfiguration extends ZuulServerAutoConfiguration {
    @Autowired(
/** 检测DiscoveryClient是否缺失 */
 @Bean
    @ConditionalOnMissingBean({DiscoveryClientRouteLocator.class})
    public DiscoveryClientRouteLocator discoveryRouteLocator() {
        return new DiscoveryClientRouteLocator(this.server.getServlet().getContextPath(), this.discovery, this.zuulProperties, this.serviceRouteMapper, this.registration);
    }
/** 检测pre类型f的ilter的bean是否缺失 */
    @Bean
    @ConditionalOnMissingBean({PreDecorationFilter.class})
    public PreDecorationFilter preDecorationFilter(RouteLocator routeLocator, ProxyRequestHelper proxyRequestHelper) {
        return new PreDecorationFilter(routeLocator, this.server.getServlet().getContextPath(), this.zuulProperties, proxyRequestHelper);
    }
    /** 检测route类型f的ilter的bean是否缺失 */
@Bean
    @ConditionalOnMissingBean({RibbonRoutingFilter.class})
    public RibbonRoutingFilter ribbonRoutingFilter(ProxyRequestHelper helper, RibbonCommandFactory<?> ribbonCommandFactory) {
        RibbonRoutingFilter filter = new RibbonRoutingFilter(helper, ribbonCommandFactory, this.requestCustomizers);
        return filter;
    }

    @Bean
    @ConditionalOnMissingBean({SimpleHostRoutingFilter.class, CloseableHttpClient.class})
    public SimpleHostRoutingFilter simpleHostRoutingFilter(ProxyRequestHelper helper, ZuulProperties zuulProperties, ApacheHttpClientConnectionManagerFactory connectionManagerFactory, ApacheHttpClientFactory httpClientFactory) {
        return new SimpleHostRoutingFilter(helper, zuulProperties, connectionManagerFactory, httpClientFactory);
    }

    @Bean
    @ConditionalOnMissingBean({SimpleHostRoutingFilter.class})
    public SimpleHostRoutingFilter simpleHostRoutingFilter2(ProxyRequestHelper helper, ZuulProperties zuulProperties, CloseableHttpClient httpClient) {
        return new SimpleHostRoutingFilter(helper, zuulProperties, httpClient);
    }

    @Bean
    @ConditionalOnMissingBean({ServiceRouteMapper.class})
    public ServiceRouteMapper serviceRouteMapper() {
        return new SimpleServiceRouteMapper();
    }

这个ZuulProxyAutoConfiguration继承了ZuulServerAutoConfiguration这个类。断点放进其父类里,debug观察到
在这里插入图片描述看一下ZuulServerAutoConfiguration

@Configuration
/** 注入了zuul的属性类 */
@EnableConfigurationProperties({ZuulProperties.class})
/** 注入了zuulservlet, zuulservletfilter */
@ConditionalOnClass({ZuulServlet.class, ZuulServletFilter.class})
@ConditionalOnBean({Marker.class})
public class ZuulServerAutoConfiguration {
/** 类似springmvc */
  @Bean
    public ZuulController zuulController() {
        return new ZuulController();
    }

    @Bean
    public ZuulHandlerMapping zuulHandlerMapping(RouteLocator routes) {
        ZuulHandlerMapping mapping = new ZuulHandlerMapping(routes, this.zuulController());
        mapping.setErrorController(this.errorController);
        mapping.setCorsConfigurations(this.getCorsConfigurations());
        return mapping;
    }

/** 检查zuulservletbean,不存在就new一个 */ 
 @Bean
    @ConditionalOnMissingBean(
        name = {"zuulServlet"}
    )
    @ConditionalOnProperty(
        name = {"zuul.use-filter"},
        havingValue = "false",
        matchIfMissing = true
    )
    public ServletRegistrationBean zuulServlet() {
        ServletRegistrationBean<ZuulServlet> servlet = new ServletRegistrationBean(new ZuulServlet(), new String[]{this.zuulProperties.getServletPattern()});
        servlet.addInitParameter("buffer-requests", "false");
        return servlet;
    }
/**内部类里将filte初始化 */
 @Configuration
    protected static class ZuulFilterConfiguration {
        @Autowired
        private Map<String, ZuulFilter> filters;

        protected ZuulFilterConfiguration() {
        }

        @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);
        }
    }

接下来介绍一下ZuulServlet,Zuulservlet作为类似于Spring MVC中的DispatchServlet,起到了前端控制器的作用,所有的请求都由它接管。它的核心代码如下

 public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        try {
        /** 调用初始化 */
            this.init((HttpServletRequest)servletRequest, (HttpServletResponse)servletResponse);
            RequestContext context = RequestContext.getCurrentContext();
            context.setZuulEngineRan();

/** 顺序执行过滤器方法 */
            try {
                this.preRoute();
            } catch (ZuulException var13) {
                this.error(var13);
                this.postRoute();
                return;
            }

            try {
                this.route();
            } catch (ZuulException var12) {
                this.error(var12);
                this.postRoute();
                return;
            }

            try {
                this.postRoute();
            } catch (ZuulException var11) {
                this.error(var11);
            }
        } catch (Throwable var14) {
            this.error(new ZuulException(var14, 500, "UNHANDLED_EXCEPTION_" + var14.getClass().getName()));
        } finally {
            RequestContext.getCurrentContext().unset();
        }
    }

点进init方法,往下走一直定位到ZuulRunner类里

/** 给当前的RequestContext赋值 */
public void init(HttpServletRequest servletRequest, HttpServletResponse servletResponse) {
        RequestContext ctx = RequestContext.getCurrentContext();
        if (this.bufferRequests) {
            ctx.setRequest(new HttpServletRequestWrapper(servletRequest));
        } else {
            ctx.setRequest(servletRequest);
        }

        ctx.setResponse(new HttpServletResponseWrapper(servletResponse));
    }

不知道读者是否发现,很多方法以及笔者自己写的filter里RequestContext的取值都是通过

RequestContext.getCurrentContext();

来获取的。zuul是如何保证RequestContext的全局唯一性呢

/ **通过源码可知,RequestContext继承了ConcurrentHashMap,而ConcurrentHashMap是线程安全的,所以
*RequestContext也是安全的 */
public class RequestContext extends ConcurrentHashMap<String, Object> {

回到之前的service()方法,可知zuul第一个执行的pre类型的过滤器,接着就看一下preRoute()的实现,往下走最终定位到FilterProcessor的runFilters 方法

 public Object runFilters(String sType) throws Throwable {
        if (RequestContext.getCurrentContext().debugRouting()) {
            Debug.addRoutingDebug("Invoking {" + sType + "} type filters");
        }

        boolean bResult = false;
        List<ZuulFilter> list = FilterLoader.getInstance().getFiltersByType(sType);
        if (list != null) {
        // 循坏调用processZuulFilter方法
            for(int i = 0; i < list.size(); ++i) {
                ZuulFilter zuulFilter = (ZuulFilter)list.get(i);
                Object result = this.processZuulFilter(zuulFilter);
                if (result != null && result instanceof Boolean) {
                    bResult |= (Boolean)result;
                }
            }
        }

        return bResult;
    }
 public Object processZuulFilter(ZuulFilter filter) throws ZuulException {
        RequestContext ctx = RequestContext.getCurrentContext();
        boolean bDebug = ctx.debugRouting();
        String metricPrefix = "zuul.filter-";
        long execTime = 0L;
        String filterName = "";

        try {
            long ltime = System.currentTimeMillis();
            filterName = filter.getClass().getSimpleName();
            RequestContext copy = null;
            Object o = null;
            Throwable t = null;
            if (bDebug) {
                Debug.addRoutingDebug("Filter " + filter.filterType() + " " + filter.filterOrder() + " " + filterName);
                copy = ctx.copy();
            }

// 获取调用结果
            ZuulFilterResult result = filter.runFilter();
            ExecutionStatus s = result.getStatus();
            execTime = System.currentTimeMillis() - ltime;
            switch(s) {
            case FAILED:
                t = result.getException();
                ctx.addFilterExecutionSummary(filterName, ExecutionStatus.FAILED.name(), execTime);
                break;
            case SUCCESS:
                o = result.getResult();
                ctx.addFilterExecutionSummary(filterName, ExecutionStatus.SUCCESS.name(), execTime);
                if (bDebug) {
                    Debug.addRoutingDebug("Filter {" + filterName + " TYPE:" + filter.filterType() + " ORDER:" + filter.filterOrder() + "} Execution time = " + execTime + "ms");
                    Debug.compareContextState(filterName, copy);
                }
            }

            if (t != null) {
                throw t;
            } else {
                this.usageNotifier.notify(filter, s);
                return o;
            }
        } catch (Throwable var15) {
            if (bDebug) {
                Debug.addRoutingDebug("Running Filter failed " + filterName + " type:" + filter.filterType() + " order:" + filter.filterOrder() + " " + var15.getMessage());
            }

            this.usageNotifier.notify(filter, ExecutionStatus.FAILED);
            if (var15 instanceof ZuulException) {
                throw (ZuulException)var15;
            } else {
                ZuulException ex = new ZuulException(var15, "Filter threw Exception", 500, filter.filterType() + ":" + filterName);
                ctx.addFilterExecutionSummary(filterName, ExecutionStatus.FAILED.name(), execTime);
                throw ex;
            }
        }
    }
/** 实例化ZuulFilterResult并输出 */
public ZuulFilterResult runFilter() {
        ZuulFilterResult zr = new ZuulFilterResult();
        if (!this.isFilterDisabled()) {
            if (this.shouldFilter()) {
                Tracer t = TracerFactory.instance().startMicroTracer("ZUUL::" + this.getClass().getSimpleName());

                try {
                    Object res = this.run();
                    zr = new ZuulFilterResult(res, ExecutionStatus.SUCCESS);
                } catch (Throwable var7) {
                    t.setName("ZUUL::" + this.getClass().getSimpleName() + " failed");
                    zr = new ZuulFilterResult(ExecutionStatus.FAILED);
                    zr.setException(var7);
                } finally {
                    t.stopAndLog();
                }
            } else {
                zr = new ZuulFilterResult(ExecutionStatus.SKIPPED);
            }
        }

        return zr;
    }

到此,我们已看到zuul过滤器执行的所有过程。

zuul和nginx的比较

很多人都喜欢拿zuul和nginx进行比较,两者还是有些差异:

    1. zuul是代码层面的,基于开发人员使用。而nginx是一颗工具,偏向于运维。
    1. zuul采用的类似spirngmvc dispatchservlet的异步阻塞模型,所有性能低于nginx。
    1. zuul可以和其他netflix产品无缝集成。

实际应用中zuul大多都作为集群,或者和nginx混合使用,最外层由nginx进行一层负载,在到zuul集群再进行路由转发。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值