spring cloud 和 netfix zuul
- 网关中可以实现的横切关注点:
- 静态路由;
- 动态路由;
- 验证和授权:
- 度量数据收集和日志记录;
- 构建zuul:
- 导入依赖包;
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zuul</artifactId> </dependency>
-
添加注解,有两个注解可用:
-
@EnableZuulProxy:会自动使用eureka来通过服务ID查询服务,其核心是反向代理,捕获客户端请求然后代表客户端访问远程资源,客户端不知道最终调用的服务;
-
@EnableZuulServer:不会加载任何zuul反向代理过滤器,也不会使用eureka进行服务发现,当我们构建自己的路由服务而不使用任何zuul预置的功能时使用该注解,如与别的服务发现引擎(如consul)进行集成时使用该注解;
-
-
添加配置与eureka进行通信;
server: port: 5555 eureka: instance: preferIpAddress: true client: registerWithEureka: true fetchRegistry: true serviceUrl: defaultZone: http://localhost:8761/eureka/
-
为所有服务调用添加前缀;
zuul: prefix: /api
- 导入依赖包;
-
在zuul中配置路由,有几种zuul路由机制,可以通过http://ip:port/routes查看所有的路径;
-
通服务发现自动映射路由:不需要通过配置,zuul可以根据其服务ID自动路由请求,如果没有指定任何路由,zuul将根据eureka中的服务ID进行路由;
-
http://ip:port/服务ID/具体的url请求:端口后面第一个被用于查找具体服务,后面的则是具体的路径;
-
关闭个别服务或全部服务的自动路径映射;
zuul: #屏蔽全部服务的自动路由,则设置'*' ignored-services: '服务名1','服务名2'
-
-
使用服务发现手动映射路由;
zuul: routes: # 服务名: /路径/** organizationservice: /organization/**
自动映射和手动映射的区别:在使用自动映射路由时,zuul只会基于eureka服务ID来公开服务,如果服务实例没有运行,zuul将不会公开该服务的路由,而在没有使用eureka注册服务实例的情况下,手动映射路由仍然会将请求映射到服务ID。
-
使用静态URL手动映射路由,可以用了路由不受eureka管理的服务;
-
不使用负载均衡:
zuul: routes: #服务名 organizationservice: #zuul用于在内部识别服务的关键字 path: /organization/** #静态路由 url: http://ip:port #服务的静态实例,将被直接调用,而不是zuul通过eureka调用
此时只能有一条路径,而不能使用负载均衡路由到各个实例。
-
开启rabbon负载均衡,此时需要禁用掉ribbon和eureka的集成,并列出ribbon进行负载均衡的各个实例;
#在ribbon中禁用掉eureka支持 ribbon: eureka: enabled: false zuul: routes: organizationservice: path: /organization/** #静态路由 serviceId: organizationservice #定义一个服务ID,该服务ID用于在ribbon中查找服务 organizationservice: #与serviceId一致 ribbon: listOfServices: http://ip:8081,http://ip:8082 #指定请求会路由到的服务列表
注意:(1)静态路由并在ribbon中禁用eureka会造成一个问题,那就是禁用了对通过zuul网关运行的所有服务的ribbon支持,意味着eureka会承受跟多的负载,因为zuul无法使用ribbon来缓存服务的查找。
(2)ribbon不会在每次发出调用时都去调用eureka,它会在本地缓存服务实例的位置,然后定期检查eureka是否有变化,缺少了ribbon,zuul每次需要解析服务的位置时都需要调用eureka来进行定位。
-
-
zuul和服务超时,zuul使用netfix的hystrix和ribbon库来防止长时间运行的服务调用影响网关的性能;
-
hystrix默认的超时间时间为1s,通过以下配置进行更改:
hystrix: command: default: execution: isolation: thread: timeoutInMilliseconds: 2500 服务ID: #通过替代defalut来设置某个服务单独的hystrix超时时间 execution: isolation: thread: timeoutInMilliseconds: 3000
-
ribbon的默认超时时间为5s,可以通过以下配置进行修改:
#修改某个服务的ribbon超时时间 服务ID.ribbon.ReadTimeout: 7000
-
-
-
zuul的真正威力:过滤器;
-
servlet过滤器和Spring Aspect被本地化为特定服务,而zuul过滤器允许为通过zuul路由的所有服务实现横切关注点;
-
zuul支持三种类型的过滤器:
-
前置过滤器:将请求发送到实际目的地前调用,可以对http请求进行检查和修改,但是前置过滤器不能进行重定向到别的端点或服务;
-
后置过滤器:在调用目标服务后来自目标服务的响应会经过该过滤器,可用来检查和修改来自被调用服务的响应,通常用来记录从目标服务返回的响应、处理错误或审核对敏感信息的响应;
-
路由过滤器:可以将请求路由到其他服务,路由过滤器不能执行http重定向,而是会终止传入的http请求,然后创建新请求并代表原始调用者调用路由;
-
-
构建前置过滤器,通过扩展ZuulFilter实现一个从zuul流出的每个请求都有关联ID;
package com.thoughtmechanix.zuulsvr.filters; import com.netflix.zuul.ZuulFilter; import com.netflix.zuul.context.RequestContext; import org.springframework.stereotype.Component; /** * 所有zuul过滤器必须扩展ZuulFilter类 */ @Component public class TrackingFilter extends ZuulFilter { /** * 定义过滤器类型,前置、后置、路由 */ @Override public String filterType() { return "pre"; } /** * 定义过滤器执行顺序 */ @Override public int filterOrder() { return 1; } /** * 是否要执行过滤器 */ public boolean shouldFilter() { return true; } /** * 每次服务通过过滤器时执行run方法 */ public Object run() { if (!isCorrelationIdPresent()) { setCorrelationId(generateCorrelationId()); } RequestContext ctx = RequestContext.getCurrentContext(); return null; } private String getCorrelationId() { RequestContext ctx = RequestContext.getCurrentContext(); if (ctx.getRequest().getHeader("tmx-correlation-id") != null) { return ctx.getRequest().getHeader("tmx-correlation-id"); } else { return ctx.getZuulRequestHeaders().get("tmx-correlation-id"); } } private void setCorrelationId(String correlationId) { RequestContext ctx = RequestContext.getCurrentContext(); ctx.addZuulRequestHeader("tmx-correlation-id", correlationId); } private boolean isCorrelationIdPresent() { return getCorrelationId() != null; } private String generateCorrelationId() { return java.util.UUID.randomUUID().toString(); } }
注意:(1)在一般的spring mvc和spring boot服务中,RequestContext是org.springframework.web.servletsupport.RequestContext类型的,而zuul提供了一个专门的RequestContext(com.netflix.zuul.context.RequestContext),它具有额外几个方法拉访问zuul特定的值。
(2)zuul不允许直接添加或修改传入请求中的HTTP头部,要向http头部添加请求头,应使用RequestContext 的addZuulRequestHeader()方法,该方法将维护一个单独的HTTP头部映射,当zuul服务器调用目标服务是,包含在ZuulRequestHeader映射中的数据将被合并。 -
当zuul调用目标服务时,在目标服务调用中使用前置过滤器添加的关联ID;
-
要求:被zuul调用的服务可以很容易访问到关联ID;被调用的服务在调用其他服务时也需要将关联ID传递下去。
-
定义servlet过滤器,拦截进入服务的请求,获取头部信息;
package com.thoughtmechanix.licenses.utils; import org.springframework.stereotype.Component; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import java.io.IOException; @Component public class UserContextFilter implements Filter { @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; //获取http头部信息,并将其映射到UserContext类(pojo),报错存头部获取的信息,方便之后使用 UserContextHolder.getContext().setCorrelationId(httpServletRequest.getHeader("tmx-correlation-id")); UserContextHolder.getContext().setUserId(httpServletRequest.getHeader("tmx-user-id")); UserContextHolder.getContext().setAuthToken(httpServletRequest.getHeader("tmx-auth-token")); UserContextHolder.getContext().setOrgId(httpServletRequest.getHeader("tmx-org-id")); filterChain.doFilter(httpServletRequest, servletResponse); } @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void destroy() { } }
UserContextHolder用户保存微服务处理的单个服务客户端请求的HTTP头部信息,通过保存在ThreadLocal中,确保用户请求的线程获取数据;
package com.thoughtmechanix.licenses.utils; import org.springframework.util.Assert; public class UserContextHolder { private static final ThreadLocal<UserContext> userContext = new ThreadLocal<UserContext>(); public static final UserContext getContext(){ UserContext context = userContext.get(); if (context == null) { context = createEmptyContext(); userContext.set(context); } return userContext.get(); } public static final void setContext(UserContext context) { Assert.notNull(context, "Only non-null UserContext instances are permitted"); userContext.set(context); } public static final UserContext createEmptyContext(){ return new UserContext(); } }
UserContext简单java对象;
package com.thoughtmechanix.licenses.utils; import lombok.Data; import org.springframework.stereotype.Component; @Component @Data public class UserContext { private String correlationId= new String(); private String authToken= new String(); private String userId = new String(); private String orgId = new String(); }
-
自定义TestTemplate和ClientHttpRequestIntercept,确保关联ID被传播;
-
定义拦截器,添加HTTP头部;
package com.thoughtmechanix.licenses.utils; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpRequest; import org.springframework.http.client.ClientHttpRequestExecution; import org.springframework.http.client.ClientHttpRequestInterceptor; import org.springframework.http.client.ClientHttpResponse; import java.io.IOException; public class UserContextInterceptor implements ClientHttpRequestInterceptor { /** * 该方法在RestTemplate发生实际的HTTP服务调用之前被调用 */ @Override public ClientHttpResponse intercept( HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { HttpHeaders headers = request.getHeaders(); headers.add("tmx-correlation-id", UserContextHolder.getContext().getCorrelationId()); headers.add("tmx-auth-token", UserContextHolder.getContext().getAuthToken()); return execution.execute(request, body); } }
-
定义RestTemplate bean,将上述拦截器添加进去;
package com.thoughtmechanix.licenses; import com.thoughtmechanix.licenses.utils.UserContextInterceptor; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.cloud.netflix.eureka.EnableEurekaClient; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; import java.util.Collections; import java.util.List; @SpringBootApplication @EnableEurekaClient @EnableCircuitBreaker public class Application { @LoadBalanced //该注解表明这个restTemplate要使用ribbon @Bean public RestTemplate getRestTemplate(){ RestTemplate template = new RestTemplate(); List interceptors = template.getInterceptors(); if (interceptors==null){ template.setInterceptors(Collections.singletonList(new UserContextInterceptor())); } else{ interceptors.add(new UserContextInterceptor()); template.setInterceptors(interceptors); } return template; } public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
-
-
-
定义后置过滤器,获取目标请求响应中的关联ID,并将其注入HTTP头部返回给服务调用者(zuul会结束目标http响应,并创建新的http响应返回给调用者,因此需要手动将目标响应中的头信息返回给服务调用者);
package com.thoughtmechanix.zuulsvr.filters; import com.netflix.zuul.ZuulFilter; import com.netflix.zuul.context.RequestContext; import org.springframework.stereotype.Component; @Component public class ResponseFilter extends ZuulFilter { @Override public String filterType() { return "post"; } @Override public int filterOrder() { return 1; } @Override public boolean shouldFilter() { return true; } @Override public Object run() { RequestContext ctx = RequestContext.getCurrentContext(); //获取原始HTTP请求中传入的关联ID,并将它注入到响应中 ctx.getResponse().addHeader("tmx-correlation-id", getCorrelationId()); return null; } public String getCorrelationId() { RequestContext ctx = RequestContext.getCurrentContext(); if (ctx.getRequest().getHeader("tmx-correlation-id") != null) { return ctx.getRequest().getHeader("tmx-correlation-id"); } else { return ctx.getZuulRequestHeaders().get("tmx-correlation-id"); } } }
-
构建动态路由过滤器,基本用不到,有点复杂,不看也罢,此处只做记录;
package com.thoughtmechanix.zuulsvr.filters; import com.netflix.zuul.ZuulFilter; import com.netflix.zuul.context.RequestContext; import com.thoughtmechanix.zuulsvr.model.AbTestingRoute; import org.apache.http.Header; import org.apache.http.HttpHost; import org.apache.http.HttpRequest; import org.apache.http.HttpResponse; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpPatch; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPut; import org.apache.http.entity.ContentType; import org.apache.http.entity.InputStreamEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.message.BasicHeader; import org.apache.http.message.BasicHttpRequest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.netflix.zuul.filters.ProxyRequestHelper; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestTemplate; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Random; @Component public class SpecialRoutesFilter extends ZuulFilter { @Autowired RestTemplate restTemplate; //spring cloud 提供的类,带有用于代理服务请求的辅助方法 private ProxyRequestHelper helper = new ProxyRequestHelper(); @Override public Object run() { RequestContext ctx = RequestContext.getCurrentContext(); boolean flag = new Random().nextBoolean(); //根据逻辑条件(此处可根据具体情况进行替代)判断是否进行动态路由 if (flag) { String route = "路由到服务的完整路径"; forwardToSpecialRoute(route); } return null; } private void forwardToSpecialRoute(String route) { RequestContext context = RequestContext.getCurrentContext(); HttpServletRequest request = context.getRequest(); //创建将发送到服务的所有HTTP请求头部的副本 MultiValueMap<String, String> headers = this.helper.buildZuulRequestHeaders(request); //创建所有HTTP请求参数的副本 MultiValueMap<String, String> params = this.helper.buildZuulRequestQueryParams(request); String verb = request.getMethod().toUpperCase(); //创建将被转发到替代服务的HTTP主体的副本 InputStream requestEntity = null; try { requestEntity = request.getInputStream(); } catch (IOException ex) { // no requestBody is ok. } if (request.getContentLength() < 0) { context.setChunkedRequestBody(); } this.helper.addIgnoredHeaders(); CloseableHttpClient httpClient = null; HttpResponse response = null; try { httpClient = HttpClients.createDefault(); //使用forward辅助方法调用替代服务 response = forward(httpClient, verb, route, request, headers, params, requestEntity); //通过setResponse辅助方法将服务调用的结果保存会回zuul服务器 setResponse(response); } catch (Exception ex) { ex.printStackTrace(); } finally { try { assert httpClient != null; httpClient.close(); } catch (IOException ignored) { } } } private HttpResponse forward(HttpClient httpclient, String verb, String uri, HttpServletRequest request, MultiValueMap<String, String> headers, MultiValueMap<String, String> params, InputStream requestEntity) throws Exception { URL host = new URL(uri); HttpHost httpHost = new HttpHost(host.getHost(), host.getPort(), host.getProtocol()); HttpRequest httpRequest; int contentLength = request.getContentLength(); InputStreamEntity entity = new InputStreamEntity(requestEntity, contentLength, request.getContentType() != null ? ContentType.create(request.getContentType()) : null); switch (verb.toUpperCase()) { case "POST": HttpPost httpPost = new HttpPost(uri); httpRequest = httpPost; httpPost.setEntity(entity); break; case "PUT": HttpPut httpPut = new HttpPut(uri); httpRequest = httpPut; httpPut.setEntity(entity); break; case "PATCH": HttpPatch httpPatch = new HttpPatch(uri); httpRequest = httpPatch; httpPatch.setEntity(entity); break; default: httpRequest = new BasicHttpRequest(verb, uri); } try { httpRequest.setHeaders(convertHeaders(headers)); HttpResponse zuulResponse = forwardRequest(httpclient, httpHost, httpRequest); return zuulResponse; } finally { } } private void setResponse(HttpResponse response) throws IOException { this.helper.setResponse(response.getStatusLine().getStatusCode(), response.getEntity() == null ? null : response.getEntity().getContent(), revertHeaders(response.getAllHeaders())); } @Override public String filterType() { return "route"; } @Override public int filterOrder() { return 1; } @Override public boolean shouldFilter() { return true; } private Header[] convertHeaders(MultiValueMap<String, String> headers) { List<Header> list = new ArrayList<>(); for (String name : headers.keySet()) { for (String value : headers.get(name)) { list.add(new BasicHeader(name, value)); } } return list.toArray(new BasicHeader[0]); } private HttpResponse forwardRequest(HttpClient httpclient, HttpHost httpHost, HttpRequest httpRequest) throws IOException { return httpclient.execute(httpHost, httpRequest); } private MultiValueMap<String, String> revertHeaders(Header[] headers) { MultiValueMap<String, String> map = new LinkedMultiValueMap<String, String>(); for (Header header : headers) { String name = header.getName(); if (!map.containsKey(name)) { map.put(name, new ArrayList<String>()); } map.get(name).add(header.getValue()); } return map; } }
-