欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

程序员文章站 2023-11-29 09:08:22
前言 zuul 是netflix 提供的一个开源组件,致力于在云平台上提供动态路由,监控,弹性,安全等边缘服务的框架。也有很多公司使用它来作为网关的重要组成部分,碰巧今年...

前言

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 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)在上述代码中,我们可以发现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
@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;
  }
 }
}

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

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

二、关于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: 3000
hystrix:
 command:
 default:
  execution:
  isolation:
   thread:
   timeoutinmilliseconds: 3000
zuul:
 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;
@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 得到如下结果:

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

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

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

总结

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