Spring Cloud学习教程之Zuul统一异常处理与回退
前言
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; } } }
启动项目后,我们通过网关访问一下试试:
二、关于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 得到如下结果:
当我们访问:http://localhost:8088/order-server/sleep/1 就得到如下结果:
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对的支持。