1、基本配置
1.1 请求头:
默认有些敏感的请求头不会转发给后端,比如 cookie,set-cookie , authorization ,也可以自己配置敏感请求头
zuul:
sensitiveHeaders: accept-language,cookie
routes:
demo:
sensitiveHeaders:cookie
1.2 路由映射:
引入 actuator ,在配置文件中,配置 management.security.enabled 设置为 false ,就可以访问 /routes 地址,然后可以看到路由的映射信息
management:
security:
enabled: false
// result
{
"/demo/**": "https://www.baidu.com"
}
1.3 Hystrix 配置
与 ribbon 整合转发时,会使用 RibbonRoutingFilter ,转发会使用 hystrix 包裹请求,如果失败,执行降级逻辑
public class ServivceBFallbackProvider implements ZuulFallbackProvider {
@Override
public String getRoute() {
return null;
}
@Override
public ClientHttpResponse fallbackResponse() {
return null;
}
}
1.4 ribbon 客户端预加载
在默认情况下,第一次请求 zuul 才会初始化 ribbon 客户端,所以可以配置预加载,这样第一次就基本不会超时了。
zuul:
ribbon:
eager-load:
enabled: true
1. 5、超时配置
zuul 使用 ribbon + hystrix 那套东西,所以,超时要考虑 hystrix 和 ribbon ,而且 hystrix 的超时要考虑 riibon 的重试次数和单次超时时间
hystrix 超时计算公示: (ribbon.connectTimeOut + ribbon.ReadTimeOut) * (ribbon.MaxAutoRetires + 1) * (ribbon.MaxAutoRetriesNextServer + 1)
ribbon:
ReadTimeOut: 100
ConnectTimeOut: 500
MaxAutoRetries: 1
MaxAutoRetriesNextServer: 1
# 如果不配置 ribbon 的超时时间,默认的 hystrix 超时时间是 4000 ms
2、高级配置
2.1 过滤器优先级
pre 过滤器
-3 :ServletDetectionFilter
-2:Servlet30WrapperFilter
-1:FromBodyWrapperFilter
1:DebugFilter
5: PreDecorationFilter
routing 过滤器
10: RibbonRoutingFilter
100: SimpleHostRoutingFilter
500: SendForwardFilter : 负责路由跳转
post 过滤器
900 : locationRewriteFilter
1000: SendResponseFilter
error 过滤器
0: SendErrorFilter
2.2 自定义过滤器
public class MyFilterClass extends ZuulFilter {
@Override
public String filterType() {
// 在哪个阶段执行过滤其
return FilterConstants.ROUTE_TYPE;
}
@Override
public int filterOrder() {
// 设置过滤器,优先级
return 0;
}
@Override
public boolean shouldFilter() {
// 是否执行过滤其
return false;
}
@Override
public Object run() {
// 执行过滤器
return null;
}
}
@Configuration
public class FilterConfigClass {
@Bean
public MyFilterClass myFilter() {
return new MyFilterClass();
}
}
2.3 动态加载过滤器
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>2.4.12</version>
</dependency>
// 在Application类里
@PostConstruct
public void zuulInit() {
FilterLoader.getInstance().setCompiler(new GroovyCompiler());
String scriptRoot = System.getProperty(“zuul.filter.root”, “groovy/filters”);
String refreshInterval = System.getProperty(“zuul.filter.refreshInterval”, “5”);
if(scriptRoot.length() > 0) {
scriptRoot = scriptRoot + File.separator;
}
try {
FilterFileManager.setFilenameFilter(new GroovyFileFilter());
FilterFileManager.init(Integer.parseInt(refreshInterval), scriptRoot + “pre“, scriptRoot + “route”, scriptRoot + “post”);
} catch(Exception e) {
throw new RuntimeException(e);
}
}
yml 文件中,配置相关属性
zuul:
filter:
root: “groovy/filters” # 设置扫描路径
refreshInterval: 5 # 设置间隔时间
在src/main/java/groovy/filters中,放一个MyFilter.groovy
class MyFilter extends ZuulFilter {
public boolean shouldFilter() {
return true;
}
public Object run() {
System.out.println(“过滤器”);
return null;
}
public String filterType() {
return FilterConstants.ROUTE_TYPE;
}
public int filterOrder() {
return 1;
}
}
先启动网关项目,然后将这个过滤器放到指定目录,过几秒钟就会生效
2.4 禁用过滤器
zuul:
SendForwardFilter:
route:
disable: true
2.5 RequestContext
在过滤器中,使用RequestContext.getCurrentContext(),可以获取到serviceId、requestURI等各种东西
2.6 @EnableZuulServer
这个的话,就是自动禁用掉PreDecorationFilter、RibbonRoutingFilter、SimpleHostRoutingFilter等过滤器。。。
2.7 error过滤器
在自定义过滤器里搞一个异常抛出来,ZuulException
然后写一个MyErrorController,继承BasicErrorController,统一处理异常,打印一些信息,这就是统一异常处理
统一异常处理
统一认证
统一限流
统一降级
3、核心原理
具体的包在 package org.springframework.cloud:spring-cloud-netfix-core
3.1 EnableZuulProxy 注解
通过 import ZuulProxyMarkerConfiguration ,触发 ZuulProxyAutoConfiguration 的执行
@EnableCircuitBreaker // 显然,这开启了熔断
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(ZuulProxyMarkerConfiguration.class)
public @interface EnableZuulProxy {
}
@ConditionalOnBean(ZuulProxyMarkerConfiguration.Marker.class) ,这个意思是说,必须有一个 ZuulProxyMarkerConfiguration.Marker 作为 Spring 容器中的 bean ,然后才能触发 ZuulProxyAutoConfiguration 的执行
@Configuration
@Import({ RibbonCommandFactoryConfiguration.RestClientRibbonConfiguration.class,
RibbonCommandFactoryConfiguration.OkHttpRibbonConfiguration.class,
RibbonCommandFactoryConfiguration.HttpClientRibbonConfiguration.class,
HttpClientConfiguration.class })
@ConditionalOnBean(ZuulProxyMarkerConfiguration.Marker.class)
public class ZuulProxyAutoConfiguration extends ZuulServerAutoConfiguration {
}
3.2 入口类 ZuulServlet
@Override
public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {
try {
init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);
// Marks this request as having passed through the "Zuul engine", as opposed to servlets
// explicitly bound in web.xml, for which requests will not have the same data attached
RequestContext context = RequestContext.getCurrentContext();
context.setZuulEngineRan();
try {
preRoute();
} catch (ZuulException e) {
error(e);
postRoute();
return;
}
try {
route();
} catch (ZuulException e) {
error(e);
postRoute();
return;
}
try {
postRoute();
} catch (ZuulException e) {
error(e);
return;
}
} catch (Throwable e) {
error(new ZuulException(e, 500, "UNHANDLED_EXCEPTION_" + e.getClass().getName()));
} finally {
RequestContext.getCurrentContext().unset();
}
}
这里的话,会根据每次请求,都构建一个 RequestContext 对象,这个的话,为了保证每个请求相互隔离,采用了 threadLocal ,然后将原生的 servletRequest 交给 RequestContext ,将原生的 servletResponse 进行下包装,交给 RequestContext
public void init(HttpServletRequest servletRequest, HttpServletResponse servletResponse) {
RequestContext ctx = RequestContext.getCurrentContext();
if (bufferRequests) {
ctx.setRequest(new HttpServletRequestWrapper(servletRequest));
} else {
ctx.setRequest(servletRequest);
}
ctx.setResponse(new HttpServletResponseWrapper(servletResponse));
}
3.3 pre 过滤器
首先在 servlet 类中,会首先去执行 pre 中的5个过滤器,拿到 pre 中的5个过滤器,并且按照执行的优先级进行排序。
public Object runFilters(String sType) throws Throwable {
if (RequestContext.getCurrentContext().debugRouting()) {
Debug.addRoutingDebug("Invoking {" + sType + "} type filters");
}
boolean bResult = false;
// 这个就是负责拿到对应过滤器中的 filter ,并且是按照优先级排好序的 。
List<ZuulFilter> list = FilterLoader.getInstance().getFiltersByType(sType);
if (list != null) {
for (int i = 0; i < list.size(); i++) {
ZuulFilter zuulFilter = list.get(i);
Object result = processZuulFilter(zuulFilter);
if (result != null && result instanceof Boolean) {
bResult |= ((Boolean) result);
}
}
}
return bResult;
}
首先要执行的是 ServletDetectionFilter 过滤器,这个逻辑很简单,就是在 requestContext 中设置 IS_DISPATCHER_SERVLET_REQUEST_KEY 属性为 true。
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
if (!(request instanceof HttpServletRequestWrapper)
&& isDispatcherServletRequest(request)) {
ctx.set(IS_DISPATCHER_SERVLET_REQUEST_KEY, true);
} else {
ctx.set(IS_DISPATCHER_SERVLET_REQUEST_KEY, false);
}
return null;
}
然后会执行 Servlet30WrapperFilter 过滤器,这个逻辑很简单,在上一个过滤器给那个属性设置了 true ,所以它这的话,就是简单的构建了一个 Servlet30RequestWrapper 实例,放到 requestContext 对象中
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
if (request instanceof HttpServletRequestWrapper) {
request = (HttpServletRequest) ReflectionUtils.getField(this.requestField,
request);
ctx.setRequest(new Servlet30RequestWrapper(request));
}
else if (RequestUtils.isDispatcherServletRequest()) {
// If it's going through the dispatcher we need to buffer the body
ctx.setRequest(new Servlet30RequestWrapper(request));
}
return null;
}
之后会走 FormBodyWrapperFilter 过滤器,这里面就是简单通过 FormBodyRequestWrapper 给包装了一下,在放到 requestContext 中,默认的话,这个是不执行的,除非 MediaType 类型是 APPLICATION_FORM_URLENCODED 或者 MULTIPART_FORM_DATA 才会执行
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
FormBodyRequestWrapper wrapper = null;
if (request instanceof HttpServletRequestWrapper) {
HttpServletRequest wrapped = (HttpServletRequest) ReflectionUtils
.getField(this.requestField, request);
wrapper = new FormBodyRequestWrapper(wrapped);
ReflectionUtils.setField(this.requestField, request, wrapper);
if (request instanceof ServletRequestWrapper) {
ReflectionUtils.setField(this.servletRequestField, request, wrapper);
}
}
else {
wrapper = new FormBodyRequestWrapper(request);
ctx.setRequest(wrapper);
}
if (wrapper != null) {
ctx.getZuulRequestHeaders().put("content-type", wrapper.getContentType());
}
return null;
}
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String contentType = request.getContentType();
// Don't use this filter on GET method
if (contentType == null) {
return false;
}
// Only use this filter for form data and only for multipart data in a
// DispatcherServlet handler
try {
MediaType mediaType = MediaType.valueOf(contentType);
return MediaType.APPLICATION_FORM_URLENCODED.includes(mediaType)
|| (isDispatcherServletRequest(request)
&& MediaType.MULTIPART_FORM_DATA.includes(mediaType));
}
catch (InvalidMediaTypeException ex) {
return false;
}
}
然后是 DebugFilter 过滤器,这个的话,就是设置 degu 属性,会在后面多打印一些 debug 日志,这个的话,默认也是不执行的,除非 设置了 debug 属性 为 true ,才会执行
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
ctx.setDebugRouting(true);
ctx.setDebugRequest(true);
return null;
}
public boolean shouldFilter() {
HttpServletRequest request = RequestContext.getCurrentContext().getRequest();
if ("true".equals(request.getParameter(DEBUG_PARAMETER.get()))) {
return true;
}
return ROUTING_DEBUG.get();
}
最后是 PreDecorationFilter 过滤器,这个逻辑比较复杂,主要的核心就是 获取到请求地址,和配置文件中配置的路由规则进行匹配,然后往 requestContext 里面设置相关属性,比如是否重试,比如 host 地址, location 请求地址等等
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
final String requestURI = this.urlPathHelper.getPathWithinApplication(ctx.getRequest());
Route route = this.routeLocator.getMatchingRoute(requestURI);
if (route != null) {
String location = route.getLocation();
if (location != null) {
ctx.put(REQUEST_URI_KEY, route.getPath());
ctx.put(PROXY_KEY, route.getId());
if (!route.isCustomSensitiveHeaders()) {
this.proxyRequestHelper
.addIgnoredHeaders(this.properties.getSensitiveHeaders().toArray(new String[0]));
}
else {
this.proxyRequestHelper.addIgnoredHeaders(route.getSensitiveHeaders().toArray(new String[0]));
}
if (route.getRetryable() != null) {
ctx.put(RETRYABLE_KEY, route.getRetryable());
}
if (location.startsWith(HTTP_SCHEME+":") || location.startsWith(HTTPS_SCHEME+":")) {
ctx.setRouteHost(getUrl(location));
ctx.addOriginResponseHeader(SERVICE_HEADER, location);
}
else if (location.startsWith(FORWARD_LOCATION_PREFIX)) {
ctx.set(FORWARD_TO_KEY,
StringUtils.cleanPath(location.substring(FORWARD_LOCATION_PREFIX.length()) + route.getPath()));
ctx.setRouteHost(null);
return null;
}
else {
// set serviceId for use in filters.route.RibbonRequest
ctx.set(SERVICE_ID_KEY, location);
ctx.setRouteHost(null);
ctx.addOriginResponseHeader(SERVICE_ID_HEADER, location);
}
if (this.properties.isAddProxyHeaders()) {
addProxyHeaders(ctx, route);
String xforwardedfor = ctx.getRequest().getHeader(X_FORWARDED_FOR_HEADER);
String remoteAddr = ctx.getRequest().getRemoteAddr();
if (xforwardedfor == null) {
xforwardedfor = remoteAddr;
}
else if (!xforwardedfor.contains(remoteAddr)) { // Prevent duplicates
xforwardedfor += ", " + remoteAddr;
}
ctx.addZuulRequestHeader(X_FORWARDED_FOR_HEADER, xforwardedfor);
}
if (this.properties.isAddHostHeader()) {
ctx.addZuulRequestHeader(HttpHeaders.HOST, toHostHeader(ctx.getRequest()));
}
}
}
else {
log.warn("No route found for uri: " + requestURI);
String fallBackUri = requestURI;
String fallbackPrefix = this.dispatcherServletPath; // default fallback
// servlet is
// DispatcherServlet
if (RequestUtils.isZuulServletRequest()) {
// remove the Zuul servletPath from the requestUri
log.debug("zuulServletPath=" + this.properties.getServletPath());
fallBackUri = fallBackUri.replaceFirst(this.properties.getServletPath(), "");
log.debug("Replaced Zuul servlet path:" + fallBackUri);
}
else {
// remove the DispatcherServlet servletPath from the requestUri
log.debug("dispatcherServletPath=" + this.dispatcherServletPath);
fallBackUri = fallBackUri.replaceFirst(this.dispatcherServletPath, "");
log.debug("Replaced DispatcherServlet servlet path:" + fallBackUri);
}
if (!fallBackUri.startsWith("/")) {
fallBackUri = "/" + fallBackUri;
}
String forwardURI = fallbackPrefix + fallBackUri;
forwardURI = forwardURI.replaceAll("//", "/");
ctx.set(FORWARD_TO_KEY, forwardURI);
}
return null;
}
3.4 PreDecorationFilter 路由解析
zuul:
routes:
ServiceB:
path: /demo/**
Map<String, ZuulRoute>
key = /demo/**
value = ZuulRoute(serviceId = “ServiceB”)
/demo/**
/demo/ServiceB/user/sayHello/1
是否匹配,如果匹配的话,就直接返回ZuulRoute,就是路由规则
ZuulRoute{id='ServiceB', path='/demo/**', serviceId='ServiceB', url='null', stripPrefix=true, retryable=null, sensitiveHeaders=[], customSensitiveHeaders=false, }
targetPath = /ServiceB/user/sayHello/1
prefix = /demo
return new Route(route.getId(), targetPath, route.getLocation(), prefix,
retryable,
route.isCustomSensitiveHeaders() ? route.getSensitiveHeaders() : null,
route.isStripPrefix());
创建了一个Route对象,先搞到了一个ZuulRoute的这么一个东西,这个东西里面封装了一些基本的路由规则,然后对这个ZuulRoute再次进行了解析,以及一些转换,尤其是处理出来了几个数据,封装了一个Route对象
**Route{id='ServiceB', fullPath='/demo/ServiceB/user/sayHello/1', path='/ServiceB/user/sayHello/1', location='ServiceB', prefix='/demo', retryable=false, sensitiveHeaders=[], customSensitiveHeaders=false, prefixStripped=true},**接下来就是将Route路由规则中的各种信息给放在了RequestContext中了,给下一个阶段route过滤器来使用
3.5 rout 过滤器
在上面的 pre 过滤器执行完毕之后,就会去执行 rout 过滤器,首先是会去执行 RibbonRoutingFilter 过滤器,这个的话,比较关键,在这就通过 ribbon 进行负载均衡发送请求到对应的服务了
public Object run() {
RequestContext context = RequestContext.getCurrentContext();
this.helper.addIgnoredHeaders();
try {
RibbonCommandContext commandContext = buildCommandContext(context);
ClientHttpResponse response = forward(commandContext);
setResponse(response);
return response;
}
catch (ZuulException ex) {
throw new ZuulRuntimeException(ex);
}
catch (Exception ex) {
throw new ZuulRuntimeException(ex);
}
}
然后会执行 SimpleHostRoutingFilter , 将请求转发到某个 rul 地址,默认不执行,最后 SendForwardFilter , 将请求转发到 zuul 自己的地址,默认不执行。
3.6 RibbonRoutingFilter 服务转发
这里的话,主要是执行 forward() 方法的执行,构建出来 RibbonCommand,这个的话,本质上来说就是 HystrixCommand ,那么他在执行的时候,就是在执行 Hystrix Command 进行执行,熔断限流等等操作。
protected ClientHttpResponse forward(RibbonCommandContext context) throws Exception {
Map<String, Object> info = this.helper.debug(context.getMethod(),
context.getUri(), context.getHeaders(), context.getParams(),
context.getRequestEntity());
RibbonCommand command = this.ribbonCommandFactory.create(context);
try {
ClientHttpResponse response = command.execute();
this.helper.appendDebug(info, response.getRawStatusCode(), response.getHeaders());
return response;
}
catch (HystrixRuntimeException ex) {
return handleException(info, ex);
}
}
在创建 RibbonCommand 的时候,会初始化 ribbon 相关的东西,通过 ribbon 进行负载均衡的操作,由 ZoneAwareLoadBaclancer 中的组件从 eureka client 中拉取注册表,根据服务对应的服务列表,通过负载均衡算法(chooseService) 选择一个要请求的服务地址出来,交由 hystrix 进行执行。
public HttpClientRibbonCommand create(final RibbonCommandContext context) {
ZuulFallbackProvider zuulFallbackProvider = getFallbackProvider(context.getServiceId());
final String serviceId = context.getServiceId();
final RibbonLoadBalancingHttpClient client = this.clientFactory.getClient(
serviceId, RibbonLoadBalancingHttpClient.class);
client.setLoadBalancer(this.clientFactory.getLoadBalancer(serviceId));
return new HttpClientRibbonCommand(serviceId, client, context, zuulProperties, zuulFallbackProvider,
clientFactory.getClientConfig(serviceId));
}
3.7 post 过滤器
post 的话,会执行两个过滤器,LocationRewriteFilter,SendResponseFilter,前者的话,只有在需要进行重定向也就是 3xx 状态码的时候,才会执行,将结果进行重定向,后者就是直接将请求结果写给浏览器。。
3.8 error 过滤器
在上述中任意的过滤器抛出异常,都会执行这个过滤器,SendErrorFilter
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
// only forward to errorPath if it hasn't been forwarded to already
return ctx.getThrowable() != null
&& !ctx.getBoolean(SEND_ERROR_FILTER_RAN, false);
}
public Object run() {
try {
RequestContext ctx = RequestContext.getCurrentContext();
ZuulException exception = findZuulException(ctx.getThrowable());
HttpServletRequest request = ctx.getRequest();
request.setAttribute("javax.servlet.error.status_code", exception.nStatusCode);
log.warn("Error during filtering", exception);
request.setAttribute("javax.servlet.error.exception", exception);
if (StringUtils.hasText(exception.errorCause)) {
request.setAttribute("javax.servlet.error.message", exception.errorCause);
}
RequestDispatcher dispatcher = request.getRequestDispatcher(
this.errorPath);
if (dispatcher != null) {
ctx.set(SEND_ERROR_FILTER_RAN, true);
if (!ctx.getResponse().isCommitted()) {
ctx.setResponseStatusCode(exception.nStatusCode);
dispatcher.forward(request, ctx.getResponse());
}
}
}
catch (Exception ex) {
ReflectionUtils.rethrowRuntimeException(ex);
}
return null;
}