Preface
Zuul is an open source component provided by Netflix, committed to providing dynamic routing, monitoring, resilience, security and other edge services on the cloud platform. There are also many companies that use it as an important part of the gateway. This year, the company's architecture group decided to develop a gateway product, integrating dynamic routing, dynamic permissions, current limit quota and other functions, providing unified external network call management for projects in other departments, and finally forming a product (Ali actually has mature gateway products in this regard, but it is not suitable for personalized configurations, and does not have integrated permissions and current limit downgrades).
This article mainly introduces the relevant content about Spring Cloud Zuul unified exception handling and fallback. It is shared for your reference and learning. I won’t say much below, let’s take a look at the detailed introduction together.
1. Unified exception handling in Filter
In fact, in the Edgware SR2 version of SpringCloud, there is a unified handling of errors in ZuulFilter, but in actual development, I think each team has its own processing specifications for the response methods of errors. So how to do custom exception handling?
We can first refer to the SendErrorFilter provided by SpringCloud:
/* * 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 Edgwarepublic 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() instance of 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 instance of 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; }}Here we can find several key points:
1) In the above code, we can find that filter has put the relevant error information into the 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) After the error is processed, it will be forwarded to the xxx/error address for processing
Then we can do an experiment. We create a filter that throws exceptions in the gateway-service project module:
package com.hzgj.lyrk.springcloud.gateway.server.filter;import com.netflix.zuul.ZuulFilter;import lombok.extern.slf4j.Slf4j;import org.springframework.stereotype.Component;@Component@Slf4jpublic 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; }}Then we define a controller to handle errors:
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;@RestControllerpublic 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("Program error"); 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; } }}After starting the project, let’s try accessing it through the gateway:
2. Questions about zuul fallback
1. Regarding the timeout problem of zuul:
There are many solutions to this problem online, but I also want to post the source code. Please pay attention to this class AbstractRibbonCommand, which integrates hystrix and ribbon in this class.
/* * 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 removed 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 instance of 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;}Please note: getRibbonTimeout method and getHystrixTimeout method, where the value of these two methods commandKey is the name of the route. For example, if we visit: http://localhost:8088/order-server/xxx to access the order-server service, then commandKey is order-server
According to the source code, we first set the gateway-server timeout parameters:
#Global ribbon settings ribbon: ConnectTimeout: 3000 ReadTimeout: 3000hystrix: command: default: execution: isolation: thread: timeoutInMilliseconds: 3000zuul: host: connectTimeoutMillis: 10000
Of course, you can also set the timeout parameters of ribbon for order-server separately: order-server.ribbon.xxxx=xxx. In order to demonstrate the fallback effect in zuul, I set the Hystrix timeout a little shorter here. Of course, it is best not to set the default timeout of Hystrix to be shorter than the timeout of Ribbon. This situation has been warned of us in the source code.
Then we add the following method under order-server:
@GetMapping("/sleep/{sleepTime}") public String sleep(@PathVariable Long sleepTime) throws InterruptedException { TimeUnit.SECONDS.sleep(sleepTime); return "SUCCESS"; }2. Zuul's fallback method
We can implement the ZuulFallbackProvider interface and implement the code:
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;@Componentpublic class FallBackHandler implements ZuulFallbackProvider { @Override public String getRoute() { //Represents that all routes are adapted to this setting 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", "Request failed", "time", LocalDateTime.now())); return new ByteArrayInputStream(result.getBytes()); } @Override public HttpHeaders getHeaders() { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); return headers; } }; }}At this time, we visit: http://localhost:8088/order-server/sleep/6 and get the following results:
When we visit: http://localhost:8088/order-server/sleep/1, we get the following result:
Summarize
The above is the entire content of this article. I hope that the content of this article has certain reference value for everyone's study or work. If you have any questions, you can leave a message to communicate. Thank you for your support to Wulin.com.