Spring Cloud學習教程之Zuul統(tǒng)一異常處理與回退
前言
Zuul 是Netflix 提供的一個開源組件,致力于在云平臺上提供動態(tài)路由,監(jiān)控,彈性,安全等邊緣服務的框架。也有很多公司使用它來作為網(wǎng)關的重要組成部分,碰巧今年公司的架構組決定自研一個網(wǎng)關產(chǎn)品,集動態(tài)路由,動態(tài)權限,限流配額等功能為一體,為其他部門的項目提供統(tǒng)一的外網(wǎng)調(diào)用管理,最終形成產(chǎn)品(這方面阿里其實已經(jīng)有成熟的網(wǎng)關產(chǎn)品了,但是不太適用于個性化的配置,也沒有集成權限和限流降級)。
本文主要給大家介紹了關于Spring Cloud Zuul統(tǒng)一異常處理與回退的相關內(nèi)容,分享出來供大家參考學習,下面話不多說了,來一起看看詳細的介紹吧。
一、Filter中統(tǒng)一異常處理
其實在SpringCloud的Edgware SR2版本中對于ZuulFilter中的錯誤有統(tǒng)一的處理,但是在實際開發(fā)當中對于錯誤的響應方式,我想每個團隊都有自己的處理規(guī)范。那么如何做到自定義的異常處理呢?
我們可以先參考一下SpringCloud提供的SendErrorFilter:
/*
* Copyright 2013-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.netflix.zuul.filters.post;
import javax.servlet.RequestDispatcher;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.netflix.zuul.util.ZuulRuntimeException;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.ERROR_TYPE;
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.SEND_ERROR_FILTER_ORDER;
/**
* Error {@link ZuulFilter} that forwards to /error (by default) if {@link RequestContext#getThrowable()} is not null.
*
* @author Spencer Gibb
*/
//TODO: move to error package in Edgware
public class SendErrorFilter extends ZuulFilter {
private static final Log log = LogFactory.getLog(SendErrorFilter.class);
protected static final String SEND_ERROR_FILTER_RAN = "sendErrorFilter.ran";
@Value("${error.path:/error}")
private String errorPath;
@Override
public String filterType() {
return ERROR_TYPE;
}
@Override
public int filterOrder() {
return SEND_ERROR_FILTER_ORDER;
}
@Override
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);
}
@Override
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;
}
ZuulException findZuulException(Throwable throwable) {
if (throwable.getCause() instanceof ZuulRuntimeException) {
// this was a failure initiated by one of the local filters
return (ZuulException) throwable.getCause().getCause();
}
if (throwable.getCause() instanceof ZuulException) {
// wrapped zuul exception
return (ZuulException) throwable.getCause();
}
if (throwable instanceof ZuulException) {
// exception thrown by zuul lifecycle
return (ZuulException) throwable;
}
// fallback, should never get here
return new ZuulException(throwable, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, null);
}
public void setErrorPath(String errorPath) {
this.errorPath = errorPath;
}
}
在這里我們可以找到幾個關鍵點:
1)在上述代碼中,我們可以發(fā)現(xiàn)filter已經(jīng)將相關的錯誤信息放到request當中了:
request.setAttribute("javax.servlet.error.status_code", exception.nStatusCode);
request.setAttribute("javax.servlet.error.exception", exception);
request.setAttribute("javax.servlet.error.message", exception.errorCause);
2)錯誤處理完畢后,會轉(zhuǎn)發(fā)到 xxx/error的地址來處理
那么我們可以來做個試驗,我們在gateway-service項目模塊里,創(chuàng)建一個會拋出異常的filter:
package com.hzgj.lyrk.springcloud.gateway.server.filter;
import com.netflix.zuul.ZuulFilter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class MyZuulFilter extends ZuulFilter {
@Override
public String filterType() {
return "post";
}
@Override
public int filterOrder() {
return 9;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
log.info("run error test ...");
throw new RuntimeException();
// return null;
}
}
緊接著我們定義一個控制器,來做錯誤處理:
package com.hzgj.lyrk.springcloud.gateway.server.filter;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
@RestController
public class ErrorHandler {
@GetMapping(value = "/error")
public ResponseEntity<ErrorBean> error(HttpServletRequest request) {
String message = request.getAttribute("javax.servlet.error.message").toString();
ErrorBean errorBean = new ErrorBean();
errorBean.setMessage(message);
errorBean.setReason("程序出錯");
return new ResponseEntity<>(errorBean, HttpStatus.BAD_GATEWAY);
}
private static class ErrorBean {
private String message;
private String reason;
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public String getReason() {
return reason;
}
public void setReason(String reason) {
this.reason = reason;
}
}
}
啟動項目后,我們通過網(wǎng)關訪問一下試試:

二、關于zuul回退的問題
1、關于zuul的超時問題:
這個問題網(wǎng)上有很多解決方案,但是我還要貼一下源代碼,請關注這個類 AbstractRibbonCommand,在這個類里集成了hystrix與ribbon。
/*
* Copyright 2013-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.springframework.cloud.netflix.zuul.filters.route.support;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.netflix.ribbon.RibbonClientConfiguration;
import org.springframework.cloud.netflix.ribbon.RibbonHttpResponse;
import org.springframework.cloud.netflix.ribbon.support.AbstractLoadBalancingClient;
import org.springframework.cloud.netflix.ribbon.support.ContextAwareRequest;
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties;
import org.springframework.cloud.netflix.zuul.filters.route.RibbonCommand;
import org.springframework.cloud.netflix.zuul.filters.route.RibbonCommandContext;
import org.springframework.cloud.netflix.zuul.filters.route.ZuulFallbackProvider;
import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider;
import org.springframework.http.client.ClientHttpResponse;
import com.netflix.client.AbstractLoadBalancerAwareClient;
import com.netflix.client.ClientRequest;
import com.netflix.client.config.DefaultClientConfigImpl;
import com.netflix.client.config.IClientConfig;
import com.netflix.client.config.IClientConfigKey;
import com.netflix.client.http.HttpResponse;
import com.netflix.config.DynamicIntProperty;
import com.netflix.config.DynamicPropertyFactory;
import com.netflix.hystrix.HystrixCommand;
import com.netflix.hystrix.HystrixCommandGroupKey;
import com.netflix.hystrix.HystrixCommandKey;
import com.netflix.hystrix.HystrixCommandProperties;
import com.netflix.hystrix.HystrixCommandProperties.ExecutionIsolationStrategy;
import com.netflix.hystrix.HystrixThreadPoolKey;
import com.netflix.zuul.constants.ZuulConstants;
import com.netflix.zuul.context.RequestContext;
/**
* @author Spencer Gibb
*/
public abstract class AbstractRibbonCommand<LBC extends AbstractLoadBalancerAwareClient<RQ, RS>, RQ extends ClientRequest, RS extends HttpResponse>
extends HystrixCommand<ClientHttpResponse> implements RibbonCommand {
private static final Log LOGGER = LogFactory.getLog(AbstractRibbonCommand.class);
protected final LBC client;
protected RibbonCommandContext context;
protected ZuulFallbackProvider zuulFallbackProvider;
protected IClientConfig config;
public AbstractRibbonCommand(LBC client, RibbonCommandContext context,
ZuulProperties zuulProperties) {
this("default", client, context, zuulProperties);
}
public AbstractRibbonCommand(String commandKey, LBC client,
RibbonCommandContext context, ZuulProperties zuulProperties) {
this(commandKey, client, context, zuulProperties, null);
}
public AbstractRibbonCommand(String commandKey, LBC client,
RibbonCommandContext context, ZuulProperties zuulProperties,
ZuulFallbackProvider fallbackProvider) {
this(commandKey, client, context, zuulProperties, fallbackProvider, null);
}
public AbstractRibbonCommand(String commandKey, LBC client,
RibbonCommandContext context, ZuulProperties zuulProperties,
ZuulFallbackProvider fallbackProvider, IClientConfig config) {
this(getSetter(commandKey, zuulProperties, config), client, context, fallbackProvider, config);
}
protected AbstractRibbonCommand(Setter setter, LBC client,
RibbonCommandContext context,
ZuulFallbackProvider fallbackProvider, IClientConfig config) {
super(setter);
this.client = client;
this.context = context;
this.zuulFallbackProvider = fallbackProvider;
this.config = config;
}
protected static HystrixCommandProperties.Setter createSetter(IClientConfig config, String commandKey, ZuulProperties zuulProperties) {
int hystrixTimeout = getHystrixTimeout(config, commandKey);
return HystrixCommandProperties.Setter().withExecutionIsolationStrategy(
zuulProperties.getRibbonIsolationStrategy()).withExecutionTimeoutInMilliseconds(hystrixTimeout);
}
protected static int getHystrixTimeout(IClientConfig config, String commandKey) {
int ribbonTimeout = getRibbonTimeout(config, commandKey);
DynamicPropertyFactory dynamicPropertyFactory = DynamicPropertyFactory.getInstance();
int defaultHystrixTimeout = dynamicPropertyFactory.getIntProperty("hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds",
0).get();
int commandHystrixTimeout = dynamicPropertyFactory.getIntProperty("hystrix.command." + commandKey + ".execution.isolation.thread.timeoutInMilliseconds",
0).get();
int hystrixTimeout;
if(commandHystrixTimeout > 0) {
hystrixTimeout = commandHystrixTimeout;
}
else if(defaultHystrixTimeout > 0) {
hystrixTimeout = defaultHystrixTimeout;
} else {
hystrixTimeout = ribbonTimeout;
}
if(hystrixTimeout < ribbonTimeout) {
LOGGER.warn("The Hystrix timeout of " + hystrixTimeout + "ms for the command " + commandKey +
" is set lower than the combination of the Ribbon read and connect timeout, " + ribbonTimeout + "ms.");
}
return hystrixTimeout;
}
protected static int getRibbonTimeout(IClientConfig config, String commandKey) {
int ribbonTimeout;
if (config == null) {
ribbonTimeout = RibbonClientConfiguration.DEFAULT_READ_TIMEOUT + RibbonClientConfiguration.DEFAULT_CONNECT_TIMEOUT;
} else {
int ribbonReadTimeout = getTimeout(config, commandKey, "ReadTimeout",
IClientConfigKey.Keys.ReadTimeout, RibbonClientConfiguration.DEFAULT_READ_TIMEOUT);
int ribbonConnectTimeout = getTimeout(config, commandKey, "ConnectTimeout",
IClientConfigKey.Keys.ConnectTimeout, RibbonClientConfiguration.DEFAULT_CONNECT_TIMEOUT);
int maxAutoRetries = getTimeout(config, commandKey, "MaxAutoRetries",
IClientConfigKey.Keys.MaxAutoRetries, DefaultClientConfigImpl.DEFAULT_MAX_AUTO_RETRIES);
int maxAutoRetriesNextServer = getTimeout(config, commandKey, "MaxAutoRetriesNextServer",
IClientConfigKey.Keys.MaxAutoRetriesNextServer, DefaultClientConfigImpl.DEFAULT_MAX_AUTO_RETRIES_NEXT_SERVER);
ribbonTimeout = (ribbonReadTimeout + ribbonConnectTimeout) * (maxAutoRetries + 1) * (maxAutoRetriesNextServer + 1);
}
return ribbonTimeout;
}
private static int getTimeout(IClientConfig config, String commandKey, String property, IClientConfigKey<Integer> configKey, int defaultValue) {
DynamicPropertyFactory dynamicPropertyFactory = DynamicPropertyFactory.getInstance();
return dynamicPropertyFactory.getIntProperty(commandKey + "." + config.getNameSpace() + "." + property, config.get(configKey, defaultValue)).get();
}
@Deprecated
//TODO remove in 2.0.x
protected static Setter getSetter(final String commandKey, ZuulProperties zuulProperties) {
return getSetter(commandKey, zuulProperties, null);
}
protected static Setter getSetter(final String commandKey,
ZuulProperties zuulProperties, IClientConfig config) {
// @formatter:off
Setter commandSetter = Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("RibbonCommand"))
.andCommandKey(HystrixCommandKey.Factory.asKey(commandKey));
final HystrixCommandProperties.Setter setter = createSetter(config, commandKey, zuulProperties);
if (zuulProperties.getRibbonIsolationStrategy() == ExecutionIsolationStrategy.SEMAPHORE){
final String name = ZuulConstants.ZUUL_EUREKA + commandKey + ".semaphore.maxSemaphores";
// we want to default to semaphore-isolation since this wraps
// 2 others commands that are already thread isolated
final DynamicIntProperty value = DynamicPropertyFactory.getInstance()
.getIntProperty(name, zuulProperties.getSemaphore().getMaxSemaphores());
setter.withExecutionIsolationSemaphoreMaxConcurrentRequests(value.get());
} else if (zuulProperties.getThreadPool().isUseSeparateThreadPools()) {
final String threadPoolKey = zuulProperties.getThreadPool().getThreadPoolKeyPrefix() + commandKey;
commandSetter.andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey(threadPoolKey));
}
return commandSetter.andCommandPropertiesDefaults(setter);
// @formatter:on
}
@Override
protected ClientHttpResponse run() throws Exception {
final RequestContext context = RequestContext.getCurrentContext();
RQ request = createRequest();
RS response;
boolean retryableClient = this.client instanceof AbstractLoadBalancingClient
&& ((AbstractLoadBalancingClient)this.client).isClientRetryable((ContextAwareRequest)request);
if (retryableClient) {
response = this.client.execute(request, config);
} else {
response = this.client.executeWithLoadBalancer(request, config);
}
context.set("ribbonResponse", response);
// Explicitly close the HttpResponse if the Hystrix command timed out to
// release the underlying HTTP connection held by the response.
//
if (this.isResponseTimedOut()) {
if (response != null) {
response.close();
}
}
return new RibbonHttpResponse(response);
}
@Override
protected ClientHttpResponse getFallback() {
if(zuulFallbackProvider != null) {
return getFallbackResponse();
}
return super.getFallback();
}
protected ClientHttpResponse getFallbackResponse() {
if (zuulFallbackProvider instanceof FallbackProvider) {
Throwable cause = getFailedExecutionException();
cause = cause == null ? getExecutionException() : cause;
if (cause == null) {
zuulFallbackProvider.fallbackResponse();
} else {
return ((FallbackProvider) zuulFallbackProvider).fallbackResponse(cause);
}
}
return zuulFallbackProvider.fallbackResponse();
}
public LBC getClient() {
return client;
}
public RibbonCommandContext getContext() {
return context;
}
protected abstract RQ createRequest() throws Exception;
}
請注意:getRibbonTimeout方法與getHystrixTimeout方法,其中這兩個方法 commandKey的值為路由的名稱,比如說我們訪問:http://localhost:8088/order-server/xxx來訪問order-server服務, 那么commandKey 就為order-server
根據(jù)源代碼,我們先設置gateway-server的超時參數(shù):
#全局的ribbon設置 ribbon: ConnectTimeout: 3000 ReadTimeout: 3000 hystrix: command: default: execution: isolation: thread: timeoutInMilliseconds: 3000 zuul: host: connectTimeoutMillis: 10000
當然也可以單獨為order-server設置ribbon的超時參數(shù):order-server.ribbon.xxxx=xxx , 為了演示zuul中的回退效果,我在這里把Hystrix超時時間設置短一點。當然最好不要將Hystrix默認的超時時間設置的比Ribbon的超時時間短,源碼里遇到此情況已經(jīng)給與我們警告了。
那么我們在order-server下添加如下方法:
@GetMapping("/sleep/{sleepTime}")
public String sleep(@PathVariable Long sleepTime) throws InterruptedException {
TimeUnit.SECONDS.sleep(sleepTime);
return "SUCCESS";
}
2、zuul的回退方法
我們可以實現(xiàn)ZuulFallbackProvider接口,實現(xiàn)代碼:
package com.hzgj.lyrk.springcloud.gateway.server.filter;
import com.google.common.collect.ImmutableMap;
import com.google.gson.GsonBuilder;
import org.springframework.cloud.netflix.zuul.filters.route.ZuulFallbackProvider;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Component;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.time.LocalDateTime;
import java.time.LocalTime;
@Component
public class FallBackHandler implements ZuulFallbackProvider {
@Override
public String getRoute() {
//代表所有的路由都適配該設置
return "*";
}
@Override
public ClientHttpResponse fallbackResponse() {
return 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 {
String result = new GsonBuilder().create().toJson(ImmutableMap.of("errorCode", 500, "content", "請求失敗", "time", LocalDateTime.now()));
return new ByteArrayInputStream(result.getBytes());
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return headers;
}
};
}
}
此時我們訪問:http://localhost:8088/order-server/sleep/6 得到如下結(jié)果:

當我們訪問:http://localhost:8088/order-server/sleep/1 就得到如下結(jié)果:

總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學習或者工作具有一定的參考學習價值,如果有疑問大家可以留言交流,謝謝大家對腳本之家的支持。
- Spring Cloud Zuul路由網(wǎng)關服務過濾實現(xiàn)代碼
- SpringCloud Zuul過濾器和谷歌Gauva實現(xiàn)限流
- SpringCloud Zuul實現(xiàn)動態(tài)路由
- SpringCloud Zuul在何種情況下使用Hystrix及問題小結(jié)
- 淺談Spring Cloud zuul http請求轉(zhuǎn)發(fā)原理
- 詳解SpringCloud Zuul過濾器返回值攔截
- SpringCloud實戰(zhàn)之Zuul網(wǎng)關服務
- spring cloud 使用Zuul 實現(xiàn)API網(wǎng)關服務問題
- Spring Cloud入門教程之Zuul實現(xiàn)API網(wǎng)關與請求過濾
- spring cloud zuul修改請求url的方法
- SpringCloud Zuul基本使用方法匯總
相關文章
Java數(shù)據(jù)結(jié)構最清晰圖解二叉樹前 中 后序遍歷
樹是一種重要的非線性數(shù)據(jù)結(jié)構,直觀地看,它是數(shù)據(jù)元素(在樹中稱為結(jié)點)按分支關系組織起來的結(jié)構,很象自然界中的樹那樣。樹結(jié)構在客觀世界中廣泛存在,如人類社會的族譜和各種社會組織機構都可用樹形象表示2022-01-01
SpringBoot自定義FailureAnalyzer詳解
這篇文章主要介紹了SpringBoot自定義FailureAnalyzer詳解,FailureAnalyzer是一種在啟動時攔截?exception?并將其轉(zhuǎn)換為?human-readable?消息的好方法,包含在故障分析中,需要的朋友可以參考下2023-11-11
Java中ByteArrayInputStream和ByteArrayOutputStream用法詳解
這篇文章主要介紹了Java中ByteArrayInputStream和ByteArrayOutputStream用法詳解,?ByteArrayInputStream?的內(nèi)部額外的定義了一個計數(shù)器,它被用來跟蹤?read()?方法要讀取的下一個字節(jié)2022-06-06
SpringBoot定時任務調(diào)度與爬蟲的配置實現(xiàn)
這篇文章主要介紹了SpringBoot定時任務調(diào)度與爬蟲的實現(xiàn),使用webmagic開發(fā)爬蟲,繼承PageProcessor接口編寫自己的處理類,process是定制爬蟲邏輯的核心接口,在這里編寫抽取邏輯,具體實現(xiàn)配置過程跟隨小編一起看看吧2022-01-01
springbooot使用google驗證碼的功能實現(xiàn)
這篇文章主要介紹了springbooot使用google驗證碼,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-05-05
解決java讀取EXCEL數(shù)據(jù)變成科學計數(shù)法的問題
這篇文章主要介紹了解決java讀取EXCEL數(shù)據(jù)變成科學計數(shù)法的問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-04-04

