1 服務網關
服務網關充當服務客戶端和被調用的服務之間的中介。服務客戶端僅與服務網關管理的單個URL進行對話。服務網關從服務客戶端調用中分離出路徑,並確定服務客戶端正在嘗試調用哪個服務。
由於服務網關位於客戶端到各個服務的所有調用之間,因此它還充當服務調用的中央策略執行點(PEP)。使用集中式PEP意味着橫切服務關注點可以在一個地方實現,而無須各個開發團隊來實現這些關注點。舉例來說,可以在服務網關中實現的橫切關注點包括以下幾個。
- 靜態路由
- 動態路由
- 驗證和授權
- 度量數據收集和日誌記錄
2 Spring Cloud和Netflix Zuul 簡介
Spring Cloud 集成了Netflix開源項目Zuul 。Zuul是一個服務網關,它非常容易通過Spring Cloud註解進行創建和使用。
要開始使用Zuul,需要完成下面3件事。
- 建立一個Zuul Spring Boot項目,並配置適當的Maven依賴項。
- 使用Spring Cloud 註解修改這個SpringBoot項目,將其聲明爲Zuul 服務。
- 配置Zuul以便Eureka進行通信(可選)。
3 建立一個Zuul Spring Boot 項目
Zuul的所有路由映射都是通過在application.yml文件中定義路由來完成的。但是,Zuul可以根據其服務ID自動路由請求,而不需要配置。如果沒有指定任何路由,Zuul將自動使用正在調用的服務的Eureka服務ID,並將其映射到下游服務實例。
3.1 pom依賴
在Maven 中建立Zuul只簾要很少的步驟,只需要在pom.xml 文件中定義一個依賴項:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
3.2 爲Zuul服務使用Spring Cloud註解
這裏只需要一個註解:@EnableZuulProxy,它可以使服務成爲一個Zuul服務器。
@SpringBootApplication
@EnableZuulProxy//此工程是一個zuul網關
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
3.3 配置Zuul與Eureka進行通信
Zuul代理服務器默認設計爲在Spring產品上工作。因此,Zuul 將自動使用Eureka來通過服務ID 查找服務,然後使用Netflix Ribbon對來自Zuul的請求進行客戶端負載均衡。配置application.yml:
server:
port: ${PORT:50201}
spring:
application:
name: zuul_gateway
eureka:
client:
registerWithEureka: true #服務註冊開關
fetchRegistry: true #服務發現開關
serviceUrl: #Eureka客戶端與Eureka服務端進行交互的地址,多箇中間用逗號分隔
defaultZone: ${EUREKA_SERVER:http://localhost:50101/eureka/}
4 在Zuul中配置路由
在微服務架構的情況下, Zuul(反向代理)從客戶端接收微服務調用並將其轉發給下游服務。服務客戶端認爲它只與Zuul通信。Zuul要與下游服務進行溝通, Zuul 必須知道如何將進來的調用映射到下游路由。Zuul 有幾種機制來做到這一點,包括:
- 通過服務發現自動映射路由;
- 使用服務發現手動映射路由;
- 使用靜態URL手動映射路由。
4.1 通過服務發現自動映射路由
Zuul的所有路由映射都是通過在application.yml 文件中定義路由來完成的。但是,Zuul可以根據其服務ID自動路由請求,而不需要配置。如果沒有指定任何路由,Zuul將自動使用正在調用的服務的Eureka服務ID,並將其映射到下游服務實例。
4.2 使用服務發現手動映射路由
開發人員可以通過縮短組織名稱來簡化路由。開發人員可以通過在application.yml中手動定義路由映射來做到這一點。
zuul:
routes:
userservice: /user/**
通過添加上述配置,現在我們就可以通過訪問/user/name路由來訪問組織服務了。
服務網關的一種常見模式是通過使用/api之類的標記來爲所有的服務調用添加前綴,從而區分API 路由與內容路由。Zuul通過在Zuul 配置中使用prefix屬性來支持這項功能。
zuul:
routes:
userservice: /user/**
prefix: /api
4.3 使用靜態URL手動映射路由
Zuul可以用來路由那些不受Eureka管理的服務。在這種情況下,可以建立Zuul 直接路由到一個靜態定義的URL。
user: Zuul用於在內部識別服務的關鍵字
path: 服務的靜態路由
url: 已建立服務的靜態實例,它將被直接調用,而不是由Zuul通過Eureka調
zuul:
prefix: /api
routes:
user:
path: /user/**
url: http://localhost:31001
這裏存在一個問題,那就是通過繞過Eureka,只有一條路徑可以用來指向請求。幸運的是,開發人員可以手動配置Zuul來禁用Ribbon與Eureka集成,然後列出Ribbon將進行負載均衡的各個服務實例。
zuul:
prefix: /api
routes:
user:
path: /user/**
serviceid: userservice
ribbon:
eureka:
enabled: false
user:
ribbon:
listOfServers: http://localhost:31001
5 過濾器
雖然通過Zuul網關代理所有請求確實可以簡化服務調用,但是在想要編寫應用於所有流經網關的服務調用的自定義邏輯時,Zuul的真正威力才發揮出來。在大多數情況下,這種自定義邏輯用於強制執行一組一致的應用程序策略,如安全性、日誌記錄和對所有服務的跟蹤。
Zuul支持以下3 種類型的過濾器:
- 前置過濾器:前置過濾器在Zuul將實際請求發送到目的地之前被調用。
- 後置過濾器:後置過濾器在目標服務被調用並將響應發送回客戶端後被調用。
- 路由過濾器:路由過濾器用於在調用目標服務之前攔截調用。
如果遵循上圖中所列出的流程,將會看到所有的事情都是從服務客戶端調用服務網關公開的服務開始的。從這裏開始,發生了以下活動。
- 在請求進入Zuul網關時,Zuul調用所有在Zuul網關中定義的前置過濾器。前置過濾器可以在HTTP請求到達實際服務之前對HTTP 請求進行檢查和修改。前置過濾器不能將用戶重定向到不同的端點或服務。
- 在針對Zuul 的傳人請求執行前置過濾器之後,Zuul 將執行已定義的路由過濾器。路由過濾器可以更改服務所指向的目的地。
- 路由過濾器可以將服務調用重定向到Zuul 服務器被配置的發送路由以外的位置。但Zuul路由過濾器不會執行HTTP重定向而是會終止傳入的HTTP 請求然後代表原始調用者調用路由。這意味着路由過濾器必須完全負責動態路由的調用, 並且不能執行HTTP重定向。
- 如果路由過濾器沒有動態、地將調用者重定向到新路由, Zuul服務器將發送到最初的目標服務的路由。
- 目標服務被調用後,Zuul後置過濾器將被調用。後置過濾器可以檢查和修改來自被調用服務的響應。
5.1 前置過濾器
我們首先將構建一個名爲TrackingFilter的Zuul前置過濾器,該過濾器將檢查所有到網關的傳入請求,並確定請求中是否存在名爲tmx-correlation-id 的HTTP首部。tmx-correlation-id首部將包含一個唯一的全局通用ID (Globally Universal ID , GUID),它可用於跨多個微服務來跟蹤用戶請求(關聯ID記錄請求經過了哪些服務)。
如果在HTTP首部中不存在tmx-correlation-id ,那麼Zuul TrackingFilter將生成並設置該關聯ID 。如果已經存在關聯ID,那麼Zuul將不會對該關聯ID進行任何操作(關聯ID是通過HTTP進行傳播的)。
什麼是關聯ID,後面會詳細解釋。
5.1.1 TrackingFilter (前置過濾器)
要在Zuul 中實現過濾器,必須擴展ZuulFilter 類,然後覆蓋4 個方法,即filterType() 、filterOrder() 、shouldFilter()和run()方法。
@Component
public class TrackingFilter extends ZuulFilter {
private static final int FILTER_ORDER = 1;
private static final boolean SHOULD_FILTER=true;
private static final Logger logger = LoggerFactory.getLogger(TrackingFilter.class);
@Autowired
FilterUtils filterUtils;
@Override
//該過濾器是前置、路由還是後置過濾器
public String filterType() {
return filterUtils.PRE_FILTER_TYPE;
}
@Override
//指示不同的過濾器的執行順序
public int filterOrder() {
return FILTER_ORDER;
}
//返回一個布爾值,指示該過濾器是否要執行
public boolean shouldFilter() {
return SHOULD_FILTER;
}
//判斷關聯id是否存在
private boolean isCorrelationIdPresent(){
if (filterUtils.getCorrelationId() !=null){
return true;
}
return false;
}
//藉助工具類生成GUID,GUID:關聯id首部包含的唯一的全局通用ID
private String generateCorrelationId(){
return java.util.UUID.randomUUID().toString();
}
public Object run() {
//把關聯id寫入日誌
if (isCorrelationIdPresent()) {
logger.debug("tmx-correlation-id found in tracking filter: {}. ", filterUtils.getCorrelationId());
}
else{
filterUtils.setCorrelationId(generateCorrelationId());
logger.debug("tmx-correlation-id generated in tracking filter: {}.", filterUtils.getCorrelationId());
}
RequestContext ctx = RequestContext.getCurrentContext();
logger.debug("Processing incoming request for {}.", ctx.getRequest().getRequestURI());
return null;
}
}
5.1.2 FilterUtils類
我們已經實現了一個名爲FilterUtils 的類。這個類用於封裝所有過濾器使用的常用功能。
@Component
public class FilterUtils {
public static final String CORRELATION_ID = "tmx-correlation-id";
public static final String AUTH_TOKEN = "tmx-auth-token";
public static final String USER_ID = "tmx-user-id";
public static final String ORG_ID = "tmx-org-id";
public static final String PRE_FILTER_TYPE = "pre";
public static final String POST_FILTER_TYPE = "post";
public static final String ROUTE_FILTER_TYPE = "route";
public String getCorrelationId(){
RequestContext ctx = RequestContext.getCurrentContext();
if (ctx.getRequest().getHeader(CORRELATION_ID) !=null) {
return ctx.getRequest().getHeader(CORRELATION_ID);
}
else{
return ctx.getZuulRequestHeaders().get(CORRELATION_ID);
}
}
public void setCorrelationId(String correlationId){
RequestContext ctx = RequestContext.getCurrentContext();
ctx.addZuulRequestHeader(CORRELATION_ID, correlationId);
}
public final String getOrgId(){
RequestContext ctx = RequestContext.getCurrentContext();
if (ctx.getRequest().getHeader(ORG_ID) !=null) {
return ctx.getRequest().getHeader(ORG_ID);
}
else{
return ctx.getZuulRequestHeaders().get(ORG_ID);
}
}
public void setOrgId(String orgId){
RequestContext ctx = RequestContext.getCurrentContext();
ctx.addZuulRequestHeader(ORG_ID, orgId);
}
public final String getUserId(){
RequestContext ctx = RequestContext.getCurrentContext();
if (ctx.getRequest().getHeader(USER_ID) !=null) {
return ctx.getRequest().getHeader(USER_ID);
}
else{
return ctx.getZuulRequestHeaders().get(USER_ID);
}
}
public void setUserId(String userId){
RequestContext ctx = RequestContext.getCurrentContext();
ctx.addZuulRequestHeader(USER_ID, userId);
}
public final String getAuthToken(){
RequestContext ctx = RequestContext.getCurrentContext();
return ctx.getRequest().getHeader(AUTH_TOKEN);
}
public String getServiceId(){
RequestContext ctx = RequestContext.getCurrentContext();
//We might not have a service id if we are using a static, non-eureka route.
if (ctx.get("serviceId")==null) return "";
return ctx.get("serviceId").toString();
}
}
5.2 在服務調用中使用關聯ID
既然已經確保每個流經Zuul的微服務調用都添加了關聯ID,那麼如何確保:
- 正在被調用的微服務可以很容易訪問關聯ID;
- 下游服務調用微服務時可能也會將關聯ID傳播到下游調用中。
要實現這一點,需要爲每個微服務構建一組3個類。這些類將協同工作,從傳人的HTTP請求中讀取關聯ID(以及稍後添加的其他信息),並將它映射到可以由應用程序中的業務邏輯輕鬆訪問和使用的類,然後確保關聯ID被傳播到任何下游服務調用。
如下圖所示,生成和獲取關聯ID的步驟:
- 當通過Zuul網關對許可證服務進行調用時,TrackingFilter會爲所有進入Zuul的調用在傳入的HTTP首部中注入一個關聯ID
- UserContextFilter 類是一個自定義的HTTP servlet過濾器。它將關聯ID映射到UserContext類。UserContext 存儲在本地線程存儲中,以便稍後在調用中使用。
- 客戶端需要執行對服務的調用。
- RestTernplate用於調用服務。Rest Template 將使用自定義的Spring 攔截器類(UserContextinterceptor)將關聯ID作爲HTTP首部注入出站調用。
5.2.1 UserContextFilter:攔截傳入的HTTP請求
要構建的第一個類是UserContextFilter類。這個類是一個HTTP servlet過濾器,它將攔截進入服務的所有傳人Hπp請求,並將關聯ID(和其他一些值) 從HTTP請求映射到UserContext類。
@Component
public class UserContextFilter implements Filter {
private static final Logger logger = LoggerFactory.getLogger(UserContextFilter.class);
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
//過濾器從首部中檢索關聯ID,並將值設置在UserContext類
UserContextHolder.getContext().setCorrelationId( httpServletRequest.getHeader(UserContext.CORRELATION_ID) );
UserContextHolder.getContext().setUserId(httpServletRequest.getHeader(UserContext.USER_ID));
UserContextHolder.getContext().setAuthToken(httpServletRequest.getHeader(UserContext.AUTH_TOKEN));
UserContextHolder.getContext().setOrgId(httpServletRequest.getHeader(UserContext.ORG_ID));
logger.debug("Special Routes Service Incoming Correlation id: {}", UserContextHolder.getContext().getCorrelationId());
filterChain.doFilter(httpServletRequest, servletResponse);
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {}
@Override
public void destroy() {}
}
5.2.2 UserContexts:使服務易於訪問HTTP首部
UserContext類用於保存由微服務處理的單個服務客戶端請求的HTTP首部值。
@Component
public class UserContext {
public static final String CORRELATION_ID = "tmx-correlation-id";
public static final String AUTH_TOKEN = "tmx-auth-token";
public static final String USER_ID = "tmx-user-id";
public static final String ORG_ID = "tmx-org-id";
private String correlationId= new String();
private String authToken= new String();
private String userId = new String();
private String orgId = new String();
public String getCorrelationId() { return correlationId;}
public void setCorrelationId(String correlationId) {
this.correlationId = correlationId;
}
public String getAuthToken() {
return authToken;
}
public void setAuthToken(String authToken) {
this.authToken = authToken;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getOrgId() {
return orgId;
}
public void setOrgId(String orgId) {
this.orgId = orgId;
}
5.2.3 自定義RestTemplate和UserContextlnteceptor: 確保關聯ID被傳播
爲了使用UserContextinterceptor,我們需要定義一個Rest Template bean,然後將UserContextlnterceptcir添加進去。
@LoadBalanced
@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;
}
5.3 後置過濾器
Zuul代表服務客戶端執行實際的HTTP 調用。Zuul有機會從目標服務調用中檢查響應, 然後修改響應或以額外的信息裝飾它。當與以前置過濾器捕獲數據相結合時,Zuul後置過濾器是收集指標井完成與用戶事務相關聯的日誌記錄的理想場所。
我們將使用Zuul後置過濾器將關聯ID注入HTTP 響應首部中,該HTTP響應首部傳回給服務調用者。這樣,就可以將關聯ID傳回給調用者,而無需接觸消息體。
@Component
public class ResponseFilter extends ZuulFilter{
private static final int FILTER_ORDER=1;
private static final boolean SHOULD_FILTER=true;
private static final Logger logger = LoggerFactory.getLogger(ResponseFilter.class);
@Autowired
FilterUtils filterUtils;
@Override
public String filterType() {
return FilterUtils.POST_FILTER_TYPE;
}
@Override
public int filterOrder() {
return FILTER_ORDER;
}
@Override
public boolean shouldFilter() {
return SHOULD_FILTER;
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
logger.debug("Adding the correlation id to the outbound headers. {}", filterUtils.getCorrelationId());
System.out.println(filterUtils.getCorrelationId());
//獲取原始HTTP請求中傳入的關聯ID,並將它注入到響應中
ctx.getResponse().addHeader(FilterUtils.CORRELATION_ID, filterUtils.getCorrelationId());
//記錄傳出請求URI,這樣就可以顯示Zuul的用戶請求的傳入和傳出條目
logger.debug("Completing outgoing request for {}.", ctx.getRequest().getRequestURI());
return null;
}
}
5.4 測試
從下圖可以看出,調用的HTTP響應首部上看到一個tmx-correlation-id 。
6 動態路由過濾器
我們將通過構建一個路由過濾器來學習Zuul路由過濾器,從而允許對新版本的服務進行AB測試。A/B 測試是推出新功能的地方,在這裏有一定比例的用戶能夠使用新功能,而其餘的用戶仍然使用舊服務。
爲此,需要構建一個名爲SpecialRoutesFilter的路由過濾器。該過濾器將接收由Zuul調用的服務的Eureka 服務ID,並調用另一個名爲SpecialRoutes 的微服務。SpecialRoutes服務將檢查內部數據庫以查看服務名稱是否存在。如果目標服務名稱存在,它將返回服務的權重以及替代位置的目的地。SpecialRoutesFilter將接收返回的權重,並根據權重隨機生成一個值,用於確定用戶的調用是否將被路由到替代組織服務或Zuul路由映射中定義的組織服務。
如下圖所示,在服務客戶端調用Zuul 背後的服務時,SpecialRoutesFilter 會執行以下操作。
- SpecialRoutesFilter 檢索被調用服務的服務ID 。
- SpecialRoutesFilter 調用SpecialRoutes 服務。SpecialRoutes 服務將查詢是否有針對目標端點定義的替代端點。如果找到一條記錄,那麼這條記錄將包含一個權重,它將告訴Zuul應該發送到舊服務和新服務的服務調用的百分比。
- 然後SpecialRoutesFilter生成一個隨機數,並將它與SpecialRoutes服務返回的權重進行比較。如果隨機生成的數字大於替代端點權重的值,那麼SpecialRoutesFilter會將請求發送到服務的新版本。
- 如果SpecialRoutesFilter 將請求發送到服務的新版本,Zuul會維持最初的預定義管道,並通過已定義的後置過濾器將響應從替代服務端點發送回來。
6.1 路由過濾器
@Component
public class SpecialRoutesFilter extends ZuulFilter {
private static final int FILTER_ORDER = 1;
private static final boolean SHOULD_FILTER =true;
@Autowired
FilterUtils filterUtils;
@Autowired
RestTemplate restTemplate;
@Override
public String filterType() {
return filterUtils.ROUTE_FILTER_TYPE;
}
@Override
public int filterOrder() {
return FILTER_ORDER;
}
@Override
public boolean shouldFilter() {
return SHOULD_FILTER;
}
private ProxyRequestHelper helper = new ProxyRequestHelper();
private AbTestingRoute getAbRoutingInfo(String serviceName){
ResponseEntity<AbTestingRoute> restExchange = null;
try {
restExchange = restTemplate.exchange(
"http://specialroutesservice/v1/route/abtesting/{serviceName}",
HttpMethod.GET,
null, AbTestingRoute.class, serviceName);
}
catch(HttpClientErrorException ex){
if (ex.getStatusCode()== HttpStatus.NOT_FOUND) return null;
throw ex;
}
return restExchange.getBody();
}
private String buildRouteString(String oldEndpoint, String newEndpoint, String serviceName){
int index = oldEndpoint.indexOf(serviceName);
String strippedRoute = oldEndpoint.substring(index + serviceName.length());
System.out.println("Target route: " + String.format("%s/%s", newEndpoint, strippedRoute));
return String.format("%s/%s", newEndpoint, strippedRoute);
}
private String getVerb(HttpServletRequest request) {
String sMethod = request.getMethod();
return sMethod.toUpperCase();
}
private HttpHost getHttpHost(URL host) {
HttpHost httpHost = new HttpHost(host.getHost(), host.getPort(),
host.getProtocol());
return httpHost;
}
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;
}
private InputStream getRequestBody(HttpServletRequest request) {
InputStream requestEntity = null;
try {
requestEntity = request.getInputStream();
}
catch (IOException ex) {
// no requestBody is ok.
}
return requestEntity;
}
private void setResponse(HttpResponse response) throws IOException {
this.helper.setResponse(response.getStatusLine().getStatusCode(),
response.getEntity() == null ? null : response.getEntity().getContent(),
revertHeaders(response.getAllHeaders()));
}
private HttpResponse forward(HttpClient httpclient, String verb, String uri,
HttpServletRequest request, MultiValueMap<String, String> headers,
MultiValueMap<String, String> params, InputStream requestEntity)
throws Exception {
Map<String, Object> info = this.helper.debug(verb, uri, headers, params,
requestEntity);
URL host = new URL( uri );
HttpHost httpHost = getHttpHost(host);
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 {
}
}
public boolean useSpecialRoute(AbTestingRoute testRoute){
Random random = new Random();
if (testRoute.getActive().equals("N")) return false;
int value = random.nextInt((10 - 1) + 1) + 1;
if (testRoute.getWeight()<value) return true;
return false;
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
AbTestingRoute abTestRoute = getAbRoutingInfo( filterUtils.getServiceId() );
if (abTestRoute!=null && useSpecialRoute(abTestRoute)) {
String route = buildRouteString(ctx.getRequest().getRequestURI(),
abTestRoute.getEndpoint(),
ctx.get("serviceId").toString());
forwardToSpecialRoute(route);
}
return null;
}
private void forwardToSpecialRoute(String route) {
RequestContext context = RequestContext.getCurrentContext();
HttpServletRequest request = context.getRequest();
MultiValueMap<String, String> headers = this.helper
.buildZuulRequestHeaders(request);
MultiValueMap<String, String> params = this.helper
.buildZuulRequestQueryParams(request);
String verb = getVerb(request);
InputStream requestEntity = getRequestBody(request);
if (request.getContentLength() < 0) {
context.setChunkedRequestBody();
}
this.helper.addIgnoredHeaders();
CloseableHttpClient httpClient = null;
HttpResponse response = null;
try {
httpClient = HttpClients.createDefault();
response = forward(httpClient, verb, route, request, headers,
params, requestEntity);
setResponse(response);
}
catch (Exception ex ) {
ex.printStackTrace();
}
finally{
try {
httpClient.close();
}
catch(IOException ex){}
}
}
}
6.1.1 run()方法
當路由請求觸發Spe cialRoutesFilter 中的run()方法時,它將對SpecialRoutes 服務執行REST調用。該服務將執行查找,並確定是否存在針對被調用的目標服務的Eureka服務ID的路由記錄。
一旦確定目標服務的路由記錄存在,就需要確定是否應該將目標服務請求路由到替代服務位置,或者路由到由Zuul路由映射靜態管理的默認服務位置。爲了做出這個決定,需要調用useSpecialRoute()方法。
6.1.2 SpecialRouteservice()方法
這個方法做了兩件事。首先,該方法檢查從SpecialRoutes服務返回的AbTestingRoute記錄中的active字段。如果該記錄設置爲” N ”, 則useSpecialRoute()方法不應該執行任何操作,因爲現在不希望進行任何路由。其次,該方法生成1到10之間的隨機數。然後,該方法將檢查返回路由的權重是否小於隨機生成的數。如果條件爲true ,useSpecialRoute()方法將返回true ,表示確實希望使用該路由。
6.1.3 forwardToSpecialRoute()方法
forwardToSpecialRoute()方法負責轉發工作。
7 關聯ID
7.1 線程上下文和Hystrix
當一個@HystrixCommand 被執行時,它可以使用兩種不同的隔離策略THREAD(線程)和SEMAPHORE (信號量)來運行。在默認情況下, Hystrix 以THREAD 隔離策略運行。用於保護調用的每個Hystrix命令都在一個單獨的線程池中運行,該線程池不與父線程共享它的上下文。
7.2 Threadlocal 與Hystrix
在默認情況下,Hystrix 不會將父線程的上下文傳播到由Hystrix命令管理的線程中。例如,在默認情況下,對被父線程調用並由@HystrixComman保護的方法而言,在父線程中設置爲ThreadLocal值的值都是不可用的(再強調一次,這是假設當前使用的是THREAD隔離級別) 。
通常在基於REST的環境中,開發人員希望將上下文信息傳遞給服務調用, 這將有助於在運維上管理該服務(收集跨很多服務的請求相關的日誌)。例如, 可以在REST 調用的HTTP首部中傳遞關聯ID ( correlation ID)或驗證令牌, 然後將其傳播到任何下游服務調用。關聯ID是唯一標識符, 該標識符可用於在單個事務中跨多個服務調用進行跟蹤。要使服務調用中的任何地方都可以使用此值,開發人員可以使用Spring過濾器類來攔截對REST服務的每個調用,並從傳人的HTTP請求中檢索此信息, 然後將此上下文信息存儲在自定義的UserContext對象中。然後在任何需要在REST服務調用中訪問該值的時候, 可以從ThreadLocal存儲變量中檢索UserContext並讀取該值。