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进行比较,两者还是有些差异:
-
- zuul是代码层面的,基于开发人员使用。而nginx是一颗工具,偏向于运维。
-
- zuul采用的类似spirngmvc dispatchservlet的异步阻塞模型,所有性能低于nginx。
-
- zuul可以和其他netflix产品无缝集成。
实际应用中zuul大多都作为集群,或者和nginx混合使用,最外层由nginx进行一层负载,在到zuul集群再进行路由转发。