SpringCloud(七)路由網關Zuul
在微服務架構中,路由是一個很重要的組成部分。比如,/
可以映射到你的Web服務,/api/users
映射到你的用戶服務,而/api/shop
可以映射到你的商城服務。SpringCloud中的Zuul是基於JVM的路由和服務端負載均衡器,可以有效地將微服務的接口納入統一管理暴露給外部。
引入並啓用Zuul
新建一個服務zuul-service
作爲路由網關服務。
pom.xml
中引入依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>
使用@EnableZuulProxy
啓用Zuul(使用@EnableZuulServer
也可以啓用Zuul,只是不會自動從Eureka中獲取並自動代理服務,也不會自動加載部分Zuul過濾器,但是可以選擇性地替換代理平臺的各個部分)。
@SpringBootApplication
@EnableEurekaClient
@EnableZuulProxy
public class ZuulApplicationStarter {
public static void main(String[] args) {
SpringApplication.run(ZuulApplicationStarter.class, args);
}
}
路由配置
在application.yaml
進行zuul路由配置。
info:
name: Zuul Service
server:
port: 8301
#不設置爲false,就不能調用/routes獲取路由表
management:
security:
enabled: false
zuul:
host:
#代理普通http請求的超時時間
socket-timeout-millis: 2000
connect-timeout-millis: 1000
max-total-connections: 2000
max-per-route-connections: 200
ignored-services: 'sms-service'
routes:
sms-service: /smsApi/**
users:
path: /userApi/**
service-id: user-service
users2:
path: /userApi2/**
url: http://localhost:8002
sms2:
service-id: sms-service
path: /sms/**
stripPrefix: false
forward:
path: /forward/**
url: forward:/myZuul
service-by-ribbon: /service-by-ribbon/**
#設置zuul.prefix所有請求都需要添加/api前綴
#prefix: /api
#strip-prefix: true
########hystrix相關配置
# 注意項:
# 1、zuul環境下,信號量模式下併發量的大小,zuul.semaphore.maxSemaphores這種配置方式優先級最高
# 2、zuul環境下,資源隔離策略默認信號量,zuul.ribbonIsolationStrategy這種配置方式優先級最高
# 3、zuul環境下,commandGroup 固定爲RibbonCommand
# 4、zuul環境下,commandKey 對應每個服務的serviceId
#
hystrix:
command:
# 這是默認的配置
default:
execution:
timeout:
enabled: true
isolation:
thread:
# 命令執行超時時間
timeoutInMilliseconds: 2000
ribbon:
# 配置ribbon默認的超時時間
ConnectTimeout: 1000
ReadTimeout: 2000
# 是否開啓重試
OkToRetryOnAllOperations: true
# 重試期間,實例切換次數
MaxAutoRetriesNextServer: 1
# 當前實例重試次數
MaxAutoRetries: 0
eureka:
enabled: false
# 定義一個針對service-by-ribbon服務的負載均衡器,服務實例信息來自配置文件,zuul默認可以集成
# 服務名
service-by-ribbon:
# 服務實例列表
listOfServers: http://localhost:8001
ribbon:
# 負載策略
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
# 設置它的服務實例信息來自配置文件, 如果不設置NIWSServerListClassName就會去euereka裏面找
NIWSServerListClassName: com.netflix.loadbalancer.ConfigurationBasedServerList
- Zuul會自動讀取註冊中心的已經註冊的服務。
user-service
服務會自動設置/user-service/**
這樣的路由,即/user-service/users
會被代理到user-service
服務的/users
請求。 zuul.ignoredServices
可以指定忽略註冊中心獲取的服務zuul.routes.<serviceId>=<path>
路由key使用一個服務名稱,對應一個路由路徑zuul.routes.<key>.serviceId=<serviceId>
指定一個服務對應路由路徑爲zuul.routes.<key>.path
zuul.routes.<key>.url=<url>
指定一個服務的url或者使用forward轉向Zuul服務的接口,對應路由路徑爲zuul.routes.<key>.path
zuul.routes.<ribbon>=<path>
使用自定義Ribbon實現路由
注意:<url>是服務的請求路徑,<path>是設置的代理路徑
<serviceId>和<url>不能同時存在,即一個路由要麼對應一個url,要麼對應一個服務
Zuul服務啓動完成後,可以訪問http://localhost:8301/routes
獲取路由列表
{
"/userApi/**": "user-service",
"/userApi2/**": "http://localhost:8002",
"/sms/**": "sms-service",
"/forward/**": "forward:/myZuul",
"/smsApi/**": "sms-service",
"/service-by-ribbon/**": "service-by-ribbon",
"/zuul-service/**": "zuul-service",
"/eureka-server/**": "eureka-server",
"/config-server/**": "config-server",
"/user-service/**": "user-service"
}
這樣我們就能根據不同的請求路徑實現路由和代理功能。
- 根據Eureka發現服務實現路由代理(
http://localhost:8301/user-service/user/exception
)
- 根據路由key實現路由代理(
http://localhost:8301/smsApi/sms
)
- 根據serviceId實現路由代理(
http://localhost:8301/userApi/user/exception
)
- 根據url實現路由代理(
http://localhost:8301/userApi2/user/exception
)
- 使用
zuul.routes.<routeName>.stripPrefix=false
在向服務發起請求時不會去掉path
前綴,即http://localhost:8301/sms
會代理到sms-service
服務的/sms
接口(如果stripPrefix
設置爲true
我們需要使用http://localhost:8301/sms/sms
才能正常訪問到這個接口)。
- forward將請求轉發至本地處理(
http://localhost:8301/forward/test
)會將請求轉發至本地的/myZuul/test
接口。
/myZuul/test
是zuul-service
的一個接口,如下:
@RestController
@RequestMapping("/myZuul")
public class MyZuulController {
@RequestMapping("/test")
public String test() {
return "Hello, you are visiting a local endpoint!";
}
}
- 使用Ribbon配置的服務(
localhost:8301/service-by-ribbon/sms
)
設置zuul.prefix=/api
後,意味着給所有的路由設置了一個全局的前綴,所有的請求前面增加/api
前綴即可。如http://localhost:8301/api/user-service/user/exception
,http://localhost:8301/api/smsApi/sms
等。
動態路由
Zuul結合SpringCloud配置中心,在修改路由配置信息後刷新配置可立即生效,無需重啓Zuul服務,這樣就實現了動態路由。
降級策略
當一個路由短路時,可以使用一個自定義的ZuulFallbackProvider
實現服務降級。在這個bean裏面你需要指定路由id並且返回一個ClientHttpReponse
作爲服務降級之後的請求結果。
下面是一個Zuul的降級實現
@Component
public class UserFallbackProvider implements ZuulFallbackProvider {
/**
* 對應的路由id,如果所有路由使用同一個fallback就返回*或者null
* @return
*/
@Override
public String getRoute() {
// return "user-service";
return "*";
}
@Override
public ClientHttpResponse fallbackResponse() {
ClientHttpResponse response = 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("invoke failed, fallback...".getBytes());
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.TEXT_PLAIN);
return headers;
}
};
return response;
}
}
getRoute()
方法返回的是路由id,如果希望這個降級策略對所有路由生效,返回null
或者*
即可。
訪問http://localhost:8301/userApi/user/timeout
或者http://localhost:8301/user-service/user/timeout
訪問http://localhost:8301/userApi2/user/timeout
會出現超時的錯誤。儘管我們的降級策略針對的是所有路由,但是/userApi2/**
走的是url配置的路由,ZuulFallbackProvider
只會對Ribbon進行尋路的路由生效。使用url
的路由在尋找原服務時使用的是SimpleHostRoutingFilter
。從Eureka中讀取的服務,使用
zuul.routes.<serviceId>=<path>
,zuul.routes.<key>.serviceId=<serviceId>
和zuul.routes.<ribbon>=<path>
這種方式配置的路由會使用RibbonRoutingFilter
進行尋路。RibbonRoutingFilter
會創建一個RibbonCommand
,RibbonCommand
繼承了HystrixExecutable
。
protected ClientHttpResponse forward(RibbonCommandContext context) throws Exception {
Map<String, Object> info = this.helper.debug(context.getMethod(),
context.getUri(), context.getHeaders(), context.getParams(),
context.getRequestEntity());
// 創建RibonCommand
RibbonCommand command = this.ribbonCommandFactory.create(context);
try {
ClientHttpResponse response = command.execute();
this.helper.appendDebug(info, response.getStatusCode().value(),
response.getHeaders());
return response;
}
catch (HystrixRuntimeException ex) {
return handleException(info, ex);
}
}
HttpClientRibbonCommandFactory.java
中創建HttpClientRibbonCommand
@Override
public HttpClientRibbonCommand create(final RibbonCommandContext context) {
// 根據serviceId獲取ZuulFallbackProvider
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));
}
Zuul Filter
Zuul進行代理時,會有一系列的Zuul Filter對Http請求的request和response進行封裝和操作。
一個Zuul Filter有下面四個要素:
- Type:類型。Zuul Filter的類型包括
pre
,routing
,post
和error
。routing
過濾器是在路由階段執行的,負責尋找原服務、請求轉發和返回接收。pre
和post
分別在routing
之前和之後執行。如果Zuul執行代理的過程中拋出ZuulException
異常,則會被error
過濾器捕獲並進行相應處理。
- Execution Order:執行順序。通過一個整型的值從小到大依次執行(相同類型過濾器間互相比較)。
- Criteria:執行條件。當滿足一定條件時,纔會執行該過濾器。
- Action:執行動作。當執行條件滿足時,進行的操作。
實現一個過濾器只要繼承ZuulFilter
,並實現filterType()
,filterOrder()
,shouldFilter()
和run()
四個方法。這些方法與上面的四個要素對應。
如果要禁用一個Zuul過濾器,只需要配置zuul.<SimpleClassName>.<filterType>.disable=true
,比如需要禁用org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter
需要配置zuul.SendResponseFilter.post.disable=true
。
下面我們使用一個pre
過濾器實現token驗證,如果Http header裏面沒有一個固定的token,則禁止訪問。
禁用Zuul默認的error
過濾器,設置固定的token和需要驗證的路由key名單
zuul:
# 禁用SpringCloud自帶的error filter
SendErrorFilter:
error:
disable: true
zuul-filter:
token-filter:
# 訪問時,需要進行認證的路由key
un-auth-routes:
- users
- smsApi
# 固定的token
static-token: xF2fdi8M
讀取自定義token配置信息
@Component
@ConfigurationProperties("zuulFilter.tokenFilter")
public class TokenValidateConfiguration {
// 在這個列表裏面存儲的routeId都是需要使用TokenValidateFilter過濾的
private List<String> unAuthRoutes;
// 給定的token
private String staticToken;
public List<String> getUnAuthRoutes() {
return unAuthRoutes;
}
public void setUnAuthRoutes(List<String> unAuthRoutes) {
this.unAuthRoutes = unAuthRoutes;
}
public String getStaticToken() {
return staticToken;
}
public void setStaticToken(String staticToken) {
this.staticToken = staticToken;
}
}
自定義過濾器
@Component
public class TokenValidateFilter extends ZuulFilter {
protected static final Logger logger = LoggerFactory.getLogger(TokenValidateFilter.class);
@Autowired
private TokenValidateConfiguration tvConfig;
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
return FilterConstants.PRE_DECORATION_FILTER_ORDER;
}
@Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
return tvConfig.getUnAuthRoutes().contains(ctx.get(FilterConstants.PROXY_KEY));
}
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String token = request.getHeader("Authorization");
if (token == null) {
logger.warn("Http Header Authorization is null");
forbidden();
return null;
}
String staticToken = tvConfig.getStaticToken();
if (StringUtils.isBlank(staticToken)) {
logger.warn("property zuulFilter.tokenFilter.staticToken was not set");
forbidden();
} else if (!staticToken.equals(token)) {
logger.warn("token is not valid");
forbidden();
}
return null;
}
/**
* 設置response的狀態碼爲403
*/
private void forbidden() {
// zuul中,將請求附帶的信息存在線程變量中。
RequestContext.getCurrentContext().setResponseStatusCode(HttpStatus.FORBIDDEN.value());
ReflectionUtils.rethrowRuntimeException(new ZuulException("token is not valid", HttpStatus.FORBIDDEN.value(),
"token校驗不通過"));
}
}
注意:如果使用zuul.routes.<serviceId>=<url>
方式配置的路由,則ctx.get(FilterConstants.PROXY_KEY)
會得到去掉頭尾的url(/smsApi/**
會得到smsApi
,/smsApi/target/**
會得到smsApi/target
),而並非路由key。所以之前配置文件中的路由
zuul:
routes:
sms-service: /smsApi/**
在請求的時候也需要攜帶token信息。
- 不攜帶token直接訪問
http://localhost:8301/userApi/user/exception
- 訪問
http://localhost:8301/userApi2/user/exception
時不需要進行攔截
- 攜帶正確的token訪問
http://localhost:8301/userApi/user/exception
說明我們的TokenValidateFilter
生效了。
類似地我們可以新建一個error
過濾器,當捕獲到ZuulException
時,返回一個JSON對象。
@Component
public class SendErrorRestFilter extends ZuulFilter {
private static final Logger logger = LoggerFactory.getLogger(SendErrorRestFilter.class);
@Override
public String filterType() {
return FilterConstants.ERROR_TYPE;
}
@Override
public int filterOrder() {
return FilterConstants.SEND_ERROR_FILTER_ORDER;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
RequestContext context = RequestContext.getCurrentContext();
Throwable throwable = getCause(context.getThrowable());
// 獲取response狀態碼
int status = context.getResponseStatusCode();
JSONObject info = new JSONObject();
info.put("code", "異常碼" + status);
info.put("message", throwable.getMessage());
// 記錄日誌
logger.warn("請求異常,被error filter攔截", context.getClass());
// 設置response
context.setResponseBody(info.toJSONString());
context.getResponse().setContentType("application/json;charset=UTF-8");
context.getResponse().setStatus(HttpStatus.OK.value());
// 處理了異常之後清空異常
context.remove("throwable");
return null;
}
private Throwable getCause(Throwable throwable) {
while (throwable.getCause() != null) {
throwable = throwable.getCause();
}
return throwable;
}
}
我們仍然關閉默認的error
過濾器,不使用token訪問http://localhost:8301/userApi/user/exception
。可以看到返回的狀態碼已經變成了200,且返回數據爲json。
使用Zuul上傳文件
如果使用了@EnableZuulProxy
代理路徑上傳文件,要儘量保證文件很小,避免超時。對於大文件,有一個替代路徑/zuul/**
可以繞過Spring DispatcherServlet
(避免Multipart處理)。即如果 zuul.routes.customers=/customers/**那樣你可以將大文件發送到“/ zuul / customers / *”。zuul.servletPath
使得servlet路徑外部化。如果大文件通過Ribbon上傳也需要提升超時設置,例如
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 60000
ribbon:
ConnectTimeout: 3000
ReadTimeout: 60000
我們在user-service
服務中增加一個/user/uploadImg
接口用於上傳文件。
@RequestMapping("/uploadImg")
public String uploadImg(MultipartFile file) throws IOException {
String srcName = file.getOriginalFilename();
String uuid = UUID.randomUUID().toString().replace("-", "");
String dstName = "D:/springcloud/upload/" + uuid +"-" + srcName;
File dstFile = new File(dstName);
File parentFile = dstFile.getParentFile();
if (!parentFile.exists()) {
parentFile.mkdirs();
}
try (InputStream in = file.getInputStream(); OutputStream out = new FileOutputStream(dstFile)) {
StreamUtils.copy(in, out);
}
return dstName;
}
我們對Multipart進行設置,允許大文件的上傳。
@Configuration
public class UploadConfig {
public static final String maxFileSize = "1024MB";
public static final String maxRequestSize = "2048MB";
@Bean
public MultipartConfigElement multipartConfigElement() {
MultipartConfigFactory factory = new MultipartConfigFactory();
// 單個文件最大
factory.setMaxFileSize(maxFileSize);
// 設置總上傳數據總大小
factory.setMaxRequestSize(maxRequestSize);
return factory.createMultipartConfig();
}
}
或者直接作如下設置
spring:
http:
multipart:
max-file-size: 1024MB
max-request-size: 2048MB
下面將使用一個大約25M的文件測試不同請求方式下的文件上傳。
- 直接向
user-service
服務發起請求(http://localhost:8002/user/uploadImg),上傳大文件成功。
- 使用Zuul代理到
user-service
服務上傳大文件失敗,由於文件過大請求被拒絕,後臺報錯信息如下(http://localhost:8301/userApi2/user/uploadImg
)。
org.apache.tomcat.util.http.fileupload.FileUploadBase$SizeLimitExceededException: the request was rejected because its size (26977350) exceeds the configured maximum (10485760)
- 使用Zuul代理添加
/zuul/**
前綴繞過SpringDispatcherServlet
進行文件上傳成功(http://localhost:8301/zuul/userApi2/user/uploadImg
)。
- 使用Zuul代理且Ribbon負載均衡的服務,如果不增加超時時間設置,將會自動降級(
http://localhost:8301/zuul/userApi/user/uploadImg
)。
- 使用Zuul代理且Ribbon負載均衡的服務,修改Hystrix和Ribbon的超時時間後,上傳文件成功(
http://localhost:8301/zuul/userApi/user/uploadImg
)。
相關代碼
SpringCloudDemo-Zuul