首页 > 编程 > Java > 正文

Spring Cloud学习教程之Zuul统一异常处理与回退

2019-11-26 10:04:07
字体:
来源:转载
供稿:网友

前言

Zuul 是Netflix 提供的一个开源组件,致力于在云平台上提供动态路由,监控,弹性,安全等边缘服务的框架。也有很多公司使用它来作为网关的重要组成部分,碰巧今年公司的架构组决定自研一个网关产品,集动态路由,动态权限,限流配额等功能为一体,为其他部门的项目提供统一的外网调用管理,最终形成产品(这方面阿里其实已经有成熟的网关产品了,但是不太适用于个性化的配置,也没有集成权限和限流降级)。

本文主要给大家介绍了关于Spring Cloud Zuul统一异常处理与回退的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧。

一、Filter中统一异常处理

  其实在SpringCloud的Edgware SR2版本中对于ZuulFilter中的错误有统一的处理,但是在实际开发当中对于错误的响应方式,我想每个团队都有自己的处理规范。那么如何做到自定义的异常处理呢?

我们可以先参考一下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 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() 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)在上述代码中,我们可以发现filter已经将相关的错误信息放到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)错误处理完毕后,会转发到 xxx/error的地址来处理 

  那么我们可以来做个试验,我们在gateway-service项目模块里,创建一个会抛出异常的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@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; }}

  紧接着我们定义一个控制器,来做错误处理:

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("程序出错");  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;  } }}

  启动项目后,我们通过网关访问一下试试:

二、关于zuul回退的问题

1、关于zuul的超时问题:

  这个问题网上有很多解决方案,但是我还要贴一下源代码,请关注这个类 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

  根据源代码,我们先设置gateway-server的超时参数:

#全局的ribbon设置ribbon: ConnectTimeout: 3000 ReadTimeout: 3000hystrix: command: default:  execution:  isolation:   thread:   timeoutInMilliseconds: 3000zuul: host: connectTimeoutMillis: 10000

  当然也可以单独为order-server设置ribbon的超时参数:order-server.ribbon.xxxx=xxx , 为了演示zuul中的回退效果,我在这里把Hystrix超时时间设置短一点。当然最好不要将Hystrix默认的超时时间设置的比Ribbon的超时时间短,源码里遇到此情况已经给与我们警告了。

  那么我们在order-server下添加如下方法:

@GetMapping("/sleep/{sleepTime}") public String sleep(@PathVariable Long sleepTime) throws InterruptedException {  TimeUnit.SECONDS.sleep(sleepTime);  return "SUCCESS"; }

2、zuul的回退方法

我们可以实现ZuulFallbackProvider接口,实现代码:

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() {  //代表所有的路由都适配该设置  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 得到如下结果:

当我们访问:http://localhost:8088/order-server/sleep/1 就得到如下结果:

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对武林网的支持。

发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表