spring cloud zuul由大名鼎鼎的netflix公司開發,已經超越spring cloud gateway微服務閘道器係統,成為了Spring Cloud全家桶裡排名第一的微服務閘道器係統了
閘道器作為所有應用系統的最前端,可以提供以下的價值
- 為後端微服務系統提供統一的入口
- 為後端微服務系統提供統一的授權機制
- 為後端微服務系統提供統一的認證機制
- 為後端微服務系統api提供統一簽名校驗機制
- 為流量入口新增日誌記錄
- qps統計
- 限流
完整程式碼已上傳GITHUB,參考:github.com/neatlife/my…
建立閘道器專案
可以在https://start.spring.io/建立新的spring boot專案作為閘道器的骨架,比如
注意需要把Zuul閘道器元件新增進來,也可以手動新增,pom依賴如下:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
複製程式碼
代理後端服務
編輯Spring Boot main方法所在的主類,新增EnableZuulProxy註解,核心程式碼如下
@SpringBootApplication
@EnableZuulProxy
public class MyzuulApplication {
public static void main(String[] args) {
SpringApplication.run(MyzuulApplication.class, args);
}
}
複製程式碼
然後在application.properties配置檔案中配置代理的後端服務,比如代理這個服務 jsonplaceholder.typicode.com/todos/
這個服務可以直接訪問效果如下:jsonplaceholder.typicode.com/todos/3 使用zuul代理訪問效果如下 可以看到資料是完全一樣的,閘道器只是起到了一個代理的作用可以結合apollo配置中心實現自動重新整理後端路由列表,參考: ZuulPropertiesRefresher.java
核心程式碼如下:
@ApolloConfigChangeListener(interestedKeyPrefixes = "zuul.")
public void onChange(ConfigChangeEvent changeEvent) {
refreshZuulProperties(changeEvent);
}
private void refreshZuulProperties(ConfigChangeEvent changeEvent) {
logger.info("Refreshing zuul properties!");
/**
* rebind configuration beans, e.g. ZuulProperties
* @see org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder#onApplicationEvent
*/
this.applicationContext.publishEvent(new EnvironmentChangeEvent(changeEvent.changedKeys()));
/**
* refresh routes
* @see org.springframework.cloud.netflix.zuul.ZuulServerAutoConfiguration.ZuulRefreshListener#onApplicationEvent
*/
this.applicationContext.publishEvent(new RoutesRefreshedEvent(routeLocator));
logger.info("Zuul properties refreshed!");
}
複製程式碼
過濾器系統
過濾器型別 | 過濾器執行時機 | 常見用法 |
---|---|---|
PRE | 在請求被路由之前呼叫 | 比如記錄請求引數,認證 |
ROUTING | 將請求路由到後端 | 比如請求dubbo搭建的後端服務 |
POST | 在請求被路由之後呼叫 | 比如記錄響應資料 |
ERROR | 發生錯誤時執行該過濾器 | 比如給前端返回統一的報錯json格式 |
web 系統
這個閘道器係統本身也是一個完整spring boot專案 可以編寫控制器api,呼叫hibernate運算元據庫等 比如在閘道器裡編寫登陸功能實現統一的授權功能
授權與認證
授權一般在閘道器裡的控制器裡實現授權的邏輯,授權一般可以指代登陸操作 認證一般在閘道器裡的前置過濾器裡實現,一般是檢查是否登陸來決定是否允許訪問閘道器後面的服務
閘道器登陸介面編寫思路
流程程式碼如下
@PostMapping("/login")
public String login(@RequestParam(name = "username") String username,
@RequestParam(name = "password") String password
) {
String token;
// 到資料中檢查使用者名稱和密碼是否合法
if ("admin".equals(username) && "admin".equals(password)) {
// 生成token,儲存到redis中
token = "21232f297a57a5a743894a0e4a801fc3";
} else {
throw new RuntimeException("使用者名稱或密碼錯誤");
}
// 返回token給前端,用來認證使用
return token;
}
複製程式碼
認證流程
使用pre型別的過濾器 流程程式碼如下
/**
* 認證
*/
@Slf4j
public class CertificationFilter extends ZuulFilter {
@Override
public Object run() {
HttpServletRequest request = RequestContext.getCurrentContext().getRequest();
// 到redis中檢查token是否存在
if ("21232f297a57a5a743894a0e4a801fc3".equals(request.getParameter("token"))) {
return null;
}
throw new ZuulRuntimeException(new ZuulException("未授權使用者禁止訪問", 403, "token校驗失敗"));
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public int filterOrder() {
return 0;
}
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
}
複製程式碼
就是普通的登陸介面,不過是把這個邏輯放到了閘道器裡面
將請求的引數記錄到日誌
這樣做的目的是在異常時可以通過日誌找到請求引數 編寫一個過濾器,獲取請求引數並記錄到日誌中,核心程式碼如下
@Override
public Object run() {
HttpServletRequest request = RequestContext.getCurrentContext().getRequest();
Map<String, Object> parameters = getParametersFromRequest(request);
log.info("閘道器有新的訪問, url: {}, method: {}, parameters: {}", request.getRequestURL(), request.getMethod(), parameters);
return null;
}
private Map<String, Object> getParametersFromRequest(HttpServletRequest request) {
Enumeration<?> parameterNames = request.getParameterNames();
Map<String, Object> parameters = new HashMap<>(16);
while (parameterNames.hasMoreElements()) {
String pName = (String) parameterNames.nextElement();
Object pValue = request.getParameter(pName);
parameters.put(pName, pValue);
}
return parameters;
}
複製程式碼
完整程式碼參考:
統一錯誤返回格式
使用error過濾器可以實現 核心程式碼如下
@Slf4j
public class CustomErrorFilter extends ZuulFilter {
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
// Remove error code to prevent further error handling in follow up filters
ctx.remove("error.status_code");
// block the SendErrorFilter from running
ctx.set("sendErrorFilter.ran");
ctx.setSendZuulResponse(false);
ctx.getResponse().setContentType("application/json");
ctx.getResponse().setCharacterEncoding("utf-8");
ctx.getResponse().setHeader("Access-Control-Allow-Origin", "*");
ctx.getResponse().setHeader("Access-Control-Allow-Methods", "*");
ctx.getResponse().setHeader("Access-Control-Allow-Age", "86400");
ctx.getResponse().setHeader("Access-Control-Allow-Headers", "*");
ctx.setResponseStatusCode(200);
StringWriter sw = new StringWriter();
ctx.getThrowable().printStackTrace(new PrintWriter(sw, true));
try {
ZuulException zuulException = (ZuulException) ctx.getThrowable().getCause().getCause();
ctx.getResponse().getWriter().write(
"{\"message\": \"" +
zuulException.getMessage() +
"\", \"code\" :" +
zuulException.nStatusCode
+ "}"
);
} catch (Exception e) {
log.error("寫入異常到客戶端異常, estring: {}", e.toString());
}
return null;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public int filterOrder() {
return -1;
}
@Override
public String filterType() {
return FilterConstants.ERROR_TYPE;
}
}
複製程式碼
注意需要加上以下設定
// 阻止別的錯誤處理過濾器再次對錯誤進行處理
ctx.remove("error.status_code");
// 阻止別的傳送錯誤的過濾器對錯誤響應結果再次進行處理
ctx.set("sendErrorFilter.ran");
複製程式碼
在瀏覽器中檢視效果
zuul設定與調優
常用的調優引數如下:
連線池最大連線,預設是200 zuul.host.maxTotalConnections=1000
每個route可用的最大連線數,預設值是20 zuul.host.maxPerRouteConnections=1000
Hystrix最大的併發請求 預設值是100 zuul.semaphore.maxSemaphores=1000
hystrix熔斷設定與調優
閘道器屬於整個系統最前端的應用,同時又屬於基礎服務,和redis, mysql等基礎服務一樣,一般是不允許當機的,可用性應該得到保證,保證可用性常用的技術就是使用降級熔斷技術hystrix了
Hystrix 超時時間配置 配置預設的hystrix超時時間
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=10000
複製程式碼
配置特定方法的超時時間
hystrix.command.<hystrixcommandkey>.execution.isolation.thread.timeoutInMilliseconds=10000
複製程式碼
的format為FeignClassName#methodSignature,下面是示例配置
hystrix.command.PressureService#getBalance(int).execution.isolation.thread.timeoutInMilliseconds=10000
複製程式碼
一些注意的點
可以在前置過濾器中校驗介面的簽名,參考:簡單API介面簽名驗證
閘道器裡面登陸的使用者需要和其它服務共享使用者的登陸資訊,可以把使用者的資訊存放到redis中進行共享
如果在開發過程中遇到問題,可加作者微信探討